xref: /aosp_15_r20/external/cronet/net/tools/root_store_tool/root_store_tool.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1 // Copyright 2021 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <inttypes.h>
6 
7 #include <iostream>
8 #include <map>
9 #include <optional>
10 #include <set>
11 #include <string>
12 #include <string_view>
13 
14 #include "base/at_exit.h"
15 #include "base/base_paths.h"
16 #include "base/command_line.h"
17 #include "base/containers/span.h"
18 #include "base/files/file_path.h"
19 #include "base/files/file_util.h"
20 #include "base/logging.h"
21 #include "base/numerics/safe_conversions.h"
22 #include "base/path_service.h"
23 #include "base/strings/strcat.h"
24 #include "base/strings/string_number_conversions.h"
25 #include "base/strings/string_util.h"
26 #include "base/strings/stringprintf.h"
27 #include "base/strings/utf_string_conversions.h"
28 #include "build/build_config.h"
29 #include "crypto/openssl_util.h"
30 #include "crypto/sha2.h"
31 #include "net/cert/root_store_proto_full/root_store.pb.h"
32 #include "third_party/boringssl/src/include/openssl/bio.h"
33 #include "third_party/boringssl/src/include/openssl/err.h"
34 #include "third_party/boringssl/src/include/openssl/pem.h"
35 #include "third_party/protobuf/src/google/protobuf/text_format.h"
36 
37 using chrome_root_store::RootStore;
38 
39 namespace {
40 
41 // Returns a map from hex-encoded SHA-256 hash to DER certificate, or
42 // `std::nullopt` if not found.
DecodeCerts(std::string_view in)43 std::optional<std::map<std::string, std::string>> DecodeCerts(
44     std::string_view in) {
45   // TODO(https://crbug.com/1216547): net/cert/pem.h has a much nicer API, but
46   // it would require some build refactoring to avoid a circular dependency.
47   // This is assuming that the chrome trust store code goes in
48   // net/cert/internal, which it may not.
49   bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(in.data(), in.size()));
50   if (!bio) {
51     return std::nullopt;
52   }
53   std::map<std::string, std::string> certs;
54   for (;;) {
55     char* name;
56     char* header;
57     unsigned char* data;
58     long len;
59     if (!PEM_read_bio(bio.get(), &name, &header, &data, &len)) {
60       uint32_t err = ERR_get_error();
61       if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
62           ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
63         // Found the last PEM block.
64         break;
65       }
66       LOG(ERROR) << "Error reading PEM.";
67       return std::nullopt;
68     }
69     bssl::UniquePtr<char> scoped_name(name);
70     bssl::UniquePtr<char> scoped_header(header);
71     bssl::UniquePtr<unsigned char> scoped_data(data);
72     if (strcmp(name, "CERTIFICATE") != 0) {
73       LOG(ERROR) << "Found PEM block of type " << name
74                  << " instead of CERTIFICATE";
75       return std::nullopt;
76     }
77     std::string sha256_hex =
78         base::ToLowerASCII(base::HexEncode(crypto::SHA256Hash(
79             base::make_span(data, base::checked_cast<size_t>(len)))));
80     certs[sha256_hex] = std::string(data, data + len);
81   }
82   return std::move(certs);
83 }
84 
ReadTextRootStore(const base::FilePath & root_store_path,const base::FilePath & certs_path)85 std::optional<RootStore> ReadTextRootStore(
86     const base::FilePath& root_store_path,
87     const base::FilePath& certs_path) {
88   std::string root_store_text;
89   if (!base::ReadFileToString(base::MakeAbsoluteFilePath(root_store_path),
90                               &root_store_text)) {
91     LOG(ERROR) << "Could not read " << root_store_path;
92     return std::nullopt;
93   }
94 
95   RootStore root_store;
96   if (!google::protobuf::TextFormat::ParseFromString(root_store_text,
97                                                      &root_store)) {
98     LOG(ERROR) << "Could not parse " << root_store_path;
99     return std::nullopt;
100   }
101 
102   std::map<std::string, std::string> certs;
103   if (!certs_path.empty()) {
104     std::string certs_data;
105     if (!base::ReadFileToString(base::MakeAbsoluteFilePath(certs_path),
106                                 &certs_data)) {
107       LOG(ERROR) << "Could not read " << certs_path;
108       return std::nullopt;
109     }
110     auto certs_opt = DecodeCerts(certs_data);
111     if (!certs_opt) {
112       LOG(ERROR) << "Could not decode " << certs_path;
113       return std::nullopt;
114     }
115     certs = std::move(*certs_opt);
116   }
117 
118   // Replace the filenames with the actual certificate contents.
119   for (auto& anchor : *root_store.mutable_trust_anchors()) {
120     if (anchor.certificate_case() !=
121         chrome_root_store::TrustAnchor::kSha256Hex) {
122       continue;
123     }
124 
125     auto iter = certs.find(anchor.sha256_hex());
126     if (iter == certs.end()) {
127       LOG(ERROR) << "Could not find certificate " << anchor.sha256_hex();
128       return std::nullopt;
129     }
130 
131     // Remove the certificate from `certs`. This both checks for duplicate
132     // certificates and allows us to check for unused certificates later.
133     anchor.set_der(std::move(iter->second));
134     certs.erase(iter);
135   }
136 
137   if (!certs.empty()) {
138     LOG(ERROR) << "Unused certificate (SHA-256 hash " << certs.begin()->first
139                << ") in " << certs_path;
140     return std::nullopt;
141   }
142 
143   return std::move(root_store);
144 }
145 
SecondsFromEpochToBaseTime(int64_t t)146 std::string SecondsFromEpochToBaseTime(int64_t t) {
147   return base::StrCat({"base::Time::UnixEpoch() + base::Seconds(",
148                        base::NumberToString(t), ")"});
149 }
150 
VersionFromString(std::string_view version_str)151 std::string VersionFromString(std::string_view version_str) {
152   return base::StrCat({"\"", version_str, "\""});
153 }
154 
155 // Returns true if file was correctly written, false otherwise.
WriteRootCppFile(const RootStore & root_store,const base::FilePath cpp_path)156 bool WriteRootCppFile(const RootStore& root_store,
157                       const base::FilePath cpp_path) {
158   // Root store should have at least one trust anchors.
159   CHECK_GT(root_store.trust_anchors_size(), 0);
160 
161   const std::string kNulloptString = "std::nullopt";
162 
163   std::string string_to_write =
164       "// This file is auto-generated, DO NOT EDIT.\n\n";
165 
166   for (int i = 0; i < root_store.trust_anchors_size(); i++) {
167     const auto& anchor = root_store.trust_anchors(i);
168     // Every trust anchor at this point should have a DER.
169     CHECK(!anchor.der().empty());
170     std::string der = anchor.der();
171 
172     base::StringAppendF(&string_to_write,
173                         "constexpr uint8_t kChromeRootCert%d[] = {", i);
174 
175     // Convert each character to hex representation, escaped.
176     for (auto c : der) {
177       base::StringAppendF(&string_to_write, "0x%02xu,",
178                           static_cast<uint8_t>(c));
179     }
180 
181     // End struct
182     string_to_write += "};\n";
183 
184     if (anchor.constraints_size() > 0) {
185       base::StringAppendF(&string_to_write,
186                           "constexpr StaticChromeRootCertConstraints "
187                           "kChromeRootConstraints%d[] = {",
188                           i);
189 
190       std::vector<std::string> constraint_strings;
191       for (const auto& constraint : anchor.constraints()) {
192         std::vector<std::string> constraint_params;
193 
194         constraint_params.push_back(
195             constraint.has_sct_not_after_sec()
196                 ? SecondsFromEpochToBaseTime(constraint.sct_not_after_sec())
197                 : kNulloptString);
198 
199         constraint_params.push_back(
200             constraint.has_sct_all_after_sec()
201                 ? SecondsFromEpochToBaseTime(constraint.sct_all_after_sec())
202                 : kNulloptString);
203 
204         constraint_params.push_back(
205             constraint.has_min_version()
206                 ? VersionFromString(constraint.min_version())
207                 : kNulloptString);
208 
209         constraint_params.push_back(
210             constraint.has_max_version_exclusive()
211                 ? VersionFromString(constraint.max_version_exclusive())
212                 : kNulloptString);
213 
214         constraint_strings.push_back(
215             base::StrCat({"{", base::JoinString(constraint_params, ","), "}"}));
216       }
217 
218       string_to_write += base::JoinString(constraint_strings, ",");
219       string_to_write += "};\n";
220     }
221   }
222 
223   string_to_write += "constexpr ChromeRootCertInfo kChromeRootCertList[] = {\n";
224 
225   for (int i = 0; i < root_store.trust_anchors_size(); i++) {
226     const auto& anchor = root_store.trust_anchors(i);
227     base::StringAppendF(&string_to_write, "    {kChromeRootCert%d, ", i);
228     if (anchor.constraints_size() > 0) {
229       base::StringAppendF(&string_to_write, "kChromeRootConstraints%d", i);
230     } else {
231       string_to_write += "{}";
232     }
233     string_to_write += "},\n";
234   }
235   string_to_write += "};";
236 
237   base::StringAppendF(&string_to_write,
238                       "\n\n\nstatic const int64_t kRootStoreVersion = %" PRId64
239                       ";\n",
240                       root_store.version_major());
241   if (!base::WriteFile(cpp_path, string_to_write)) {
242     return false;
243   }
244   return true;
245 }
246 
247 // Returns true if file was correctly written, false otherwise.
WriteEvCppFile(const RootStore & root_store,const base::FilePath cpp_path)248 bool WriteEvCppFile(const RootStore& root_store,
249                     const base::FilePath cpp_path) {
250   // There should be at least one EV root.
251   CHECK_GT(root_store.trust_anchors_size(), 0);
252 
253   std::string string_to_write =
254       "// This file is auto-generated, DO NOT EDIT.\n\n"
255       "static const EVMetadata kEvRootCaMetadata[] = {\n";
256 
257   for (auto& anchor : root_store.trust_anchors()) {
258     // Every trust anchor at this point should have a DER.
259     CHECK(!anchor.der().empty());
260     if (anchor.ev_policy_oids_size() == 0) {
261       // The same input file is used for the Chrome Root Store and EV enabled
262       // certificates. Skip anchors that have no EV policy OIDs when generating
263       // the EV include file.
264       continue;
265     }
266 
267     std::string sha256_hash = crypto::SHA256HashString(anchor.der());
268 
269     // Begin struct. Assumed type of EVMetadata:
270     //
271     // struct EVMetadata {
272     //  static const size_t kMaxOIDsPerCA = 2;
273     //  SHA256HashValue fingerprint;
274     //  const base::StringPiece policy_oids[kMaxOIDsPerCA];
275     // };
276     string_to_write += "    {\n";
277     string_to_write += "        {{";
278 
279     int wrap_count = 0;
280     for (auto c : sha256_hash) {
281       if (wrap_count != 0) {
282         if (wrap_count % 11 == 0) {
283           string_to_write += ",\n          ";
284         } else {
285           string_to_write += ", ";
286         }
287       }
288       base::StringAppendF(&string_to_write, "0x%02x", static_cast<uint8_t>(c));
289       wrap_count++;
290     }
291 
292     string_to_write += "}},\n";
293     string_to_write += "        {\n";
294 
295     // struct expects exactly two policy oids, and we can only support 1 or 2
296     // policy OIDs. These checks will need to change if we ever merge the EV and
297     // Chrome Root Store textprotos.
298     const int kMaxPolicyOids = 2;
299     int oids_size = anchor.ev_policy_oids_size();
300     std::string hexencode_hash = base::HexEncode(sha256_hash);
301     if (oids_size > kMaxPolicyOids) {
302       PLOG(ERROR) << hexencode_hash << " has too many OIDs!";
303       return false;
304     }
305     for (int i = 0; i < kMaxPolicyOids; i++) {
306       std::string oid;
307       if (i < oids_size) {
308         oid = anchor.ev_policy_oids(i);
309       }
310       string_to_write += "            \"" + oid + "\",\n";
311     }
312 
313     // End struct
314     string_to_write += "        },\n";
315     string_to_write += "    },\n";
316   }
317   string_to_write += "};\n";
318   if (!base::WriteFile(cpp_path, string_to_write)) {
319     PLOG(ERROR) << "Error writing cpp include file";
320     return false;
321   }
322   return true;
323 }
324 
325 }  // namespace
326 
main(int argc,char ** argv)327 int main(int argc, char** argv) {
328   base::AtExitManager at_exit_manager;
329   base::CommandLine::Init(argc, argv);
330 
331   logging::LoggingSettings settings;
332   settings.logging_dest =
333       logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
334   logging::InitLogging(settings);
335 
336   crypto::EnsureOpenSSLInit();
337 
338   base::CommandLine& command_line = *base::CommandLine::ForCurrentProcess();
339   base::FilePath proto_path = command_line.GetSwitchValuePath("write-proto");
340   base::FilePath root_store_cpp_path =
341       command_line.GetSwitchValuePath("write-cpp-root-store");
342   base::FilePath ev_roots_cpp_path =
343       command_line.GetSwitchValuePath("write-cpp-ev-roots");
344   base::FilePath root_store_path =
345       command_line.GetSwitchValuePath("root-store");
346   base::FilePath certs_path = command_line.GetSwitchValuePath("certs");
347 
348   if ((proto_path.empty() && root_store_cpp_path.empty() &&
349        ev_roots_cpp_path.empty()) ||
350       root_store_path.empty() || command_line.HasSwitch("help")) {
351     std::cerr << "Usage: root_store_tool "
352               << "--root-store=TEXTPROTO_FILE "
353               << "[--certs=CERTS_FILE] "
354               << "[--write-proto=PROTO_FILE] "
355               << "[--write-cpp-root-store=CPP_FILE] "
356               << "[--write-cpp-ev-roots=CPP_FILE] " << std::endl;
357     return 1;
358   }
359 
360   std::optional<RootStore> root_store =
361       ReadTextRootStore(root_store_path, certs_path);
362   if (!root_store) {
363     return 1;
364   }
365 
366   // TODO(https://crbug.com/1216547): Figure out how to use the serialized
367   // proto to support component update.
368   // components/resources/ssl/ssl_error_assistant/push_proto.py
369   // does it through a GCS bucket (I think) so that might be an option.
370   if (!proto_path.empty()) {
371     std::string serialized;
372     if (!root_store->SerializeToString(&serialized)) {
373       LOG(ERROR) << "Error serializing root store proto"
374                  << root_store->DebugString();
375       return 1;
376     }
377     if (!base::WriteFile(proto_path, serialized)) {
378       PLOG(ERROR) << "Error writing serialized proto root store";
379       return 1;
380     }
381   }
382 
383   if (!root_store_cpp_path.empty() &&
384       !WriteRootCppFile(*root_store, root_store_cpp_path)) {
385     PLOG(ERROR) << "Error writing root store C++ include file";
386     return 1;
387   }
388   if (!ev_roots_cpp_path.empty() &&
389       !WriteEvCppFile(*root_store, ev_roots_cpp_path)) {
390     PLOG(ERROR) << "Error writing EV roots C++ include file";
391     return 1;
392   }
393 
394   return 0;
395 }
396