summaryrefslogtreecommitdiffstats
path: root/security/manager/tools/genRootCAHashes.js
blob: ea5794a2aadf500f13e77b7ebceeba1201c4e1d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// How to run this file:
// 1. [obtain firefox source code]
// 2. [build/obtain firefox binaries]
// 3. run `[path to]/run-mozilla.sh [path to]/xpcshell genRootCAHashes.js \
//                                  [absolute path to]/RootHashes.inc'

const nsX509CertDB = "@mozilla.org/security/x509certdb;1";
const CertDb = Cc[nsX509CertDB].getService(Ci.nsIX509CertDB);

const { FileUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/FileUtils.sys.mjs"
);
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const { CommonUtils } = ChromeUtils.import(
  "resource://services-common/utils.js"
);

const FILENAME_OUTPUT = "RootHashes.inc";
const FILENAME_TRUST_ANCHORS = "KnownRootHashes.json";
const ROOT_NOT_ASSIGNED = -1;

const JSON_HEADER = `// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//
//***************************************************************************
// This is an automatically generated file. It's used to maintain state for
// runs of genRootCAHashes.js; you should never need to manually edit it
//***************************************************************************

// Notes:
// binNumber 1 used to be for "GTE_CyberTrust_Global_Root", but that root was
// removed from the built-in roots module, so now it is used to indicate that
// the certificate is not a built-in and was found in the softoken (cert9.db).

// binNumber 2 used to be for "Thawte_Server_CA", but that root was removed from
// the built-in roots module, so now it is used to indicate that the certificate
// is not a built-in and was found on an external PKCS#11 token.

// binNumber 3 used to be for "Thawte_Premium_Server_CA", but that root was
// removed from the built-in roots module, so now it is used to indicate that
// the certificate is not a built-in and was temporarily imported from the OS as
// part of the "Enterprise Roots" feature.

`;

const FILE_HEADER =
  "/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
  " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
  " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" +
  "\n" +
  "/*****************************************************************************/\n" +
  "/* This is an automatically generated file. If you're not                    */\n" +
  "/* RootCertificateTelemetryUtils.cpp, you shouldn't be #including it.        */\n" +
  "/*****************************************************************************/\n" +
  "\n" +
  "#define HASH_LEN 32\n";

const FP_PREAMBLE =
  "struct CertAuthorityHash {\n" +
  "  // See bug 1338873 about making these fields const.\n" +
  "  uint8_t hash[HASH_LEN];\n" +
  "  int32_t binNumber;\n" +
  "};\n\n" +
  "static const struct CertAuthorityHash ROOT_TABLE[] = {\n";

const FP_POSTAMBLE = "};\n";

// Helper
function writeString(fos, string) {
  fos.write(string, string.length);
}

// Remove all colons from a string
function stripColons(hexString) {
  return hexString.replace(/:/g, "");
}

// Expect an array of bytes and make it C-formatted
function hexSlice(bytes, start, end) {
  let ret = "";
  for (let i = start; i < end; i++) {
    let hex = (0 + bytes.charCodeAt(i).toString(16)).slice(-2).toUpperCase();
    ret += "0x" + hex;
    if (i < end - 1) {
      ret += ", ";
    }
  }
  return ret;
}

function stripComments(buf) {
  let lines = buf.split("\n");
  let entryRegex = /^\s*\/\//;
  let data = "";
  for (let i = 0; i < lines.length; i++) {
    let match = entryRegex.exec(lines[i]);
    if (!match) {
      data = data + lines[i];
    }
  }
  return data;
}

// Load the trust anchors JSON object from disk
function loadTrustAnchors(file) {
  if (file.exists()) {
    let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
      Ci.nsIFileInputStream
    );
    stream.init(file, -1, 0, 0);
    let buf = NetUtil.readInputStreamToString(stream, stream.available());
    return JSON.parse(stripComments(buf));
  }
  // If there's no input file, bootstrap.
  return { roots: [], maxBin: 0 };
}

// Saves our persistence file so that we don't lose track of the mapping
// between bin numbers and the CA-hashes, even as CAs come and go.
function writeTrustAnchors(file) {
  let fos = FileUtils.openSafeFileOutputStream(file);

  let serializedData = JSON.stringify(gTrustAnchors, null, "  ");
  fos.write(JSON_HEADER, JSON_HEADER.length);
  fos.write(serializedData, serializedData.length);

  FileUtils.closeSafeFileOutputStream(fos);
}

