Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(28)

Side by Side Diff: net/tools/transport_security_state_generator/transport_security_state_generator.cc

Issue 2660793002: Add transport security state generator tests. (Closed)
Patch Set: export method for tests Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include <iostream> 5 #include <iostream>
6 6
7 #include <map> 7 #include <map>
8 #include <set> 8 #include <set>
9 #include <string> 9 #include <string>
10 #include <vector> 10 #include <vector>
11 11
12 #include "base/command_line.h" 12 #include "base/command_line.h"
13 #include "base/files/file_util.h" 13 #include "base/files/file_util.h"
14 #include "base/json/json_reader.h"
15 #include "base/path_service.h" 14 #include "base/path_service.h"
16 #include "base/strings/string_piece.h" 15 #include "base/strings/string_piece.h"
17 #include "base/strings/string_split.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/utf_string_conversions.h" 16 #include "base/strings/utf_string_conversions.h"
20 #include "base/values.h"
21 #include "crypto/openssl_util.h" 17 #include "crypto/openssl_util.h"
22 #include "net/tools/transport_security_state_generator/cert_util.h" 18 #include "net/tools/transport_security_state_generator/input_file_parsers.h"
23 #include "net/tools/transport_security_state_generator/pinset.h"
24 #include "net/tools/transport_security_state_generator/pinsets.h" 19 #include "net/tools/transport_security_state_generator/pinsets.h"
25 #include "net/tools/transport_security_state_generator/preloaded_state_generator .h" 20 #include "net/tools/transport_security_state_generator/preloaded_state_generator .h"
26 #include "net/tools/transport_security_state_generator/spki_hash.h"
27 #include "net/tools/transport_security_state_generator/transport_security_state_ entry.h" 21 #include "net/tools/transport_security_state_generator/transport_security_state_ entry.h"
28 #include "third_party/boringssl/src/include/openssl/x509v3.h"
29 22
30 using net::transport_security_state::TransportSecurityStateEntry;
31 using net::transport_security_state::TransportSecurityStateEntries; 23 using net::transport_security_state::TransportSecurityStateEntries;
32 using net::transport_security_state::Pinset;
33 using net::transport_security_state::Pinsets; 24 using net::transport_security_state::Pinsets;
34 using net::transport_security_state::PreloadedStateGenerator; 25 using net::transport_security_state::PreloadedStateGenerator;
35 using net::transport_security_state::DomainIDList; 26 using net::transport_security_state::DomainIDList;
36 using net::transport_security_state::SPKIHash;
37 27
38 namespace { 28 namespace {
39 29
40 // Print the command line help. 30 // Print the command line help.
41 void PrintHelp() { 31 void PrintHelp() {
42 std::cout << "transport_security_state_generator <json-file> <pins-file>" 32 std::cout << "transport_security_state_generator <json-file> <pins-file>"
43 << " <template-file> <output-file> [-v]" << std::endl; 33 << " <template-file> <output-file> [-v]" << std::endl;
44 } 34 }
45 35
46 // Parses the |json| string and copies the items under the "entries" key to
47 // |entries|, the pinsets under the "pinsets" key to |pinsets|, and the domain
48 // IDs under the "domain_ids" key to |domain_ids|.
49 //
50 // More info on the format can be found in
51 // net/http/transport_security_state_static.json
52 bool ParseJSON(const std::string& json,
53 TransportSecurityStateEntries* entries,
54 Pinsets* pinsets,
55 DomainIDList* domain_ids) {
56 std::unique_ptr<base::Value> value = base::JSONReader::Read(json);
57 base::DictionaryValue* dict_value = nullptr;
58 if (!value.get() || !value->GetAsDictionary(&dict_value)) {
59 std::cerr << "Could not parse the input JSON" << std::endl;
60 return false;
61 }
62
63 const base::ListValue* preload_entries = nullptr;
64 if (!dict_value->GetList("entries", &preload_entries)) {
65 std::cerr << "Could not parse the entries in the input JSON" << std::endl;
66 return false;
67 }
68
69 for (size_t i = 0; i < preload_entries->GetSize(); ++i) {
70 const base::DictionaryValue* parsed = nullptr;
71 if (!preload_entries->GetDictionary(i, &parsed)) {
72 std::cerr << "Could not parse entry " << i << std::endl;
73 return false;
74 }
75
76 std::unique_ptr<TransportSecurityStateEntry> entry(
77 new TransportSecurityStateEntry());
78
79 if (!parsed->GetString("name", &entry->hostname)) {
80 std::cerr << "Could not extract the name for entry " << i << std::endl;
81 return false;
82 }
83
84 parsed->GetBoolean("include_subdomains", &entry->include_subdomains);
85 std::string mode;
86 parsed->GetString("mode", &mode);
87 entry->force_https = (mode == "force-https");
88 parsed->GetBoolean("include_subdomains_for_pinning",
89 &entry->hpkp_include_subdomains);
90 parsed->GetString("pins", &entry->pinset);
91 parsed->GetBoolean("expect_ct", &entry->expect_ct);
92 parsed->GetString("expect_ct_report_uri", &entry->expect_ct_report_uri);
93 parsed->GetBoolean("expect_staple", &entry->expect_staple);
94 parsed->GetBoolean("include_subdomains_for_expect_staple",
95 &entry->expect_staple_include_subdomains);
96 parsed->GetString("expect_staple_report_uri",
97 &entry->expect_staple_report_uri);
98
99 entries->push_back(std::move(entry));
100 }
101
102 const base::ListValue* pinsets_list = nullptr;
103 if (!dict_value->GetList("pinsets", &pinsets_list)) {
104 std::cerr << "Could not parse the pinsets in the input JSON" << std::endl;
105 return false;
106 }
107
108 for (size_t i = 0; i < pinsets_list->GetSize(); ++i) {
109 const base::DictionaryValue* parsed = nullptr;
110 if (!pinsets_list->GetDictionary(i, &parsed)) {
111 std::cerr << "Could not parse pinset " << i << std::endl;
112 return false;
113 }
114
115 std::string name;
116 if (!parsed->GetString("name", &name)) {
117 std::cerr << "Could not extract the name for pinset " << i << std::endl;
118 return false;
119 }
120
121 std::string report_uri;
122 parsed->GetString("report_uri", &report_uri);
123
124 std::unique_ptr<Pinset> pinset(new Pinset(name, report_uri));
125
126 const base::ListValue* pinset_static_hashes_list = nullptr;
127 if (parsed->GetList("static_spki_hashes", &pinset_static_hashes_list)) {
128 for (size_t i = 0; i < pinset_static_hashes_list->GetSize(); ++i) {
129 std::string hash;
130 pinset_static_hashes_list->GetString(i, &hash);
131 pinset->AddStaticSPKIHash(hash);
132 }
133 }
134
135 const base::ListValue* pinset_bad_static_hashes_list = nullptr;
136 if (parsed->GetList("bad_static_spki_hashes",
137 &pinset_bad_static_hashes_list)) {
138 for (size_t i = 0; i < pinset_bad_static_hashes_list->GetSize(); ++i) {
139 std::string hash;
140 pinset_bad_static_hashes_list->GetString(i, &hash);
141 pinset->AddBadStaticSPKIHash(hash);
142 }
143 }
144
145 pinsets->RegisterPinset(std::move(pinset));
146 }
147
148 // TODO(Martijnc): Remove the domain IDs from the preload format.
149 // https://crbug.com/661206.
150 const base::ListValue* domain_ids_list = nullptr;
151 if (!dict_value->GetList("domain_ids", &domain_ids_list)) {
152 std::cerr << "Failed parsing JSON (domain_ids)" << std::endl;
153 return false;
154 }
155
156 for (size_t i = 0; i < domain_ids_list->GetSize(); ++i) {
157 std::string domain;
158 domain_ids_list->GetString(i, &domain);
159 domain_ids->push_back(domain);
160 }
161
162 return true;
163 }
164
165 bool IsImportantWordInCertificateName(base::StringPiece name) {
166 const char* const important_words[] = {"Universal", "Global", "EV", "G1",
167 "G2", "G3", "G4", "G5"};
168 for (auto* important_word : important_words) {
169 if (name == important_word) {
170 return true;
171 }
172 }
173 return false;
174 }
175
176 // Strips all characters not matched by the RegEx [A-Za-z0-9_] from |name| and
177 // returns the result.
178 std::string FilterName(base::StringPiece name) {
179 std::string filtered;
180 for (const char& character : name) {
181 if ((character >= '0' && character <= '9') ||
182 (character >= 'a' && character <= 'z') ||
183 (character >= 'A' && character <= 'Z') || character == '_') {
184 filtered += character;
185 }
186 }
187 return base::ToLowerASCII(filtered);
188 }
189
190 // Returns true if |pin_name| is a reasonable match for the certificate name
191 // |name|.
192 bool MatchCertificateName(base::StringPiece name, base::StringPiece pin_name) {
193 std::vector<base::StringPiece> words = base::SplitStringPiece(
194 name, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
195 if (words.empty()) {
196 std::cerr << "no words in certificate name" << std::endl;
197 return false;
198 }
199 base::StringPiece first_word = words[0];
200
201 if (first_word.ends_with(",")) {
202 first_word = first_word.substr(0, first_word.size() - 1);
203 }
204
205 if (first_word.starts_with("*.")) {
206 first_word = first_word.substr(2, first_word.size() - 2);
207 }
208
209 size_t pos = first_word.find('.');
210 if (pos != std::string::npos) {
211 first_word = first_word.substr(0, first_word.size() - pos);
212 }
213
214 pos = first_word.find('-');
215 if (pos != std::string::npos) {
216 first_word = first_word.substr(0, first_word.size() - pos);
217 }
218
219 if (first_word.empty()) {
220 std::cerr << "first word of certificate name is empty" << std::endl;
221 return false;
222 }
223
224 std::string filtered_word = FilterName(first_word);
225 first_word = filtered_word;
226 if (!base::EqualsCaseInsensitiveASCII(pin_name.substr(0, first_word.size()),
227 first_word)) {
228 std::cerr << "the first word of the certificate name ("
229 << first_word.as_string()
230 << ") isn't a prefix of the variable name ("
231 << pin_name.as_string() << ")" << std::endl;
232 return false;
233 }
234
235 for (size_t i = 0; i < words.size(); ++i) {
236 const base::StringPiece& word = words[i];
237 if (word == "Class" && (i + 1) < words.size()) {
238 std::string class_name = word.as_string();
239 words[i + 1].AppendToString(&class_name);
240
241 size_t pos = pin_name.find(class_name);
242 if (pos == std::string::npos) {
243 std::cerr << "class specification doesn't appear in the variable name"
244 << std::endl;
245 return false;
246 }
247 } else if (word.size() == 1 && word[0] >= '0' && word[0] <= '9') {
248 size_t pos = pin_name.find(word);
249 if (pos == std::string::npos) {
250 std::cerr << "number doesn't appear in the variable name" << std::endl;
251 return false;
252 }
253 } else if (IsImportantWordInCertificateName(word)) {
254 size_t pos = pin_name.find(word);
255 if (pos == std::string::npos) {
256 std::cerr << word.as_string() << " doesn't appear in the variable name"
257 << std::endl;
258 return false;
259 }
260 }
261 }
262
263 return true;
264 }
265
266 // Returns true iff |candidate| is not empty, the first character is in the
267 // range A-Z, and the remaining characters are in the ranges a-Z, 0-9, or '_'.
268 bool IsValidName(const std::string& candidate) {
269 if (candidate.empty() || candidate[0] < 'A' || candidate[0] > 'Z') {
270 return false;
271 }
272
273 bool isValid = true;
274 for (const char& character : candidate) {
275 isValid = (character >= '0' && character <= '9') ||
276 (character >= 'a' && character <= 'z') ||
277 (character >= 'A' && character <= 'Z') || character == '_';
278 if (!isValid) {
279 return false;
280 }
281 }
282 return true;
283 }
284
285 static const char kStartOfCert[] = "-----BEGIN CERTIFICATE";
286 static const char kStartOfPublicKey[] = "-----BEGIN PUBLIC KEY";
287 static const char kEndOfCert[] = "-----END CERTIFICATE";
288 static const char kEndOfPublicKey[] = "-----END PUBLIC KEY";
289 static const char kStartOfSHA256[] = "sha256/";
290
291 enum class CertificateParserState {
292 PRE_NAME,
293 POST_NAME,
294 IN_CERTIFICATE,
295 IN_PUBLIC_KEY
296 };
297
298 // Extracts SPKI information from the preloaded pins file. The SPKI's can be
299 // in the form of a PEM certificate, a PEM public key, or a BASE64 string.
300 //
301 // More info on the format can be found in
302 // net/http/transport_security_state_static.pins
303 bool ParseCertificatesFile(const std::string& certs_input, Pinsets* pinsets) {
304 std::istringstream input_stream(certs_input);
305 std::string line;
306 CertificateParserState current_state = CertificateParserState::PRE_NAME;
307
308 const base::CompareCase& compare_mode = base::CompareCase::INSENSITIVE_ASCII;
309 std::string name;
310 std::string buffer;
311 std::string subject_name;
312 bssl::UniquePtr<X509> certificate;
313 SPKIHash hash;
314
315 for (std::string line; std::getline(input_stream, line);) {
316 if (line[0] == '#') {
317 continue;
318 }
319
320 if (line.empty() && current_state == CertificateParserState::PRE_NAME) {
321 continue;
322 }
323
324 switch (current_state) {
325 case CertificateParserState::PRE_NAME:
326 if (!IsValidName(line)) {
327 std::cerr << "Invalid name in certificates file: " << line;
328 return false;
329 }
330 name = line;
331 current_state = CertificateParserState::POST_NAME;
332 break;
333 case CertificateParserState::POST_NAME:
334 if (base::StartsWith(line, kStartOfSHA256, compare_mode)) {
335 if (!hash.FromString(line)) {
336 std::cerr << "Invalid hash value in certificate file for " << name
337 << std::endl;
338 return false;
339 }
340
341 pinsets->RegisterSPKIHash(name, hash);
342 current_state = CertificateParserState::PRE_NAME;
343 } else if (base::StartsWith(line, kStartOfCert, compare_mode)) {
344 buffer = line + '\n';
345 current_state = CertificateParserState::IN_CERTIFICATE;
346 } else if (base::StartsWith(line, kStartOfPublicKey, compare_mode)) {
347 buffer = line + '\n';
348 current_state = CertificateParserState::IN_PUBLIC_KEY;
349 } else {
350 std::cerr << "Invalid value in certificates file for " << name
351 << std::endl;
352 return false;
353 }
354 break;
355 case CertificateParserState::IN_CERTIFICATE:
356 buffer += line + '\n';
357 if (!base::StartsWith(line, kEndOfCert, compare_mode)) {
358 continue;
359 }
360
361 certificate = GetX509CertificateFromPEM(buffer);
362 if (!certificate) {
363 std::cerr << "Could not parse certificate " << name << std::endl;
364 return false;
365 }
366
367 if (!CalculateSPKIHashFromCertificate(certificate.get(), &hash)) {
368 std::cerr << "Could not extract SPKI from certificate " << name
369 << std::endl;
370 return false;
371 }
372
373 if (!ExtractSubjectNameFromCertificate(certificate.get(),
374 &subject_name)) {
375 std::cerr << "Could not extract name from certificate " << name
376 << std::endl;
377 return false;
378 }
379
380 if (!MatchCertificateName(subject_name, name)) {
381 std::cerr << name << " is not a reasonable name for " << subject_name
382 << std::endl;
383 return false;
384 }
385
386 pinsets->RegisterSPKIHash(name, hash);
387 current_state = CertificateParserState::PRE_NAME;
388 break;
389 case CertificateParserState::IN_PUBLIC_KEY:
390 buffer += line + '\n';
391 if (!base::StartsWith(line, kEndOfPublicKey, compare_mode)) {
392 continue;
393 }
394
395 if (!CalculateSPKIHashFromKey(buffer, &hash)) {
396 std::cerr << "Parsing of the public key " << name << " failed"
397 << std::endl;
398 return false;
399 }
400
401 pinsets->RegisterSPKIHash(name, hash);
402 current_state = CertificateParserState::PRE_NAME;
403 break;
404 default:
405 DCHECK(false) << "Unknown parser state";
406 }
407 }
408
409 return true;
410 }
411
412 // Checks if there are pins with the same name or the same hash. 36 // Checks if there are pins with the same name or the same hash.
413 bool CheckForDuplicatePins(const Pinsets& pinsets) { 37 bool CheckForDuplicatePins(const Pinsets& pinsets) {
414 std::set<std::string> seen_names; 38 std::set<std::string> seen_names;
415 std::map<std::string, std::string> seen_hashes; 39 std::map<std::string, std::string> seen_hashes;
416 40
417 for (const auto& pin : pinsets.spki_hashes()) { 41 for (const auto& pin : pinsets.spki_hashes()) {
418 if (seen_names.find(pin.first) != seen_names.cend()) { 42 if (seen_names.find(pin.first) != seen_names.cend()) {
419 std::cerr << "Duplicate pin name " << pin.first << std::endl; 43 std::cerr << "Duplicate pin name " << pin.first << std::endl;
420 return false; 44 return false;
421 } 45 }
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after
547 args.push_back(base::WideToUTF8(arg)); 171 args.push_back(base::WideToUTF8(arg));
548 } 172 }
549 #else 173 #else
550 base::CommandLine::StringVector args = command_line.GetArgs(); 174 base::CommandLine::StringVector args = command_line.GetArgs();
551 #endif 175 #endif
552 if (args.size() < 4U) { 176 if (args.size() < 4U) {
553 PrintHelp(); 177 PrintHelp();
554 return 1; 178 return 1;
555 } 179 }
556 180
557 bool verbose = command_line.HasSwitch("v");
558
559 base::FilePath json_filepath = base::FilePath::FromUTF8Unsafe(argv[1]); 181 base::FilePath json_filepath = base::FilePath::FromUTF8Unsafe(argv[1]);
560 if (!base::PathExists(json_filepath)) { 182 if (!base::PathExists(json_filepath)) {
561 std::cerr << "Input JSON file doesn't exist." << std::endl; 183 std::cerr << "Input JSON file doesn't exist." << std::endl;
562 return 1; 184 return 1;
563 } 185 }
564 json_filepath = base::MakeAbsoluteFilePath(json_filepath); 186 json_filepath = base::MakeAbsoluteFilePath(json_filepath);
565 187
566 std::string json_input; 188 std::string json_input;
567 if (!base::ReadFileToString(json_filepath, &json_input)) { 189 if (!base::ReadFileToString(json_filepath, &json_input)) {
568 std::cerr << "Could not read input JSON file." << std::endl; 190 std::cerr << "Could not read input JSON file." << std::endl;
(...skipping 10 matching lines...) Expand all
579 std::string certs_input; 201 std::string certs_input;
580 if (!base::ReadFileToString(pins_filepath, &certs_input)) { 202 if (!base::ReadFileToString(pins_filepath, &certs_input)) {
581 std::cerr << "Could not read input pins file." << std::endl; 203 std::cerr << "Could not read input pins file." << std::endl;
582 return 1; 204 return 1;
583 } 205 }
584 206
585 TransportSecurityStateEntries entries; 207 TransportSecurityStateEntries entries;
586 Pinsets pinsets; 208 Pinsets pinsets;
587 DomainIDList domain_ids; 209 DomainIDList domain_ids;
588 210
589 if (!ParseCertificatesFile(certs_input, &pinsets)) { 211 std::vector<std::string> errors;
590 std::cerr << "Error while parsing the pins file." << std::endl; 212 if (!ParseCertificatesFile(certs_input, &pinsets, &errors) ||
591 return 1; 213 !ParseJSON(json_input, &entries, &pinsets, &domain_ids, &errors)) {
592 } 214 std::cerr << "Error while parsing the iput files." << std::endl;
593 if (!ParseJSON(json_input, &entries, &pinsets, &domain_ids)) { 215
594 std::cerr << "Error while parsing the JSON file." << std::endl; 216 for (const auto& error : errors) {
217 std::cerr << error << std::endl;
218 }
219
595 return 1; 220 return 1;
596 } 221 }
597 222
598 if (!CheckDuplicateEntries(entries) || !CheckNoopEntries(entries) || 223 if (!CheckDuplicateEntries(entries) || !CheckNoopEntries(entries) ||
599 !CheckSubdomainsFlags(entries) || !CheckForDuplicatePins(pinsets) || 224 !CheckSubdomainsFlags(entries) || !CheckForDuplicatePins(pinsets) ||
600 !CheckCertificatesInPinsets(pinsets)) { 225 !CheckCertificatesInPinsets(pinsets)) {
601 std::cerr << "Checks failed. Aborting." << std::endl; 226 std::cerr << "Checks failed. Aborting." << std::endl;
602 return 1; 227 return 1;
603 } 228 }
604 229
605 base::FilePath template_path = base::FilePath::FromUTF8Unsafe(argv[3]); 230 base::FilePath template_path = base::FilePath::FromUTF8Unsafe(argv[3]);
606 if (!base::PathExists(template_path)) { 231 if (!base::PathExists(template_path)) {
607 std::cerr << "Template file doesn't exist." << std::endl; 232 std::cerr << "Template file doesn't exist." << std::endl;
608 return 1; 233 return 1;
609 } 234 }
610 template_path = base::MakeAbsoluteFilePath(template_path); 235 template_path = base::MakeAbsoluteFilePath(template_path);
611 236
612 std::string preload_template; 237 std::string preload_template;
613 if (!base::ReadFileToString(template_path, &preload_template)) { 238 if (!base::ReadFileToString(template_path, &preload_template)) {
614 std::cerr << "Could not read template file." << std::endl; 239 std::cerr << "Could not read template file." << std::endl;
615 return 1; 240 return 1;
616 } 241 }
617 242
618 std::string result; 243 std::string result;
619 PreloadedStateGenerator generator; 244 PreloadedStateGenerator generator;
620 result = generator.Generate(preload_template, entries, domain_ids, pinsets, 245 if (!generator.Generate(preload_template, entries, domain_ids, pinsets,
621 verbose); 246 &result)) {
247 std::cerr << "Trie generation failed." << std::endl;
248 return 1;
249 }
622 250
623 base::FilePath output_path; 251 base::FilePath output_path;
624 output_path = base::FilePath::FromUTF8Unsafe(argv[4]); 252 output_path = base::FilePath::FromUTF8Unsafe(argv[4]);
625 253
626 if (base::WriteFile(output_path, result.c_str(), 254 if (base::WriteFile(output_path, result.c_str(),
627 static_cast<uint32_t>(result.size())) <= 0) { 255 static_cast<uint32_t>(result.size())) <= 0) {
628 std::cerr << "Failed to write output." << std::endl; 256 std::cerr << "Failed to write output." << std::endl;
629 return 1; 257 return 1;
630 } 258 }
631 259
260 if (command_line.HasSwitch("v")) {
261 std::cout << "Wrote trie containing " << entries.size()
262 << " entries, referencing " << pinsets.size() << " pinsets and "
263 << domain_ids.size() << " domain IDs to "
264 << output_path.AsUTF8Unsafe() << std::endl;
265 }
266
632 return 0; 267 return 0;
633 } 268 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698