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