// Write the C++ header file
function writeRootHashes(fos) {
  try {
    writeString(fos, FILE_HEADER);

    // Output the sorted gTrustAnchors
    writeString(fos, FP_PREAMBLE);
    gTrustAnchors.roots.forEach(function(fp) {
      let fpBytes = atob(fp.sha256Fingerprint);

      writeString(fos, "  {\n");
      writeString(fos, "    /* " + fp.label + " */\n");
      writeString(fos, "    { " + hexSlice(fpBytes, 0, 16) + ",\n");
      writeString(fos, "      " + hexSlice(fpBytes, 16, 32) + " },\n");
      writeString(fos, "      " + fp.binNumber + " /* Bin Number */\n");

      writeString(fos, "  },\n");
    });
    writeString(fos, FP_POSTAMBLE);

    writeString(fos, "\n");
  } catch (e) {
    dump("ERROR: problem writing output: " + e + "\n");
  }
}

// Scan our list (linearly) for the given fingerprint string
function findTrustAnchorByFingerprint(sha256Fingerprint) {
  for (let i = 0; i < gTrustAnchors.roots.length; i++) {
    if (sha256Fingerprint == gTrustAnchors.roots[i].sha256Fingerprint) {
      return i;
    }
  }
  return ROOT_NOT_ASSIGNED;
}

// Get a clean label for a given certificate; usually the common name.
function getLabelForCert(cert) {
  let label = cert.commonName;

  if (label.length < 5) {
    label = cert.subjectName;
  }

  // replace non-ascii characters
  label = label.replace(/[^[:ascii:]]/g, "_");
  // replace non-word characters
  label = label.replace(/[^A-Za-z0-9]/g, "_");
  return label;
}

// Fill in the gTrustAnchors list with trust anchors from the database.
function insertTrustAnchorsFromDatabase() {
  // We only want CA certs for SSL
  const CERT_TYPE = Ci.nsIX509Cert.CA_CERT;
  const TRUST_TYPE = Ci.nsIX509CertDB.TRUSTED_SSL;

  // Iterate through the whole Cert DB
  for (let cert of CertDb.getCerts()) {
    // Find the certificate in our existing list. Do it here because we need to check if
    // it's untrusted too.

    // If this is a trusted cert
    if (CertDb.isCertTrusted(cert, CERT_TYPE, TRUST_TYPE)) {
      // Base64 encode the hex string
      let binaryFingerprint = CommonUtils.hexToBytes(
        stripColons(cert.sha256Fingerprint)
      );
      let encodedFingerprint = btoa(binaryFingerprint);

      // Scan to see if this is already in the database.
      if (
        findTrustAnchorByFingerprint(encodedFingerprint) == ROOT_NOT_ASSIGNED
      ) {
        // Let's get a usable name; some old certs do not have CN= filled out
        let label = getLabelForCert(cert);

        // Add to list
        gTrustAnchors.maxBin += 1;
        gTrustAnchors.roots.push({
          label,
          binNumber: gTrustAnchors.maxBin,
          sha256Fingerprint: encodedFingerprint,
        });
      }
    }
  }
}

//
//  PRIMARY LOGIC
//

if (arguments.length != 1) {
  throw new Error(
    "Usage: genRootCAHashes.js <absolute path to current RootHashes.inc>"
  );
}

var trustAnchorsFile = new FileUtils.File(
  PathUtils.join(
    Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
    FILENAME_TRUST_ANCHORS
  )
);
var rootHashesFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
rootHashesFile.initWithPath(arguments[0]);

// Open the known hashes file; this is to ensure stable bin numbers.
var gTrustAnchors = loadTrustAnchors(trustAnchorsFile);

// Collect all certificate entries
insertTrustAnchorsFromDatabase();

// Update known hashes before we sort
writeTrustAnchors(trustAnchorsFile);

// Sort all trust anchors before writing, as AccumulateRootCA.cpp
// will perform binary searches
gTrustAnchors.roots.sort(function(a, b) {
  // We need to work from the binary values, not the base64 values.
  let aBin = atob(a.sha256Fingerprint);
  let bBin = atob(b.sha256Fingerprint);

  if (aBin < bBin) {
    return -1;
  }
  if (aBin > bBin) {
    return 1;
  }
  return 0;
});

// Write the output file.
var rootHashesFileOutputStream = FileUtils.openSafeFileOutputStream(
  rootHashesFile
);
writeRootHashes(rootHashesFileOutputStream);
FileUtils.closeSafeFileOutputStream(rootHashesFileOutputStream);