| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 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 // This program converts the information in | |
| 6 // transport_security_state_static.json and | |
| 7 // transport_security_state_static.certs into | |
| 8 // transport_security_state_static.h. The input files contain information about | |
| 9 // public key pinning and HTTPS-only sites that is compiled into Chromium. | |
| 10 | |
| 11 // Run as: | |
| 12 // % go run transport_security_state_static_generate.go transport_security_state
_static.json transport_security_state_static.certs | |
| 13 // | |
| 14 // It will write transport_security_state_static.h | |
| 15 | |
| 16 package main | |
| 17 | |
| 18 import ( | |
| 19 "bufio" | |
| 20 "bytes" | |
| 21 "crypto/sha1" | |
| 22 "crypto/x509" | |
| 23 "encoding/base64" | |
| 24 "encoding/json" | |
| 25 "encoding/pem" | |
| 26 "errors" | |
| 27 "fmt" | |
| 28 "io" | |
| 29 "os" | |
| 30 "regexp" | |
| 31 "strings" | |
| 32 ) | |
| 33 | |
| 34 // A pin represents an entry in transport_security_state_static.certs. It's a | |
| 35 // name associated with a SubjectPublicKeyInfo hash and, optionally, a | |
| 36 // certificate. | |
| 37 type pin struct { | |
| 38 name string | |
| 39 cert *x509.Certificate | |
| 40 spkiHash []byte | |
| 41 spkiHashFunc string // i.e. "sha1" | |
| 42 } | |
| 43 | |
| 44 // preloaded represents the information contained in the | |
| 45 // transport_security_state_static.json file. This structure and the two | |
| 46 // following are used by the "json" package to parse the file. See the comments | |
| 47 // in transport_security_state_static.json for details. | |
| 48 type preloaded struct { | |
| 49 Pinsets []pinset `json:"pinsets"` | |
| 50 Entries []hsts `json:"entries"` | |
| 51 } | |
| 52 | |
| 53 type pinset struct { | |
| 54 Name string `json:"name"` | |
| 55 Include []string `json:"static_spki_hashes"` | |
| 56 Exclude []string `json:"bad_static_spki_hashes"` | |
| 57 } | |
| 58 | |
| 59 type hsts struct { | |
| 60 Name string `json:"name"` | |
| 61 Subdomains bool `json:"include_subdomains"` | |
| 62 Mode string `json:"mode"` | |
| 63 Pins string `json:"pins"` | |
| 64 SNIOnly bool `json:"snionly"` | |
| 65 } | |
| 66 | |
| 67 func main() { | |
| 68 if len(os.Args) != 3 { | |
| 69 fmt.Fprintf(os.Stderr, "Usage: %s <json file> <certificates file
>\n", os.Args[0]) | |
| 70 os.Exit(1) | |
| 71 } | |
| 72 | |
| 73 if err := process(os.Args[1], os.Args[2]); err != nil { | |
| 74 fmt.Fprintf(os.Stderr, "Conversion failed: %s\n", err.Error()) | |
| 75 os.Exit(1) | |
| 76 } | |
| 77 } | |
| 78 | |
| 79 func process(jsonFileName, certsFileName string) error { | |
| 80 jsonFile, err := os.Open(jsonFileName) | |
| 81 if err != nil { | |
| 82 return fmt.Errorf("failed to open input file: %s\n", err.Error()
) | |
| 83 } | |
| 84 defer jsonFile.Close() | |
| 85 | |
| 86 jsonBytes, err := removeComments(jsonFile) | |
| 87 if err != nil { | |
| 88 return fmt.Errorf("failed to remove comments from JSON: %s\n", e
rr.Error()) | |
| 89 } | |
| 90 | |
| 91 var preloaded preloaded | |
| 92 if err := json.Unmarshal(jsonBytes, &preloaded); err != nil { | |
| 93 return fmt.Errorf("failed to parse JSON: %s\n", err.Error()) | |
| 94 } | |
| 95 | |
| 96 certsFile, err := os.Open(certsFileName) | |
| 97 if err != nil { | |
| 98 return fmt.Errorf("failed to open input file: %s\n", err.Error()
) | |
| 99 } | |
| 100 defer certsFile.Close() | |
| 101 | |
| 102 pins, err := parseCertsFile(certsFile) | |
| 103 if err != nil { | |
| 104 return fmt.Errorf("failed to parse certificates file: %s\n", err
) | |
| 105 } | |
| 106 | |
| 107 if err := checkDuplicatePins(pins); err != nil { | |
| 108 return err | |
| 109 } | |
| 110 | |
| 111 if err := checkCertsInPinsets(preloaded.Pinsets, pins); err != nil { | |
| 112 return err | |
| 113 } | |
| 114 | |
| 115 outFile, err := os.OpenFile("transport_security_state_static.h", os.O_WR
ONLY|os.O_CREATE|os.O_TRUNC, 0644) | |
| 116 if err != nil { | |
| 117 return err | |
| 118 } | |
| 119 defer outFile.Close() | |
| 120 | |
| 121 out := bufio.NewWriter(outFile) | |
| 122 writeHeader(out) | |
| 123 writeCertsOutput(out, pins) | |
| 124 writeHSTSOutput(out, preloaded) | |
| 125 writeFooter(out) | |
| 126 out.Flush() | |
| 127 | |
| 128 return nil | |
| 129 } | |
| 130 | |
| 131 var newLine = []byte("\n") | |
| 132 var startOfCert = []byte("-----BEGIN CERTIFICATE") | |
| 133 var endOfCert = []byte("-----END CERTIFICATE") | |
| 134 var startOfSHA1 = []byte("sha1/") | |
| 135 | |
| 136 // nameRegexp matches valid pin names: an uppercase letter followed by zero or | |
| 137 // more letters and digits. | |
| 138 var nameRegexp = regexp.MustCompile("[A-Z][a-zA-Z0-9_]*") | |
| 139 | |
| 140 // commentRegexp matches lines that optionally start with whitespace | |
| 141 // followed by "//". | |
| 142 var commentRegexp = regexp.MustCompile("^[ \t]*//") | |
| 143 | |
| 144 // removeComments reads the contents of |r| and removes any lines beginning | |
| 145 // with optional whitespace followed by "//" | |
| 146 func removeComments(r io.Reader) ([]byte, error) { | |
| 147 var buf bytes.Buffer | |
| 148 in := bufio.NewReader(r) | |
| 149 | |
| 150 for { | |
| 151 line, isPrefix, err := in.ReadLine() | |
| 152 if isPrefix { | |
| 153 return nil, errors.New("line too long in JSON") | |
| 154 } | |
| 155 if err == io.EOF { | |
| 156 break | |
| 157 } | |
| 158 if err != nil { | |
| 159 return nil, err | |
| 160 } | |
| 161 if commentRegexp.Match(line) { | |
| 162 continue | |
| 163 } | |
| 164 buf.Write(line) | |
| 165 buf.Write(newLine) | |
| 166 } | |
| 167 | |
| 168 return buf.Bytes(), nil | |
| 169 } | |
| 170 | |
| 171 // parseCertsFile parses |inFile|, in the format of | |
| 172 // transport_security_state_static.certs. See the comments at the top of that | |
| 173 // file for details of the format. | |
| 174 func parseCertsFile(inFile io.Reader) ([]pin, error) { | |
| 175 const ( | |
| 176 PRENAME = iota | |
| 177 POSTNAME = iota | |
| 178 INCERT = iota | |
| 179 ) | |
| 180 | |
| 181 in := bufio.NewReader(inFile) | |
| 182 | |
| 183 lineNo := 0 | |
| 184 var pemCert []byte | |
| 185 state := PRENAME | |
| 186 var name string | |
| 187 var pins []pin | |
| 188 | |
| 189 for { | |
| 190 lineNo++ | |
| 191 line, isPrefix, err := in.ReadLine() | |
| 192 if isPrefix { | |
| 193 return nil, fmt.Errorf("line %d is too long to process\n
", lineNo) | |
| 194 } | |
| 195 if err == io.EOF { | |
| 196 break | |
| 197 } | |
| 198 if err != nil { | |
| 199 return nil, fmt.Errorf("error reading from input: %s\n",
err.Error()) | |
| 200 } | |
| 201 | |
| 202 if len(line) == 0 || line[0] == '#' { | |
| 203 continue | |
| 204 } | |
| 205 | |
| 206 switch state { | |
| 207 case PRENAME: | |
| 208 name = string(line) | |
| 209 if !nameRegexp.MatchString(name) { | |
| 210 return nil, fmt.Errorf("invalid name on line %d\
n", lineNo) | |
| 211 } | |
| 212 state = POSTNAME | |
| 213 case POSTNAME: | |
| 214 switch { | |
| 215 case bytes.HasPrefix(line, startOfSHA1): | |
| 216 hash, err := base64.StdEncoding.DecodeString(str
ing(line[len(startOfSHA1):])) | |
| 217 if err != nil { | |
| 218 return nil, fmt.Errorf("failed to decode
hash on line %d: %s\n", lineNo, err) | |
| 219 } | |
| 220 if len(hash) != 20 { | |
| 221 return nil, fmt.Errorf("bad SHA1 hash le
ngth on line %d: %s\n", lineNo, err) | |
| 222 } | |
| 223 pins = append(pins, pin{ | |
| 224 name: name, | |
| 225 spkiHashFunc: "sha1", | |
| 226 spkiHash: hash, | |
| 227 }) | |
| 228 state = PRENAME | |
| 229 continue | |
| 230 case bytes.HasPrefix(line, startOfCert): | |
| 231 pemCert = pemCert[:0] | |
| 232 pemCert = append(pemCert, line...) | |
| 233 pemCert = append(pemCert, '\n') | |
| 234 state = INCERT | |
| 235 default: | |
| 236 return nil, fmt.Errorf("line %d, after a name, i
s not a hash nor a certificate\n", lineNo) | |
| 237 } | |
| 238 case INCERT: | |
| 239 pemCert = append(pemCert, line...) | |
| 240 pemCert = append(pemCert, '\n') | |
| 241 if !bytes.HasPrefix(line, endOfCert) { | |
| 242 continue | |
| 243 } | |
| 244 | |
| 245 block, _ := pem.Decode(pemCert) | |
| 246 if block == nil { | |
| 247 return nil, fmt.Errorf("failed to decode certifi
cate ending on line %d\n", lineNo) | |
| 248 } | |
| 249 cert, err := x509.ParseCertificate(block.Bytes) | |
| 250 if err != nil { | |
| 251 return nil, fmt.Errorf("failed to parse certific
ate ending on line %d: %s\n", lineNo, err.Error()) | |
| 252 } | |
| 253 certName := cert.Subject.CommonName | |
| 254 if len(certName) == 0 { | |
| 255 certName = cert.Subject.Organization[0] + " " +
cert.Subject.OrganizationalUnit[0] | |
| 256 } | |
| 257 if err := matchNames(certName, name); err != nil { | |
| 258 return nil, fmt.Errorf("name failure on line %d:
%s\n%s -> %s\n", lineNo, err, certName, name) | |
| 259 } | |
| 260 h := sha1.New() | |
| 261 h.Write(cert.RawSubjectPublicKeyInfo) | |
| 262 pins = append(pins, pin{ | |
| 263 name: name, | |
| 264 cert: cert, | |
| 265 spkiHashFunc: "sha1", | |
| 266 spkiHash: h.Sum(nil), | |
| 267 }) | |
| 268 state = PRENAME | |
| 269 } | |
| 270 } | |
| 271 | |
| 272 return pins, nil | |
| 273 } | |
| 274 | |
| 275 // matchNames returns true if the given pin name is a reasonable match for the | |
| 276 // given CN. | |
| 277 func matchNames(name, v string) error { | |
| 278 words := strings.Split(name, " ") | |
| 279 if len(words) == 0 { | |
| 280 return errors.New("no words in certificate name") | |
| 281 } | |
| 282 firstWord := words[0] | |
| 283 if strings.HasSuffix(firstWord, ",") { | |
| 284 firstWord = firstWord[:len(firstWord)-1] | |
| 285 } | |
| 286 if pos := strings.Index(firstWord, "."); pos != -1 { | |
| 287 firstWord = firstWord[:pos] | |
| 288 } | |
| 289 if pos := strings.Index(firstWord, "-"); pos != -1 { | |
| 290 firstWord = firstWord[:pos] | |
| 291 } | |
| 292 if !strings.HasPrefix(v, firstWord) { | |
| 293 return errors.New("the first word of the certificate name isn't
a prefix of the variable name") | |
| 294 } | |
| 295 | |
| 296 for i, word := range words { | |
| 297 if word == "Class" && i+1 < len(words) { | |
| 298 if strings.Index(v, word+words[i+1]) == -1 { | |
| 299 return errors.New("class specification doesn't a
ppear in the variable name") | |
| 300 } | |
| 301 } else if len(word) == 1 && word[0] >= '0' && word[0] <= '9' { | |
| 302 if strings.Index(v, word) == -1 { | |
| 303 return errors.New("number doesn't appear in the
variable name") | |
| 304 } | |
| 305 } else if isImportantWordInCertificateName(word) { | |
| 306 if strings.Index(v, word) == -1 { | |
| 307 return errors.New(word + " doesn't appear in the
variable name") | |
| 308 } | |
| 309 } | |
| 310 } | |
| 311 | |
| 312 return nil | |
| 313 } | |
| 314 | |
| 315 // isImportantWordInCertificateName returns true if w must be found in any | |
| 316 // corresponding variable name. | |
| 317 func isImportantWordInCertificateName(w string) bool { | |
| 318 switch w { | |
| 319 case "Universal", "Global", "EV", "G1", "G2", "G3", "G4", "G5": | |
| 320 return true | |
| 321 } | |
| 322 return false | |
| 323 } | |
| 324 | |
| 325 // checkDuplicatePins returns an error if any pins have the same name or the sam
e hash. | |
| 326 func checkDuplicatePins(pins []pin) error { | |
| 327 seenNames := make(map[string]bool) | |
| 328 seenHashes := make(map[string]string) | |
| 329 | |
| 330 for _, pin := range pins { | |
| 331 if _, ok := seenNames[pin.name]; ok { | |
| 332 return fmt.Errorf("duplicate name: %s", pin.name) | |
| 333 } | |
| 334 seenNames[pin.name] = true | |
| 335 | |
| 336 strHash := string(pin.spkiHash) | |
| 337 if otherName, ok := seenHashes[strHash]; ok { | |
| 338 return fmt.Errorf("duplicate hash for %s and %s", pin.na
me, otherName) | |
| 339 } | |
| 340 seenHashes[strHash] = pin.name | |
| 341 } | |
| 342 | |
| 343 return nil | |
| 344 } | |
| 345 | |
| 346 // checkCertsInPinsets returns an error if | |
| 347 // a) unknown pins are mentioned in |pinsets| | |
| 348 // b) unused pins are given in |pins| | |
| 349 // c) a pinset name is used twice | |
| 350 func checkCertsInPinsets(pinsets []pinset, pins []pin) error { | |
| 351 pinNames := make(map[string]bool) | |
| 352 for _, pin := range pins { | |
| 353 pinNames[pin.name] = true | |
| 354 } | |
| 355 | |
| 356 usedPinNames := make(map[string]bool) | |
| 357 pinsetNames := make(map[string]bool) | |
| 358 | |
| 359 for _, pinset := range pinsets { | |
| 360 if _, ok := pinsetNames[pinset.Name]; ok { | |
| 361 return fmt.Errorf("duplicate pinset name: %s", pinset.Na
me) | |
| 362 } | |
| 363 pinsetNames[pinset.Name] = true | |
| 364 | |
| 365 var allPinNames []string | |
| 366 allPinNames = append(allPinNames, pinset.Include...) | |
| 367 allPinNames = append(allPinNames, pinset.Exclude...) | |
| 368 | |
| 369 for _, pinName := range allPinNames { | |
| 370 if _, ok := pinNames[pinName]; !ok { | |
| 371 return fmt.Errorf("unknown pin: %s", pinName) | |
| 372 } | |
| 373 usedPinNames[pinName] = true | |
| 374 } | |
| 375 } | |
| 376 | |
| 377 for pinName := range pinNames { | |
| 378 if _, ok := usedPinNames[pinName]; !ok { | |
| 379 return fmt.Errorf("unused pin: %s", pinName) | |
| 380 } | |
| 381 } | |
| 382 | |
| 383 return nil | |
| 384 } | |
| 385 | |
| 386 func writeHeader(out *bufio.Writer) { | |
| 387 out.WriteString(`// Copyright (c) 2012 The Chromium Authors. All rights
reserved. | |
| 388 // Use of this source code is governed by a BSD-style license that can be | |
| 389 // found in the LICENSE file. | |
| 390 | |
| 391 // This file is automatically generated by transport_security_state_static_gener
ate.go | |
| 392 | |
| 393 #ifndef NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_ | |
| 394 #define NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_ | |
| 395 #pragma once | |
| 396 | |
| 397 `) | |
| 398 | |
| 399 } | |
| 400 | |
| 401 func writeFooter(out *bufio.Writer) { | |
| 402 out.WriteString("#endif // NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_\n
") | |
| 403 } | |
| 404 | |
| 405 func writeCertsOutput(out *bufio.Writer, pins []pin) { | |
| 406 out.WriteString(`// These are SubjectPublicKeyInfo hashes for public key
pinning. The | |
| 407 // hashes are base64 encoded, SHA1 digests. | |
| 408 | |
| 409 `) | |
| 410 | |
| 411 for _, pin := range pins { | |
| 412 if pin.cert != nil { | |
| 413 out.WriteString("#if 0\n") | |
| 414 pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: p
in.cert.Raw}) | |
| 415 out.WriteString("#endif\n") | |
| 416 } | |
| 417 fmt.Fprintf(out, "static const char kSPKIHash_%s[] =\n", pin.nam
e) | |
| 418 fmt.Fprintf(out, " \"%s/%s\";\n\n", pin.spkiHashFunc, base64.
StdEncoding.EncodeToString(pin.spkiHash)) | |
| 419 } | |
| 420 } | |
| 421 | |
| 422 // uppercaseFirstLetter returns s with the first letter uppercased. | |
| 423 func uppercaseFirstLetter(s string) string { | |
| 424 // We need to find the index of the second code-point, which may not be | |
| 425 // one. | |
| 426 for i := range s { | |
| 427 if i == 0 { | |
| 428 continue | |
| 429 } | |
| 430 return strings.ToUpper(s[:i]) + s[i:] | |
| 431 } | |
| 432 return strings.ToUpper(s) | |
| 433 } | |
| 434 | |
| 435 func writeListOfPins(w io.Writer, name string, pinNames []string) { | |
| 436 fmt.Fprintf(w, "static const char* const %s[] = {\n", name) | |
| 437 for _, pinName := range pinNames { | |
| 438 fmt.Fprintf(w, " kSPKIHash_%s,\n", pinName) | |
| 439 } | |
| 440 fmt.Fprintf(w, " NULL,\n};\n") | |
| 441 } | |
| 442 | |
| 443 // toDNS returns a string converts the domain name |s| into C-escaped, | |
| 444 // length-prefixed form and also returns the length of the interpreted string. | |
| 445 // i.e. for an input "example.com" it will return "\\007example\\003com", 13. | |
| 446 func toDNS(s string) (string, int) { | |
| 447 labels := strings.Split(s, ".") | |
| 448 | |
| 449 var name string | |
| 450 var l int | |
| 451 for _, label := range labels { | |
| 452 if len(label) > 63 { | |
| 453 panic("DNS label too long") | |
| 454 } | |
| 455 name += fmt.Sprintf("\\%03o", len(label)) | |
| 456 name += label | |
| 457 l += len(label) + 1 | |
| 458 } | |
| 459 l += 1 // For the length of the root label. | |
| 460 | |
| 461 return name, l | |
| 462 } | |
| 463 | |
| 464 // domainConstant converts the domain name |s| into a string of the form | |
| 465 // "DOMAIN_" + uppercase last two labels. | |
| 466 func domainConstant(s string) string { | |
| 467 labels := strings.Split(s, ".") | |
| 468 gtld := strings.ToUpper(labels[len(labels)-1]) | |
| 469 domain := strings.Replace(strings.ToUpper(labels[len(labels)-2]), "-", "
_", -1) | |
| 470 | |
| 471 return fmt.Sprintf("DOMAIN_%s_%s", domain, gtld) | |
| 472 } | |
| 473 | |
| 474 func writeHSTSEntry(out *bufio.Writer, entry hsts) { | |
| 475 dnsName, dnsLen := toDNS(entry.Name) | |
| 476 domain := "DOMAIN_NOT_PINNED" | |
| 477 pinsetName := "kNoPins" | |
| 478 if len(entry.Pins) > 0 { | |
| 479 pinsetName = fmt.Sprintf("k%sPins", uppercaseFirstLetter(entry.P
ins)) | |
| 480 domain = domainConstant(entry.Name) | |
| 481 } | |
| 482 fmt.Fprintf(out, " {%d, %t, \"%s\", %t, %s, %s },\n", dnsLen, entry.Sub
domains, dnsName, entry.Mode == "force-https", pinsetName, domain) | |
| 483 } | |
| 484 | |
| 485 func writeHSTSOutput(out *bufio.Writer, hsts preloaded) error { | |
| 486 out.WriteString(`// The following is static data describing the hosts th
at are hardcoded with | |
| 487 // certificate pins or HSTS information. | |
| 488 | |
| 489 // kNoRejectedPublicKeys is a placeholder for when no public keys are rejected. | |
| 490 static const char* const kNoRejectedPublicKeys[] = { | |
| 491 NULL, | |
| 492 }; | |
| 493 | |
| 494 `) | |
| 495 | |
| 496 for _, pinset := range hsts.Pinsets { | |
| 497 name := uppercaseFirstLetter(pinset.Name) | |
| 498 acceptableListName := fmt.Sprintf("k%sAcceptableCerts", name) | |
| 499 writeListOfPins(out, acceptableListName, pinset.Include) | |
| 500 | |
| 501 rejectedListName := "kNoRejectedPublicKeys" | |
| 502 if len(pinset.Exclude) > 0 { | |
| 503 rejectedListName = fmt.Sprintf("k%sRejectedCerts", name) | |
| 504 writeListOfPins(out, rejectedListName, pinset.Exclude) | |
| 505 } | |
| 506 fmt.Fprintf(out, `#define k%sPins { \ | |
| 507 %s, \ | |
| 508 %s, \ | |
| 509 } | |
| 510 | |
| 511 `, name, acceptableListName, rejectedListName) | |
| 512 } | |
| 513 | |
| 514 out.WriteString(`#define kNoPins {\ | |
| 515 NULL, NULL, \ | |
| 516 } | |
| 517 | |
| 518 static const struct HSTSPreload kPreloadedSTS[] = { | |
| 519 `) | |
| 520 | |
| 521 for _, entry := range hsts.Entries { | |
| 522 if entry.SNIOnly { | |
| 523 continue | |
| 524 } | |
| 525 writeHSTSEntry(out, entry) | |
| 526 } | |
| 527 | |
| 528 out.WriteString(`}; | |
| 529 static const size_t kNumPreloadedSTS = ARRAYSIZE_UNSAFE(kPreloadedSTS); | |
| 530 | |
| 531 static const struct HSTSPreload kPreloadedSNISTS[] = { | |
| 532 `) | |
| 533 | |
| 534 for _, entry := range hsts.Entries { | |
| 535 if !entry.SNIOnly { | |
| 536 continue | |
| 537 } | |
| 538 writeHSTSEntry(out, entry) | |
| 539 } | |
| 540 | |
| 541 out.WriteString(`}; | |
| 542 static const size_t kNumPreloadedSNISTS = ARRAYSIZE_UNSAFE(kPreloadedSNISTS); | |
| 543 | |
| 544 `) | |
| 545 | |
| 546 return nil | |
| 547 } | |
| OLD | NEW |