Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 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 "net/tools/transport_security_state_generator/input_file_parsers.h" | |
| 6 | |
| 7 #include <sstream> | |
| 8 #include <vector> | |
| 9 | |
| 10 #include "base/json/json_reader.h" | |
| 11 #include "base/strings/string_number_conversions.h" | |
| 12 #include "base/strings/string_piece.h" | |
| 13 #include "base/strings/string_split.h" | |
| 14 #include "base/strings/string_util.h" | |
| 15 #include "base/values.h" | |
| 16 #include "net/tools/transport_security_state_generator/cert_util.h" | |
| 17 #include "net/tools/transport_security_state_generator/pinset.h" | |
| 18 #include "net/tools/transport_security_state_generator/pinsets.h" | |
| 19 #include "net/tools/transport_security_state_generator/spki_hash.h" | |
| 20 #include "third_party/boringssl/src/include/openssl/x509v3.h" | |
| 21 | |
| 22 namespace net { | |
| 23 | |
| 24 namespace transport_security_state { | |
| 25 | |
| 26 namespace { | |
| 27 | |
| 28 bool IsImportantWordInCertificateName(base::StringPiece name) { | |
| 29 const char* const important_words[] = {"Universal", "Global", "EV", "G1", | |
| 30 "G2", "G3", "G4", "G5"}; | |
| 31 for (auto* important_word : important_words) { | |
| 32 if (name == important_word) { | |
| 33 return true; | |
| 34 } | |
| 35 } | |
| 36 return false; | |
| 37 } | |
| 38 | |
| 39 // Strips all characters not matched by the RegEx [A-Za-z0-9_] from |name| and | |
| 40 // returns the result. | |
| 41 std::string FilterName(base::StringPiece name) { | |
| 42 std::string filtered; | |
| 43 for (const char& character : name) { | |
| 44 if ((character >= '0' && character <= '9') || | |
| 45 (character >= 'a' && character <= 'z') || | |
| 46 (character >= 'A' && character <= 'Z') || character == '_') { | |
| 47 filtered += character; | |
| 48 } | |
| 49 } | |
| 50 return base::ToLowerASCII(filtered); | |
| 51 } | |
| 52 | |
| 53 // Returns true if |pin_name| is a reasonable match for the certificate name | |
| 54 // |name|. | |
| 55 bool MatchCertificateName(base::StringPiece name, base::StringPiece pin_name) { | |
| 56 std::vector<base::StringPiece> words = base::SplitStringPiece( | |
| 57 name, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); | |
| 58 if (words.empty()) { | |
| 59 LOG(ERROR) << "No words in certificate name for pin " | |
| 60 << pin_name.as_string(); | |
| 61 return false; | |
| 62 } | |
| 63 base::StringPiece first_word = words[0]; | |
| 64 | |
| 65 if (first_word.ends_with(",")) { | |
| 66 first_word = first_word.substr(0, first_word.size() - 1); | |
| 67 } | |
| 68 | |
| 69 if (first_word.starts_with("*.")) { | |
| 70 first_word = first_word.substr(2, first_word.size() - 2); | |
| 71 } | |
| 72 | |
| 73 size_t pos = first_word.find('.'); | |
| 74 if (pos != std::string::npos) { | |
| 75 first_word = first_word.substr(0, first_word.size() - pos); | |
| 76 } | |
| 77 | |
| 78 pos = first_word.find('-'); | |
| 79 if (pos != std::string::npos) { | |
| 80 first_word = first_word.substr(0, first_word.size() - pos); | |
| 81 } | |
| 82 | |
| 83 if (first_word.empty()) { | |
| 84 LOG(ERROR) << "First word of certificate name (" << name.as_string() | |
| 85 << ") is empty"; | |
| 86 return false; | |
| 87 } | |
| 88 | |
| 89 std::string filtered_word = FilterName(first_word); | |
| 90 first_word = filtered_word; | |
| 91 if (!base::EqualsCaseInsensitiveASCII(pin_name.substr(0, first_word.size()), | |
| 92 first_word)) { | |
| 93 LOG(ERROR) << "The first word of the certificate name (" | |
| 94 << first_word.as_string() | |
| 95 << ") isn't a prefix of the variable name (" | |
| 96 << pin_name.as_string() << ")"; | |
| 97 return false; | |
| 98 } | |
| 99 | |
| 100 for (size_t i = 0; i < words.size(); ++i) { | |
| 101 const base::StringPiece& word = words[i]; | |
| 102 if (word == "Class" && (i + 1) < words.size()) { | |
| 103 std::string class_name = word.as_string(); | |
| 104 words[i + 1].AppendToString(&class_name); | |
| 105 | |
| 106 size_t pos = pin_name.find(class_name); | |
| 107 if (pos == std::string::npos) { | |
| 108 LOG(ERROR) | |
| 109 << "Certficate class specification doesn't appear in the variable " | |
| 110 "name (" | |
| 111 << pin_name.as_string() << ")"; | |
| 112 return false; | |
| 113 } | |
| 114 } else if (word.size() == 1 && word[0] >= '0' && word[0] <= '9') { | |
| 115 size_t pos = pin_name.find(word); | |
| 116 if (pos == std::string::npos) { | |
| 117 LOG(ERROR) << "Number doesn't appear in the certificate variable name (" | |
| 118 << pin_name.as_string() << ")"; | |
| 119 return false; | |
| 120 } | |
| 121 } else if (IsImportantWordInCertificateName(word)) { | |
| 122 size_t pos = pin_name.find(word); | |
| 123 if (pos == std::string::npos) { | |
| 124 LOG(ERROR) << word.as_string() + | |
| 125 " doesn't appear in the certificate variable name (" | |
| 126 << pin_name.as_string() << ")"; | |
| 127 return false; | |
| 128 } | |
| 129 } | |
| 130 } | |
| 131 | |
| 132 return true; | |
| 133 } | |
| 134 | |
| 135 // Returns true iff |candidate| is not empty, the first character is in the | |
| 136 // range A-Z, and the remaining characters are in the ranges a-Z, 0-9, or '_'. | |
| 137 bool IsValidName(base::StringPiece candidate) { | |
| 138 if (candidate.empty() || candidate[0] < 'A' || candidate[0] > 'Z') { | |
| 139 return false; | |
| 140 } | |
| 141 | |
| 142 bool isValid = true; | |
| 143 for (const char& character : candidate) { | |
| 144 isValid = (character >= '0' && character <= '9') || | |
| 145 (character >= 'a' && character <= 'z') || | |
| 146 (character >= 'A' && character <= 'Z') || character == '_'; | |
| 147 if (!isValid) { | |
| 148 return false; | |
| 149 } | |
| 150 } | |
| 151 return true; | |
| 152 } | |
| 153 | |
| 154 static const char kStartOfCert[] = "-----BEGIN CERTIFICATE"; | |
| 155 static const char kStartOfPublicKey[] = "-----BEGIN PUBLIC KEY"; | |
| 156 static const char kEndOfCert[] = "-----END CERTIFICATE"; | |
| 157 static const char kEndOfPublicKey[] = "-----END PUBLIC KEY"; | |
| 158 static const char kStartOfSHA256[] = "sha256/"; | |
| 159 | |
| 160 enum class CertificateParserState { | |
| 161 PRE_NAME, | |
| 162 POST_NAME, | |
| 163 IN_CERTIFICATE, | |
| 164 IN_PUBLIC_KEY | |
| 165 }; | |
| 166 | |
| 167 } // namespace | |
| 168 | |
| 169 bool ParseCertificatesFile(base::StringPiece certs_input, Pinsets* pinsets) { | |
| 170 std::string line; | |
| 171 CertificateParserState current_state = CertificateParserState::PRE_NAME; | |
| 172 | |
| 173 const base::CompareCase& compare_mode = base::CompareCase::INSENSITIVE_ASCII; | |
| 174 std::string name; | |
| 175 std::string buffer; | |
| 176 std::string subject_name; | |
| 177 bssl::UniquePtr<X509> certificate; | |
| 178 SPKIHash hash; | |
| 179 | |
| 180 for (const base::StringPiece& line : SplitStringPiece( | |
| 181 certs_input, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { | |
|
davidben
2017/04/04 23:35:32
Oh, my bad! It didn't occur to me that you'd have
| |
| 182 if (line[0] == '#') { | |
| 183 continue; | |
| 184 } | |
| 185 | |
| 186 if (line.empty() && current_state == CertificateParserState::PRE_NAME) { | |
| 187 continue; | |
| 188 } | |
| 189 | |
| 190 switch (current_state) { | |
| 191 case CertificateParserState::PRE_NAME: | |
| 192 if (!IsValidName(line)) { | |
| 193 LOG(ERROR) << "Invalid name in pins file: " << line; | |
| 194 return false; | |
| 195 } | |
| 196 name = line.as_string(); | |
| 197 current_state = CertificateParserState::POST_NAME; | |
| 198 break; | |
| 199 case CertificateParserState::POST_NAME: | |
| 200 if (base::StartsWith(line, kStartOfSHA256, compare_mode)) { | |
| 201 if (!hash.FromString(line)) { | |
| 202 LOG(ERROR) << "Invalid hash value in pins file for " << name; | |
| 203 return false; | |
| 204 } | |
| 205 | |
| 206 pinsets->RegisterSPKIHash(name, hash); | |
| 207 current_state = CertificateParserState::PRE_NAME; | |
| 208 } else if (base::StartsWith(line, kStartOfCert, compare_mode)) { | |
| 209 buffer = line.as_string() + '\n'; | |
| 210 current_state = CertificateParserState::IN_CERTIFICATE; | |
| 211 } else if (base::StartsWith(line, kStartOfPublicKey, compare_mode)) { | |
| 212 buffer = line.as_string() + '\n'; | |
| 213 current_state = CertificateParserState::IN_PUBLIC_KEY; | |
| 214 } else { | |
| 215 LOG(ERROR) << "Invalid value in pins file for " << name; | |
| 216 return false; | |
| 217 } | |
| 218 break; | |
| 219 case CertificateParserState::IN_CERTIFICATE: | |
| 220 buffer += line.as_string() + '\n'; | |
| 221 if (!base::StartsWith(line, kEndOfCert, compare_mode)) { | |
| 222 continue; | |
| 223 } | |
| 224 | |
| 225 certificate = GetX509CertificateFromPEM(buffer); | |
| 226 if (!certificate) { | |
| 227 LOG(ERROR) << "Could not parse certificate " << name; | |
| 228 return false; | |
| 229 } | |
| 230 | |
| 231 if (!CalculateSPKIHashFromCertificate(certificate.get(), &hash)) { | |
| 232 LOG(ERROR) << "Could not extract SPKI from certificate " << name; | |
| 233 return false; | |
| 234 } | |
| 235 | |
| 236 if (!ExtractSubjectNameFromCertificate(certificate.get(), | |
| 237 &subject_name)) { | |
| 238 LOG(ERROR) << "Could not extract name from certificate " << name; | |
| 239 return false; | |
| 240 } | |
| 241 | |
| 242 if (!MatchCertificateName(subject_name, name)) { | |
| 243 LOG(ERROR) << name << " is not a reasonable name for " | |
| 244 << subject_name; | |
| 245 return false; | |
| 246 } | |
| 247 | |
| 248 pinsets->RegisterSPKIHash(name, hash); | |
| 249 current_state = CertificateParserState::PRE_NAME; | |
| 250 break; | |
| 251 case CertificateParserState::IN_PUBLIC_KEY: | |
| 252 buffer += line.as_string() + '\n'; | |
| 253 if (!base::StartsWith(line, kEndOfPublicKey, compare_mode)) { | |
| 254 continue; | |
| 255 } | |
| 256 | |
| 257 if (!CalculateSPKIHashFromKey(buffer, &hash)) { | |
| 258 LOG(ERROR) << "Could not parse the public key for " << name; | |
| 259 return false; | |
| 260 } | |
| 261 | |
| 262 pinsets->RegisterSPKIHash(name, hash); | |
| 263 current_state = CertificateParserState::PRE_NAME; | |
| 264 break; | |
| 265 default: | |
| 266 DCHECK(false) << "Unknown parser state"; | |
| 267 } | |
| 268 } | |
| 269 | |
| 270 return true; | |
| 271 } | |
| 272 | |
| 273 bool ParseJSON(base::StringPiece json, | |
| 274 TransportSecurityStateEntries* entries, | |
| 275 Pinsets* pinsets, | |
| 276 DomainIDList* domain_ids) { | |
| 277 std::unique_ptr<base::Value> value = base::JSONReader::Read(json); | |
| 278 base::DictionaryValue* dict_value = nullptr; | |
| 279 if (!value.get() || !value->GetAsDictionary(&dict_value)) { | |
| 280 LOG(ERROR) << "Could not parse the input JSON file"; | |
| 281 return false; | |
| 282 } | |
| 283 | |
| 284 const base::ListValue* preload_entries = nullptr; | |
| 285 if (!dict_value->GetList("entries", &preload_entries)) { | |
| 286 LOG(ERROR) << "Could not parse the entries in the input JSON"; | |
| 287 return false; | |
| 288 } | |
| 289 | |
| 290 for (size_t i = 0; i < preload_entries->GetSize(); ++i) { | |
| 291 const base::DictionaryValue* parsed = nullptr; | |
| 292 if (!preload_entries->GetDictionary(i, &parsed)) { | |
| 293 LOG(ERROR) << "Could not parse entry " << base::SizeTToString(i) | |
| 294 << " in the input JSON"; | |
| 295 return false; | |
| 296 } | |
| 297 | |
| 298 std::unique_ptr<TransportSecurityStateEntry> entry( | |
| 299 new TransportSecurityStateEntry()); | |
| 300 | |
| 301 if (!parsed->GetString("name", &entry->hostname)) { | |
| 302 LOG(ERROR) << "Could not extract the hostname for entry " | |
| 303 << base::SizeTToString(i) << " from the input JSON"; | |
| 304 return false; | |
| 305 } | |
| 306 | |
| 307 parsed->GetBoolean("include_subdomains", &entry->include_subdomains); | |
| 308 std::string mode; | |
| 309 parsed->GetString("mode", &mode); | |
| 310 entry->force_https = (mode == "force-https"); | |
| 311 parsed->GetBoolean("include_subdomains_for_pinning", | |
| 312 &entry->hpkp_include_subdomains); | |
| 313 parsed->GetString("pins", &entry->pinset); | |
| 314 parsed->GetBoolean("expect_ct", &entry->expect_ct); | |
| 315 parsed->GetString("expect_ct_report_uri", &entry->expect_ct_report_uri); | |
| 316 parsed->GetBoolean("expect_staple", &entry->expect_staple); | |
| 317 parsed->GetBoolean("include_subdomains_for_expect_staple", | |
| 318 &entry->expect_staple_include_subdomains); | |
| 319 parsed->GetString("expect_staple_report_uri", | |
| 320 &entry->expect_staple_report_uri); | |
| 321 | |
| 322 entries->push_back(std::move(entry)); | |
| 323 } | |
| 324 | |
| 325 const base::ListValue* pinsets_list = nullptr; | |
| 326 if (!dict_value->GetList("pinsets", &pinsets_list)) { | |
| 327 LOG(ERROR) << "Could not parse the pinsets in the input JSON"; | |
| 328 return false; | |
| 329 } | |
| 330 | |
| 331 for (size_t i = 0; i < pinsets_list->GetSize(); ++i) { | |
| 332 const base::DictionaryValue* parsed = nullptr; | |
| 333 if (!pinsets_list->GetDictionary(i, &parsed)) { | |
| 334 LOG(ERROR) << "Could not parse pinset " << base::SizeTToString(i) | |
| 335 << " in the input JSON"; | |
| 336 return false; | |
| 337 } | |
| 338 | |
| 339 std::string name; | |
| 340 if (!parsed->GetString("name", &name)) { | |
| 341 LOG(ERROR) << "Could not extract the name for pinset " | |
| 342 << base::SizeTToString(i) << " from the input JSON"; | |
| 343 return false; | |
| 344 } | |
| 345 | |
| 346 std::string report_uri; | |
| 347 parsed->GetString("report_uri", &report_uri); | |
| 348 | |
| 349 std::unique_ptr<Pinset> pinset(new Pinset(name, report_uri)); | |
| 350 | |
| 351 const base::ListValue* pinset_static_hashes_list = nullptr; | |
| 352 if (parsed->GetList("static_spki_hashes", &pinset_static_hashes_list)) { | |
| 353 for (size_t i = 0; i < pinset_static_hashes_list->GetSize(); ++i) { | |
| 354 std::string hash; | |
| 355 pinset_static_hashes_list->GetString(i, &hash); | |
| 356 pinset->AddStaticSPKIHash(hash); | |
| 357 } | |
| 358 } | |
| 359 | |
| 360 const base::ListValue* pinset_bad_static_hashes_list = nullptr; | |
| 361 if (parsed->GetList("bad_static_spki_hashes", | |
| 362 &pinset_bad_static_hashes_list)) { | |
| 363 for (size_t i = 0; i < pinset_bad_static_hashes_list->GetSize(); ++i) { | |
| 364 std::string hash; | |
| 365 pinset_bad_static_hashes_list->GetString(i, &hash); | |
| 366 pinset->AddBadStaticSPKIHash(hash); | |
| 367 } | |
| 368 } | |
| 369 | |
| 370 pinsets->RegisterPinset(std::move(pinset)); | |
| 371 } | |
| 372 | |
| 373 // TODO(Martijnc): Remove the domain IDs from the preload format. | |
| 374 // https://crbug.com/661206. | |
| 375 const base::ListValue* domain_ids_list = nullptr; | |
| 376 if (!dict_value->GetList("domain_ids", &domain_ids_list)) { | |
| 377 LOG(ERROR) << "Could not parse the domain IDs in the input JSON"; | |
| 378 return false; | |
| 379 } | |
| 380 | |
| 381 for (size_t i = 0; i < domain_ids_list->GetSize(); ++i) { | |
| 382 std::string domain; | |
| 383 domain_ids_list->GetString(i, &domain); | |
| 384 domain_ids->push_back(domain); | |
| 385 } | |
| 386 | |
| 387 return true; | |
| 388 } | |
| 389 | |
| 390 } // namespace transport_security_state | |
| 391 | |
| 392 } // namespace net | |
| OLD | NEW |