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 }