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 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 } | |
OLD | NEW |