tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

pippki.sys.mjs (9360B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 *
      3 * This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /*
      8 * These are helper functions to be included
      9 * pippki UI js files.
     10 */
     11 
     12 export function setText(doc, id, value) {
     13  let element = doc.getElementById(id);
     14  if (!element) {
     15    return;
     16  }
     17  if (element.hasChildNodes()) {
     18    element.firstChild.remove();
     19  }
     20  element.appendChild(doc.createTextNode(value));
     21 }
     22 
     23 export async function getCertViewerUrl(cert) {
     24  if (!cert) {
     25    return "";
     26  }
     27  let results = await asyncDetermineUsages(cert);
     28  let chain = getBestChain(results);
     29  if (!chain) {
     30    chain = [cert];
     31  }
     32  let certs = chain.map(elem => encodeURIComponent(elem.getBase64DERString()));
     33  let certsStringURL = certs.map(elem => `cert=${elem}`);
     34  certsStringURL = certsStringURL.join("&");
     35  return `about:certificate?${certsStringURL}`;
     36 }
     37 
     38 export async function viewCertHelper(parent, cert, openingOption = "tab") {
     39  if (cert) {
     40    let win = Services.wm.getMostRecentBrowserWindow();
     41    let url = await getCertViewerUrl(cert);
     42    let opened = win.switchToTabHavingURI(url, false, {});
     43    if (!opened) {
     44      win.openTrustedLinkIn(url, openingOption);
     45    }
     46  }
     47  if (Cu.isInAutomation) {
     48    Services.obs.notifyObservers(null, "viewCertHelper-done");
     49  }
     50 }
     51 
     52 function getPKCS7Array(certArray) {
     53  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
     54    Ci.nsIX509CertDB
     55  );
     56  let pkcs7String = certdb.asPKCS7Blob(certArray);
     57  let pkcs7Array = new Uint8Array(pkcs7String.length);
     58  for (let i = 0; i < pkcs7Array.length; i++) {
     59    pkcs7Array[i] = pkcs7String.charCodeAt(i);
     60  }
     61  return pkcs7Array;
     62 }
     63 
     64 export function getPEMString(cert) {
     65  var derb64 = cert.getBase64DERString();
     66  // Wrap the Base64 string into lines of 64 characters with CRLF line breaks
     67  // (as specified in RFC 1421).
     68  var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
     69  return (
     70    "-----BEGIN CERTIFICATE-----\r\n" +
     71    wrapped +
     72    "\r\n-----END CERTIFICATE-----\r\n"
     73  );
     74 }
     75 
     76 export function alertPromptService(window, title, message) {
     77  // XXX Bug 1425832 - Using Services.prompt here causes tests to report memory
     78  // leaks.
     79  // eslint-disable-next-line mozilla/use-services
     80  var ps = Cc["@mozilla.org/prompter;1"].getService(Ci.nsIPromptService);
     81  ps.alert(window, title, message);
     82 }
     83 
     84 const DEFAULT_CERT_EXTENSION = "crt";
     85 
     86 /**
     87 * Generates a filename for a cert suitable to set as the |defaultString|
     88 * attribute on an Ci.nsIFilePicker.
     89 *
     90 * @param {nsIX509Cert} cert
     91 *        The cert to generate a filename for.
     92 * @returns {string}
     93 *          Generated filename.
     94 */
     95 function certToFilename(cert) {
     96  let filename = cert.displayName;
     97 
     98  // Remove unneeded and/or unsafe characters.
     99  filename = filename
    100    .replace(/\s/g, "")
    101    .replace(/\./g, "_")
    102    .replace(/\\/g, "")
    103    .replace(/\//g, "");
    104 
    105  // Ci.nsIFilePicker.defaultExtension is more of a suggestion to some
    106  // implementations, so we include the extension in the file name as well. This
    107  // is what the documentation for Ci.nsIFilePicker.defaultString says we should do
    108  // anyways.
    109  return `${filename}.${DEFAULT_CERT_EXTENSION}`;
    110 }
    111 
    112 export async function exportToFile(parent, document, cert) {
    113  if (!cert) {
    114    return;
    115  }
    116 
    117  let results = await asyncDetermineUsages(cert);
    118  let chain = getBestChain(results);
    119  if (!chain) {
    120    chain = [cert];
    121  }
    122 
    123  let formats = {
    124    base64: "*.crt; *.pem",
    125    "base64-chain": "*.crt; *.pem",
    126    der: "*.der",
    127    pkcs7: "*.p7c",
    128    "pkcs7-chain": "*.p7c",
    129  };
    130  let [saveCertAs, ...formatLabels] = await document.l10n.formatValues(
    131    ["save-cert-as", ...Object.keys(formats).map(f => "cert-format-" + f)].map(
    132      id => ({ id })
    133    )
    134  );
    135 
    136  var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    137  fp.init(parent.browsingContext, saveCertAs, Ci.nsIFilePicker.modeSave);
    138  fp.defaultString = certToFilename(cert);
    139  fp.defaultExtension = DEFAULT_CERT_EXTENSION;
    140  for (let format of Object.values(formats)) {
    141    fp.appendFilter(formatLabels.shift(), format);
    142  }
    143  fp.appendFilters(Ci.nsIFilePicker.filterAll);
    144  let filePickerResult = await new Promise(resolve => {
    145    fp.open(resolve);
    146  });
    147 
    148  if (
    149    filePickerResult != Ci.nsIFilePicker.returnOK &&
    150    filePickerResult != Ci.nsIFilePicker.returnReplace
    151  ) {
    152    return;
    153  }
    154 
    155  var content = "";
    156  switch (fp.filterIndex) {
    157    case 1:
    158      content = getPEMString(cert);
    159      for (let i = 1; i < chain.length; i++) {
    160        content += getPEMString(chain[i]);
    161      }
    162      break;
    163    case 2:
    164      // IOUtils.write requires a typed array.
    165      // nsIX509Cert.getRawDER() returns an array (not a typed array), so we
    166      // convert it here.
    167      content = Uint8Array.from(cert.getRawDER());
    168      break;
    169    case 3:
    170      // getPKCS7Array returns a typed array already, so no conversion is
    171      // necessary.
    172      content = getPKCS7Array([cert]);
    173      break;
    174    case 4:
    175      content = getPKCS7Array(chain);
    176      break;
    177    case 0:
    178    default:
    179      content = getPEMString(cert);
    180      break;
    181  }
    182 
    183  if (typeof content === "string") {
    184    content = new TextEncoder().encode(content);
    185  }
    186 
    187  try {
    188    await IOUtils.write(fp.file.path, content);
    189  } catch (ex) {
    190    let title = await document.l10n.formatValue("write-file-failure");
    191    alertPromptService(parent, title, ex.toString());
    192  }
    193  if (Cu.isInAutomation) {
    194    Services.obs.notifyObservers(null, "cert-export-finished");
    195  }
    196 }
    197 
    198 const PRErrorCodeSuccess = 0;
    199 
    200 // A map from the name of a certificate usage to the value of the usage.
    201 // Useful for printing debugging information and for enumerating all supported
    202 // usages.
    203 const verifyUsages = new Map([
    204  ["verifyUsageTLSClient", Ci.nsIX509CertDB.verifyUsageTLSClient],
    205  ["verifyUsageTLSServer", Ci.nsIX509CertDB.verifyUsageTLSServer],
    206  ["verifyUsageTLSServerCA", Ci.nsIX509CertDB.verifyUsageTLSServerCA],
    207  ["verifyUsageEmailSigner", Ci.nsIX509CertDB.verifyUsageEmailSigner],
    208  ["verifyUsageEmailRecipient", Ci.nsIX509CertDB.verifyUsageEmailRecipient],
    209 ]);
    210 
    211 /**
    212 * Returns a promise that will resolve with a results array consisting of what
    213 * usages the given certificate successfully verified for.
    214 *
    215 * @param {nsIX509Cert} cert
    216 *        The certificate to determine valid usages for.
    217 * @returns {Promise}
    218 *        A promise that will resolve with the results of the verifications.
    219 */
    220 export function asyncDetermineUsages(cert) {
    221  let promises = [];
    222  let now = Date.now() / 1000;
    223  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    224    Ci.nsIX509CertDB
    225  );
    226  verifyUsages.keys().forEach(usageString => {
    227    promises.push(
    228      new Promise(resolve => {
    229        let usage = verifyUsages.get(usageString);
    230        certdb.asyncVerifyCertAtTime(
    231          cert,
    232          usage,
    233          0,
    234          null,
    235          now,
    236          [],
    237          (aPRErrorCode, aVerifiedChain) => {
    238            resolve({
    239              usageString,
    240              errorCode: aPRErrorCode,
    241              chain: aVerifiedChain,
    242            });
    243          }
    244        );
    245      })
    246    );
    247  });
    248  return Promise.all(promises);
    249 }
    250 
    251 /**
    252 * Given a results array, returns the "best" verified certificate chain. Since
    253 * the primary use case is for TLS server certificates in Firefox, such a
    254 * verified chain will be returned if present. Otherwise, the priority is: TLS
    255 * client certificate, email signer, email recipient, CA. Returns null if no
    256 * usage verified successfully.
    257 *
    258 * @param {Array} results
    259 *        An array of results from `asyncDetermineUsages`. See `displayUsages`.
    260 * @returns {Array} An array of `nsIX509Cert` representing the verified
    261 *          certificate chain for the given usage, or null if there is none.
    262 */
    263 export function getBestChain(results) {
    264  let usages = [
    265    Ci.nsIX509CertDB.verifyUsageTLSServer,
    266    Ci.nsIX509CertDB.verifyUsageTLSClient,
    267    Ci.nsIX509CertDB.verifyUsageEmailSigner,
    268    Ci.nsIX509CertDB.verifyUsageEmailRecipient,
    269    Ci.nsIX509CertDB.verifyUsageTLSServerCA,
    270  ];
    271  for (let usage of usages) {
    272    let chain = getChainForUsage(results, usage);
    273    if (chain) {
    274      return chain;
    275    }
    276  }
    277  return null;
    278 }
    279 
    280 /**
    281 * Given a results array, returns the chain corresponding to the desired usage,
    282 * if verifying for that usage succeeded. Returns null otherwise.
    283 *
    284 * @param {Array} results
    285 *        An array of results from `asyncDetermineUsages`. See `displayUsages`.
    286 * @param {number} usage
    287 *        A usage, see `nsIX509CertDB::VerifyUsage`.
    288 * @returns {Array} An array of `nsIX509Cert` representing the verified
    289 *          certificate chain for the given usage, or null if there is none.
    290 */
    291 function getChainForUsage(results, usage) {
    292  for (let result of results) {
    293    if (
    294      verifyUsages.get(result.usageString) == usage &&
    295      result.errorCode == PRErrorCodeSuccess
    296    ) {
    297      return result.chain;
    298    }
    299  }
    300  return null;
    301 }
    302 
    303 // Performs an XMLHttpRequest because the script for the dialog is prevented
    304 // from doing so by CSP.
    305 export async function checkCertHelper(uri, grabber) {
    306  let req = new XMLHttpRequest();
    307  req.open("GET", uri.prePath);
    308  req.onerror = grabber.bind(null, req);
    309  req.onload = grabber.bind(null, req);
    310  req.send(null);
    311 }