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

Side by Side Diff: net/base/hsts_preloaded_generate.go

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

Powered by Google App Engine
This is Rietveld 408576698