security/manager/tools/genHPKPStaticPins.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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);

mercurial