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

Side by Side Diff: net/base/transport_security_state_static_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, 8 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
« no previous file with comments | « net/base/transport_security_state_static.json ('k') | net/net.gyp » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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(`// This file is automatically generated by transport_se curity_state_static_generate.go
388
389 #ifndef NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_
390 #define NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_
391 #pragma once
392
393 `)
394
395 }
396
397 func writeFooter(out *bufio.Writer) {
398 out.WriteString("#endif // NET_BASE_TRANSPORT_SECURITY_STATE_STATIC_H_\n ")
399 }
400
401 func writeCertsOutput(out *bufio.Writer, pins []pin) {
402 out.WriteString(`// These are SubjectPublicKeyInfo hashes for public key pinning. The
403 // hashes are base64 encoded, SHA1 digests.
404
405 `)
406
407 for _, pin := range pins {
408 if pin.cert != nil {
409 out.WriteString("#if 0\n")
410 pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: p in.cert.Raw})
411 out.WriteString("#endif\n")
412 }
413 fmt.Fprintf(out, "static const char kSPKIHash_%s[] =\n", pin.nam e)
414 fmt.Fprintf(out, " \"%s/%s\";\n\n", pin.spkiHashFunc, base64. StdEncoding.EncodeToString(pin.spkiHash))
415 }
416 }
417
418 // uppercaseFirstLetter returns s with the first letter uppercased.
419 func uppercaseFirstLetter(s string) string {
420 // We need to find the index of the second code-point, which may not be
421 // one.
422 for i := range s {
423 if i == 0 {
424 continue
425 }
426 return strings.ToUpper(s[:i]) + s[i:]
427 }
428 return strings.ToUpper(s)
429 }
430
431 func writeListOfPins(w io.Writer, name string, pinNames []string) {
432 fmt.Fprintf(w, "static const char* const %s[] = {\n", name)
433 for _, pinName := range pinNames {
434 fmt.Fprintf(w, " kSPKIHash_%s,\n", pinName)
435 }
436 fmt.Fprintf(w, " NULL,\n};\n")
437 }
438
439 // toDNS returns a string converts the domain name |s| into C-escaped,
440 // length-prefixed form and also returns the length of the interpreted string.
441 // i.e. for an input "example.com" it will return "\\007example\\003com", 13.
442 func toDNS(s string) (string, int) {
443 labels := strings.Split(s, ".")
444
445 var name string
446 var l int
447 for _, label := range labels {
448 if len(label) > 63 {
449 panic("DNS label too long")
450 }
451 name += fmt.Sprintf("\\%03o", len(label))
452 name += label
453 l += len(label) + 1
454 }
455 l += 1 // For the length of the root label.
456
457 return name, l
458 }
459
460 // domainConstant converts the domain name |s| into a string of the form
461 // "DOMAIN_" + uppercase last two labels.
462 func domainConstant(s string) string {
463 labels := strings.Split(s, ".")
464 gtld := strings.ToUpper(labels[len(labels)-1])
465 domain := strings.Replace(strings.ToUpper(labels[len(labels)-2]), "-", " _", -1)
466
467 return fmt.Sprintf("DOMAIN_%s_%s", domain, gtld)
468 }
469
470 func writeHSTSEntry(out *bufio.Writer, entry hsts) {
471 dnsName, dnsLen := toDNS(entry.Name)
472 domain := "DOMAIN_NOT_PINNED"
473 pinsetName := "kNoPins"
474 if len(entry.Pins) > 0 {
475 pinsetName = fmt.Sprintf("k%sPins", uppercaseFirstLetter(entry.P ins))
476 domain = domainConstant(entry.Name)
477 }
478 fmt.Fprintf(out, " {%d, %t, \"%s\", %t, %s, %s },\n", dnsLen, entry.Sub domains, dnsName, entry.Mode == "force-https", pinsetName, domain)
479 }
480
481 func writeHSTSOutput(out *bufio.Writer, hsts preloaded) error {
482 out.WriteString(`// The following is static data describing the hosts th at are hardcoded with
483 // certificate pins or HSTS information.
484
485 // kNoRejectedPublicKeys is a placeholder for when no public keys are rejected.
486 static const char* const kNoRejectedPublicKeys[] = {
487 NULL,
488 };
489
490 `)
491
492 for _, pinset := range hsts.Pinsets {
493 name := uppercaseFirstLetter(pinset.Name)
494 acceptableListName := fmt.Sprintf("k%sAcceptableCerts", name)
495 writeListOfPins(out, acceptableListName, pinset.Include)
496
497 rejectedListName := "kNoRejectedPublicKeys"
498 if len(pinset.Exclude) > 0 {
499 rejectedListName = fmt.Sprintf("k%sRejectedCerts", name)
500 writeListOfPins(out, rejectedListName, pinset.Exclude)
501 }
502 fmt.Fprintf(out, `#define k%sPins { \
503 %s, \
504 %s, \
505 }
506
507 `, name, acceptableListName, rejectedListName)
508 }
509
510 out.WriteString(`#define kNoPins {\
511 NULL, NULL, \
512 }
513
514 static const struct HSTSPreload kPreloadedSTS[] = {
515 `)
516
517 for _, entry := range hsts.Entries {
518 if entry.SNIOnly {
519 continue
520 }
521 writeHSTSEntry(out, entry)
522 }
523
524 out.WriteString(`};
525 static const size_t kNumPreloadedSTS = ARRAYSIZE_UNSAFE(kPreloadedSTS);
526
527 static const struct HSTSPreload kPreloadedSNISTS[] = {
528 `)
529
530 for _, entry := range hsts.Entries {
531 if !entry.SNIOnly {
532 continue
533 }
534 writeHSTSEntry(out, entry)
535 }
536
537 out.WriteString(`};
538 static const size_t kNumPreloadedSNISTS = ARRAYSIZE_UNSAFE(kPreloadedSNISTS);
539
540 `)
541
542 return nil
543 }
OLDNEW
« no previous file with comments | « net/base/transport_security_state_static.json ('k') | net/net.gyp » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698