Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // How to run this file:
6 // 1. [obtain firefox source code]
7 // 2. [build/obtain firefox binaries]
8 // 3. run `[path to]/run-mozilla.sh [path to]/xpcshell \
9 // [path to]/genHPKPStaticpins.js \
10 // [absolute path to]/PreloadedHPKPins.json \
11 // [absolute path to]/default-ee.der \
12 // [absolute path to]/StaticHPKPins.h
14 if (arguments.length != 3) {
15 throw "Usage: genHPKPStaticPins.js " +
16 "<absolute path to PreloadedHPKPins.json> " +
17 "<absolute path to default-ee.der> " +
18 "<absolute path to StaticHPKPins.h>";
19 }
21 const { 'classes': Cc, 'interfaces': Ci, 'utils': Cu, 'results': Cr } = Components;
23 let { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
24 let { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
25 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
27 let gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
28 .getService(Ci.nsIX509CertDB);
30 const BUILT_IN_NICK_PREFIX = "Builtin Object Token:";
31 const SHA1_PREFIX = "sha1/";
32 const SHA256_PREFIX = "sha256/";
33 const GOOGLE_PIN_PREFIX = "GOOGLE_PIN_";
35 // Pins expire in 14 weeks (6 weeks on Beta + 8 weeks on stable)
36 const PINNING_MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 14;
38 const FILE_HEADER = "/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
39 " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
40 " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" +
41 "\n" +
42 "/*****************************************************************************/\n" +
43 "/* This is an automatically generated file. If you're not */\n" +
44 "/* PublicKeyPinningService.cpp, you shouldn't be #including it. */\n" +
45 "/*****************************************************************************/\n" +
46 "#include <stdint.h>" +
47 "\n";
49 const DOMAINHEADER = "/* Domainlist */\n" +
50 "struct TransportSecurityPreload {\n" +
51 " const char* mHost;\n" +
52 " const bool mIncludeSubdomains;\n" +
53 " const bool mTestMode;\n" +
54 " const bool mIsMoz;\n" +
55 " const int32_t mId;\n" +
56 " const StaticPinset *pinset;\n" +
57 "};\n\n";
59 const PINSETDEF = "/* Pinsets are each an ordered list by the actual value of the fingerprint */\n" +
60 "struct StaticFingerprints {\n" +
61 " const size_t size;\n" +
62 " const char* const* data;\n" +
63 "};\n\n" +
64 "struct StaticPinset {\n" +
65 " const StaticFingerprints* sha1;\n" +
66 " const StaticFingerprints* sha256;\n" +
67 "};\n\n";
69 // Command-line arguments
70 var gStaticPins = parseJson(arguments[0]);
71 var gTestCertFile = arguments[1];
73 // Open the output file.
74 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
75 file.initWithPath(arguments[2]);
76 let gFileOutputStream = FileUtils.openSafeFileOutputStream(file);
78 function writeString(string) {
79 gFileOutputStream.write(string, string.length);
80 }
82 function readFileToString(filename) {
83 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
84 file.initWithPath(filename);
85 let stream = Cc["@mozilla.org/network/file-input-stream;1"]
86 .createInstance(Ci.nsIFileInputStream);
87 stream.init(file, -1, 0, 0);
88 let buf = NetUtil.readInputStreamToString(stream, stream.available());
89 return buf;
90 }
92 function stripComments(buf) {
93 var lines = buf.split("\n");
94 let entryRegex = /^\s*\/\//;
95 let data = "";
96 for (let i = 0; i < lines.length; ++i) {
97 let match = entryRegex.exec(lines[i]);
98 if (!match) {
99 data = data + lines[i];
100 }
101 }
102 return data;
103 }
105 function isBuiltinToken(tokenName) {
106 return tokenName == "Builtin Object Token";
107 }
109 function isCertBuiltIn(cert) {
110 let tokenNames = cert.getAllTokenNames({});
111 if (!tokenNames) {
112 return false;
113 }
114 if (tokenNames.some(isBuiltinToken)) {
115 return true;
116 }
117 return false;
118 }
120 function download(filename) {
121 var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
122 .createInstance(Ci.nsIXMLHttpRequest);
123 req.open("GET", filename, false); // doing the request synchronously
124 try {
125 req.send();
126 }
127 catch (e) {
128 throw "ERROR: problem downloading '" + filename + "': " + e;
129 }
131 if (req.status != 200) {
132 throw("ERROR: problem downloading '" + filename + "': status " +
133 req.status);
134 }
135 return req.responseText;
136 }
138 function downloadAsJson(filename) {
139 // we have to filter out '//' comments
140 var result = download(filename).replace(/\/\/[^\n]*\n/g, "");
141 var data = null;
142 try {
143 data = JSON.parse(result);
144 }
145 catch (e) {
146 throw "ERROR: could not parse data from '" + filename + "': " + e;
147 }
148 return data;
149 }
151 // Returns a Subject Public Key Digest from the given pem, if it exists.
152 function getSKDFromPem(pem) {
153 let cert = gCertDB.constructX509FromBase64(pem, pem.length);
154 return cert.sha256SubjectPublicKeyInfoDigest;
155 }
157 // Downloads the static certs file and tries to map Google Chrome nicknames
158 // to Mozilla nicknames, as well as storing any hashes for pins for which we
159 // don't have root PEMs. Each entry consists of a line containing the name of
160 // the pin followed either by a hash in the format "sha1/" + base64(hash), or
161 // a PEM encoded certificate. For certificates that we have in our database,
162 // return a map of Google's nickname to ours. For ones that aren't return a
163 // map of Google's nickname to sha1 values. This code is modeled after agl's
164 // https://github.com/agl/transport-security-state-generate, which doesn't
165 // live in the Chromium repo because go is not an official language in
166 // Chromium.
167 // For all of the entries in this file:
168 // - If the entry has a hash format, find the Mozilla pin name (cert nickname)
169 // and stick the hash into certSKDToName
170 // - If the entry has a PEM format, parse the PEM, find the Mozilla pin name
171 // and stick the hash in certSKDToName
172 // We MUST be able to find a corresponding cert nickname for the Chrome names,
173 // otherwise we skip all pinsets referring to that Chrome name.
174 function downloadAndParseChromeCerts(filename, certSKDToName) {
175 // Prefixes that we care about.
176 const BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
177 const END_CERT = "-----END CERTIFICATE-----";
179 // Parsing states.
180 const PRE_NAME = 0;
181 const POST_NAME = 1;
182 const IN_CERT = 2;
183 let state = PRE_NAME;
185 let lines = download(filename).split("\n");
186 let name = "";
187 let pemCert = "";
188 let hash = "";
189 let chromeNameToHash = {};
190 let chromeNameToMozName = {}
191 for (let i = 0; i < lines.length; ++i) {
192 let line = lines[i];
193 // Skip comments and newlines.
194 if (line.length == 0 || line[0] == '#') {
195 continue;
196 }
197 switch(state) {
198 case PRE_NAME:
199 chromeName = line;
200 state = POST_NAME;
201 break;
202 case POST_NAME:
203 if (line.startsWith(SHA1_PREFIX) ||
204 line.startsWith(SHA256_PREFIX)) {
205 if (line.startsWith(SHA1_PREFIX)) {
206 hash = line.substring(SHA1_PREFIX.length);
207 } else if (line.startsWith(SHA256_PREFIX)) {
208 hash = line.substring(SHA256_PREFIX);
209 }
210 // Store the entire prefixed hash, so we can disambiguate sha1 from
211 // sha256 later.
212 chromeNameToHash[chromeName] = line;
213 certNameToSKD[chromeName] = hash;
214 certSKDToName[hash] = chromeName;
215 state = PRE_NAME;
216 } else if (line.startsWith(BEGIN_CERT)) {
217 state = IN_CERT;
218 } else {
219 throw "ERROR: couldn't parse Chrome certificate file " + line;
220 }
221 break;
222 case IN_CERT:
223 if (line.startsWith(END_CERT)) {
224 state = PRE_NAME;
225 hash = getSKDFromPem(pemCert);
226 pemCert = "";
227 if (hash in certSKDToName) {
228 mozName = certSKDToName[hash];
229 } else {
230 // Not one of our built-in certs. Prefix the name with
231 // GOOGLE_PIN_.
232 mozName = GOOGLE_PIN_PREFIX + chromeName;
233 dump("Can't find hash in builtin certs for Chrome nickname " +
234 chromeName + ", inserting " + mozName + "\n");
235 certSKDToName[hash] = mozName;
236 certNameToSKD[mozName] = hash;
237 }
238 chromeNameToMozName[chromeName] = mozName;
239 } else {
240 pemCert += line;
241 }
242 break;
243 default:
244 throw "ERROR: couldn't parse Chrome certificate file " + line;
245 }
246 }
247 return [ chromeNameToHash, chromeNameToMozName ];
248 }
250 // We can only import pinsets from chrome if for every name in the pinset:
251 // - We have a hash from Chrome's static certificate file
252 // - We have a builtin cert
253 // If the pinset meets these requirements, we store a map array of pinset
254 // objects:
255 // {
256 // pinset_name : {
257 // // Array of names with entries in certNameToSKD
258 // sha1_hashes: [],
259 // sha256_hashes: []
260 // }
261 // }
262 // and an array of imported pinset entries:
263 // { name: string, include_subdomains: boolean, test_mode: boolean,
264 // pins: pinset_name }
265 function downloadAndParseChromePins(filename,
266 chromeNameToHash,
267 chromeNameToMozName,
268 certNameToSKD,
269 certSKDToName) {
270 let chromePreloads = downloadAsJson(filename);
271 let chromePins = chromePreloads.pinsets;
272 let chromeImportedPinsets = {};
273 let chromeImportedEntries = [];
275 chromePins.forEach(function(pin) {
276 let valid = true;
277 let pinset = { name: pin.name, sha1_hashes: [], sha256_hashes: [] };
278 // Translate the Chrome pinset format to ours
279 pin.static_spki_hashes.forEach(function(name) {
280 if (name in chromeNameToHash) {
281 let hash = chromeNameToHash[name];
282 if (hash.startsWith(SHA1_PREFIX)) {
283 hash = hash.substring(SHA1_PREFIX.length);
284 pinset.sha1_hashes.push(certSKDToName[hash]);
285 } else if (hash.startsWith(SHA256_PREFIX)) {
286 hash = hash.substring(SHA256_PREFIX.length);
287 pinset.sha256_hashes.push(certSKDToName[hash]);
288 } else {
289 throw("Unsupported hash type: " + chromeNameToHash[name]);
290 }
291 // We should have already added hashes for all of these when we
292 // imported the certificate file.
293 if (!certNameToSKD[name]) {
294 throw("No hash for name: " + name);
295 }
296 } else if (name in chromeNameToMozName) {
297 pinset.sha256_hashes.push(chromeNameToMozName[name]);
298 } else {
299 dump("Skipping Chrome pinset " + pinset.name + ", couldn't find " +
300 "builtin " + name + " from cert file\n");
301 valid = false;
302 }
303 });
304 if (valid) {
305 chromeImportedPinsets[pinset.name] = pinset;
306 }
307 });
309 // Grab the domain entry lists. Chrome's entry format is similar to
310 // ours, except theirs includes a HSTS mode.
311 const cData = gStaticPins.chromium_data;
312 let entries = chromePreloads.entries;
313 entries.forEach(function(entry) {
314 let pinsetName = cData.substitute_pinsets[entry.pins];
315 if (!pinsetName) {
316 pinsetName = entry.pins;
317 }
318 let isProductionDomain =
319 (cData.production_domains.indexOf(entry.name) != -1);
320 let isProductionPinset =
321 (cData.production_pinsets.indexOf(pinsetName) != -1);
322 let excludeDomain =
323 (cData.exclude_domains.indexOf(entry.name) != -1);
324 let isTestMode = !isProductionPinset && !isProductionDomain;
325 if (entry.pins && !excludeDomain && chromeImportedPinsets[entry.pins]) {
326 chromeImportedEntries.push({
327 name: entry.name,
328 include_subdomains: entry.include_subdomains,
329 test_mode: isTestMode,
330 is_moz: false,
331 pins: pinsetName });
332 }
333 });
334 return [ chromeImportedPinsets, chromeImportedEntries ];
335 }
337 // Returns a pair of maps [certNameToSKD, certSKDToName] between cert
338 // nicknames and digests of the SPKInfo for the mozilla trust store
339 function loadNSSCertinfo(derTestFile, extraCertificates) {
340 let allCerts = gCertDB.getCerts();
341 let enumerator = allCerts.getEnumerator();
342 let certNameToSKD = {};
343 let certSKDToName = {};
344 while (enumerator.hasMoreElements()) {
345 let cert = enumerator.getNext().QueryInterface(Ci.nsIX509Cert);
346 if (!isCertBuiltIn(cert)) {
347 continue;
348 }
349 let name = cert.nickname.substr(BUILT_IN_NICK_PREFIX.length);
350 let SKD = cert.sha256SubjectPublicKeyInfoDigest;
351 certNameToSKD[name] = SKD;
352 certSKDToName[SKD] = name;
353 }
355 for (let cert of extraCertificates) {
356 let name = cert.commonName;
357 let SKD = cert.sha256SubjectPublicKeyInfoDigest;
358 certNameToSKD[name] = SKD;
359 certSKDToName[SKD] = name;
360 }
362 {
363 // A certificate for *.example.com.
364 let der = readFileToString(derTestFile);
365 let testCert = gCertDB.constructX509(der, der.length);
366 // We can't include this cert in the previous loop, because it skips
367 // non-builtin certs and the nickname is not built-in to the cert.
368 let name = "End Entity Test Cert";
369 let SKD = testCert.sha256SubjectPublicKeyInfoDigest;
370 certNameToSKD[name] = SKD;
371 certSKDToName[SKD] = name;
372 }
373 return [certNameToSKD, certSKDToName];
374 }
376 function parseJson(filename) {
377 let json = stripComments(readFileToString(filename));
378 return JSON.parse(json);
379 }
381 function nameToAlias(certName) {
382 // change the name to a string valid as a c identifier
383 // remove non-ascii characters
384 certName = certName.replace( /[^[:ascii:]]/g, "_");
385 // replace non word characters
386 certName = certName.replace(/[^A-Za-z0-9]/g ,"_");
388 return "k" + certName + "Fingerprint";
389 }
391 function compareByName (a, b) {
392 return a.name.localeCompare(b.name);
393 }
395 function genExpirationTime() {
396 let now = new Date();
397 let nowMillis = now.getTime();
398 let expirationMillis = nowMillis + (PINNING_MINIMUM_REQUIRED_MAX_AGE * 1000);
399 let expirationMicros = expirationMillis * 1000;
400 return "static const PRTime kPreloadPKPinsExpirationTime = INT64_C(" +
401 expirationMicros +");\n";
402 }
404 function writeFullPinset(certNameToSKD, certSKDToName, pinset) {
405 // We aren't guaranteed to have sha1 hashes in our own imported pins.
406 let prefix = "kPinset_" + pinset.name;
407 let sha1Name = "nullptr";
408 let sha256Name = "nullptr";
409 if (pinset.sha1_hashes && pinset.sha1_hashes.length > 0) {
410 writeFingerprints(certNameToSKD, certSKDToName, pinset.name,
411 pinset.sha1_hashes, "sha1");
412 sha1Name = "&" + prefix + "_sha1";
413 }
414 if (pinset.sha256_hashes && pinset.sha256_hashes.length > 0) {
415 writeFingerprints(certNameToSKD, certSKDToName, pinset.name,
416 pinset.sha256_hashes, "sha256");
417 sha256Name = "&" + prefix + "_sha256";
418 }
419 writeString("static const StaticPinset " + prefix + " = {\n" +
420 " " + sha1Name + ",\n " + sha256Name + "\n};\n\n");
421 }
423 function writeFingerprints(certNameToSKD, certSKDToName, name, hashes, type) {
424 let varPrefix = "kPinset_" + name + "_" + type;
425 writeString("static const char* " + varPrefix + "_Data[] = {\n");
426 let SKDList = [];
427 for (let certName of hashes) {
428 if (!(certName in certNameToSKD)) {
429 throw "Can't find " + certName + " in certNameToSKD";
430 }
431 SKDList.push(certNameToSKD[certName]);
432 }
433 for (let skd of SKDList.sort()) {
434 writeString(" " + nameToAlias(certSKDToName[skd]) + ",\n");
435 }
436 if (hashes.length == 0) {
437 // ANSI C requires that an initialiser list be non-empty.
438 writeString(" 0\n");
439 }
440 writeString("};\n");
441 writeString("static const StaticFingerprints " + varPrefix + " = {\n " +
442 "sizeof(" + varPrefix + "_Data) / sizeof(const char*),\n " + varPrefix +
443 "_Data\n};\n\n");
444 }
446 function writeEntry(entry) {
447 let printVal = " { \"" + entry.name + "\",\ ";
448 if (entry.include_subdomains) {
449 printVal += "true, ";
450 } else {
451 printVal += "false, ";
452 }
453 // Default to test mode if not specified.
454 let testMode = true;
455 if (entry.hasOwnProperty("test_mode")) {
456 testMode = entry.test_mode;
457 }
458 if (testMode) {
459 printVal += "true, ";
460 } else {
461 printVal += "false, ";
462 }
463 if (entry.is_moz || (entry.pins == "mozilla")) {
464 printVal += "true, ";
465 } else {
466 printVal += "false, ";
467 }
468 if (entry.id >= 256) {
469 throw("Not enough buckets in histogram");
470 }
471 if (entry.id >= 0) {
472 printVal += entry.id + ", ";
473 } else {
474 printVal += "-1, ";
475 }
476 printVal += "&kPinset_" + entry.pins;
477 printVal += " },\n";
478 writeString(printVal);
479 }
481 function writeDomainList(chromeImportedEntries) {
482 writeString("/* Sort hostnames for binary search. */\n");
483 writeString("static const TransportSecurityPreload " +
484 "kPublicKeyPinningPreloadList[] = {\n");
485 let count = 0;
486 let sortedEntries = gStaticPins.entries;
487 sortedEntries.push.apply(sortedEntries, chromeImportedEntries);
488 for (let entry of sortedEntries.sort(compareByName)) {
489 count++;
490 writeEntry(entry);
491 }
492 writeString("};\n");
494 writeString("\n// Pinning Preload List Length = " + count + ";\n");
495 writeString("\nstatic const int32_t kUnknownId = -1;\n");
496 }
498 function writeFile(certNameToSKD, certSKDToName,
499 chromeImportedPinsets, chromeImportedEntries) {
500 // Compute used pins from both Chrome's and our pinsets, so we can output
501 // them later.
502 usedFingerprints = {};
503 gStaticPins.pinsets.forEach(function(pinset) {
504 // We aren't guaranteed to have sha1_hashes in our own JSON.
505 if (pinset.sha1_hashes) {
506 pinset.sha1_hashes.forEach(function(name) {
507 usedFingerprints[name] = true;
508 });
509 }
510 if (pinset.sha256_hashes) {
511 pinset.sha256_hashes.forEach(function(name) {
512 usedFingerprints[name] = true;
513 });
514 }
515 });
516 for (let key in chromeImportedPinsets) {
517 let pinset = chromeImportedPinsets[key];
518 pinset.sha1_hashes.forEach(function(name) {
519 usedFingerprints[name] = true;
520 });
521 pinset.sha256_hashes.forEach(function(name) {
522 usedFingerprints[name] = true;
523 });
524 }
526 writeString(FILE_HEADER);
528 // Write actual fingerprints.
529 Object.keys(usedFingerprints).sort().forEach(function(certName) {
530 if (certName) {
531 writeString("/* " + certName + " */\n");
532 writeString("static const char " + nameToAlias(certName) + "[] =\n");
533 writeString(" \"" + certNameToSKD[certName] + "\";\n");
534 writeString("\n");
535 }
536 });
538 // Write the pinsets
539 writeString(PINSETDEF);
540 writeString("/* PreloadedHPKPins.json pinsets */\n");
541 gStaticPins.pinsets.sort(compareByName).forEach(function(pinset) {
542 writeFullPinset(certNameToSKD, certSKDToName, pinset);
543 });
544 writeString("/* Chrome static pinsets */\n");
545 for (let key in chromeImportedPinsets) {
546 writeFullPinset(certNameToSKD, certSKDToName, chromeImportedPinsets[key]);
547 }
549 // Write the domainlist entries.
550 writeString(DOMAINHEADER);
551 writeDomainList(chromeImportedEntries);
552 writeString("\n");
553 writeString(genExpirationTime());
554 }
556 function loadExtraCertificates(certStringList) {
557 let constructedCerts = [];
558 for (let certString of certStringList) {
559 constructedCerts.push(gCertDB.constructX509FromBase64(certString));
560 }
561 return constructedCerts;
562 }
564 let extraCertificates = loadExtraCertificates(gStaticPins.extra_certificates);
565 let [ certNameToSKD, certSKDToName ] = loadNSSCertinfo(gTestCertFile,
566 extraCertificates);
567 let [ chromeNameToHash, chromeNameToMozName ] = downloadAndParseChromeCerts(
568 gStaticPins.chromium_data.cert_file_url, certSKDToName);
569 let [ chromeImportedPinsets, chromeImportedEntries ] =
570 downloadAndParseChromePins(gStaticPins.chromium_data.json_file_url,
571 chromeNameToHash, chromeNameToMozName, certNameToSKD, certSKDToName);
573 writeFile(certNameToSKD, certSKDToName, chromeImportedPinsets,
574 chromeImportedEntries);
576 FileUtils.closeSafeFileOutputStream(gFileOutputStream);