/* 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"; /** * @file Implements functionality for certViewer.xul and its tabs certDump.xul * and viewCertDetails.xul: a dialog that allows various attributes of a * certificate to be viewed. * @argument {nsISupports} window.arguments[0] * The cert to view, queryable to nsIX509Cert. */ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); const nsIX509Cert = Ci.nsIX509Cert; const nsX509CertDB = "@mozilla.org/security/x509certdb;1"; const nsIX509CertDB = Ci.nsIX509CertDB; const nsPK11TokenDB = "@mozilla.org/security/pk11tokendb;1"; const nsIPK11TokenDB = Ci.nsIPK11TokenDB; const nsIASN1Object = Ci.nsIASN1Object; const nsIASN1Sequence = Ci.nsIASN1Sequence; const nsIASN1PrintableItem = Ci.nsIASN1PrintableItem; const nsIASN1Tree = Ci.nsIASN1Tree; const nsASN1Tree = "@mozilla.org/security/nsASN1Tree;1"; var bundle; function doPrompt(msg) { let prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. getService(Components.interfaces.nsIPromptService); prompts.alert(window, null, msg); } /** * Fills out the "Certificate Hierarchy" tree of the cert viewer "Details" tab. * * @param {tree} node * Parent tree node to append to. * @param {nsIArray} chain * Chain where cert element n is issued by cert element n + 1. */ function AddCertChain(node, chain) { var child = document.getElementById(node); var currCert; var displayVal; for (let i = chain.length - 1; i >= 0; i--) { currCert = chain.queryElementAt(i, nsIX509Cert); if (currCert.commonName) { displayVal = currCert.commonName; } else { displayVal = currCert.windowTitle; } let addTwistie = i != 0; child = addChildrenToTree(child, displayVal, currCert.dbKey, addTwistie); } } /** * Adds a "verified usage" of a cert to the "General" tab of the cert viewer. * * @param {String} usage * Verified usage to add. */ function AddUsage(usage) { let verifyInfoBox = document.getElementById("verify_info_box"); let text = document.createElement("textbox"); text.setAttribute("value", usage); text.setAttribute("style", "margin: 2px 5px"); text.setAttribute("readonly", "true"); text.setAttribute("class", "scrollfield"); verifyInfoBox.appendChild(text); } function setWindowName() { bundle = document.getElementById("pippki_bundle"); let cert = window.arguments[0].QueryInterface(Ci.nsIX509Cert); document.title = bundle.getFormattedString("certViewerTitle", [cert.windowTitle]); // // Set the cert attributes for viewing // // The chain of trust AddCertChain("treesetDump", cert.getChain()); DisplayGeneralDataFromCert(cert); BuildPrettyPrint(cert); asyncDetermineUsages(cert); } // Certificate usages we care about in the certificate viewer. const certificateUsageSSLClient = 0x0001; const certificateUsageSSLServer = 0x0002; const certificateUsageSSLCA = 0x0008; const certificateUsageEmailSigner = 0x0010; const certificateUsageEmailRecipient = 0x0020; const certificateUsageObjectSigner = 0x0040; // A map from the name of a certificate usage to the value of the usage. // Useful for printing debugging information and for enumerating all supported // usages. const certificateUsages = { certificateUsageSSLClient, certificateUsageSSLServer, certificateUsageSSLCA, certificateUsageEmailSigner, certificateUsageEmailRecipient, certificateUsageObjectSigner, }; // Map of certificate usage name to localization identifier. const certificateUsageToStringBundleName = { certificateUsageSSLClient: "VerifySSLClient", certificateUsageSSLServer: "VerifySSLServer", certificateUsageSSLCA: "VerifySSLCA", certificateUsageEmailSigner: "VerifyEmailSigner", certificateUsageEmailRecipient: "VerifyEmailRecip", certificateUsageObjectSigner: "VerifyObjSign", }; const PRErrorCodeSuccess = 0; const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11; const SEC_ERROR_REVOKED_CERTIFICATE = SEC_ERROR_BASE + 12; const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20; const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21; const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30; const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = SEC_ERROR_BASE + 176; /** * Kicks off asynchronous verifications of the given certificate to determine * what usages it is currently valid for. Updates the usage display area when * complete. * * @param {nsIX509Cert} cert * The certificate to determine valid usages for. */ function asyncDetermineUsages(cert) { let promises = []; let now = Date.now() / 1000; let certdb = Cc["@mozilla.org/security/x509certdb;1"] .getService(Ci.nsIX509CertDB); Object.keys(certificateUsages).forEach(usageString => { promises.push(new Promise((resolve, reject) => { let usage = certificateUsages[usageString]; certdb.asyncVerifyCertAtTime(cert, usage, 0, null, now, (aPRErrorCode, aVerifiedChain, aHasEVPolicy) => { resolve({ usageString: usageString, errorCode: aPRErrorCode }); }); })); }); Promise.all(promises).then(displayUsages); } /** * Updates the usage display area given the results from asyncDetermineUsages. * * @param {Array} results * An array of objects with the properties "usageString" and "errorCode". * usageString is a string that is a key in the certificateUsages map. * errorCode is either an NSPR error code or PRErrorCodeSuccess (which is * a pseudo-NSPR error code with the value 0 that indicates success). */ function displayUsages(results) { document.getElementById("verify_pending").setAttribute("hidden", "true"); let verified = document.getElementById("verified"); let someSuccess = results.some(result => result.errorCode == PRErrorCodeSuccess ); if (someSuccess) { let verifystr = bundle.getString("certVerified"); verified.textContent = verifystr; let pipnssBundle = Services.strings.createBundle( "chrome://pipnss/locale/pipnss.properties"); results.forEach(result => { if (result.errorCode != PRErrorCodeSuccess) { return; } let bundleName = certificateUsageToStringBundleName[result.usageString]; let usage = pipnssBundle.GetStringFromName(bundleName); AddUsage(usage); }); } else { const errorRankings = [ { error: SEC_ERROR_REVOKED_CERTIFICATE, bundleString: "certNotVerified_CertRevoked" }, { error: SEC_ERROR_UNTRUSTED_CERT, bundleString: "certNotVerified_CertNotTrusted" }, { error: SEC_ERROR_UNTRUSTED_ISSUER, bundleString: "certNotVerified_IssuerNotTrusted" }, { error: SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, bundleString: "certNotVerified_AlgorithmDisabled" }, { error: SEC_ERROR_EXPIRED_CERTIFICATE, bundleString: "certNotVerified_CertExpired" }, { error: SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE, bundleString: "certNotVerified_CAInvalid" }, { error: SEC_ERROR_UNKNOWN_ISSUER, bundleString: "certNotVerified_IssuerUnknown" }, ]; let verifystr; for (let errorRanking of errorRankings) { let errorPresent = results.some(result => result.errorCode == errorRanking.error ); if (errorPresent) { verifystr = bundle.getString(errorRanking.bundleString); break; } } if (!verifystr) { verifystr = bundle.getString("certNotVerified_Unknown"); } verified.textContent = verifystr; } // Notify that we are done determining the certificate's valid usages (this // should be treated as an implementation detail that enables tests to run // efficiently - other code in the browser probably shouldn't rely on this). Services.obs.notifyObservers(window, "ViewCertDetails:CertUsagesDone", null); } function addChildrenToTree(parentTree, label, value, addTwistie) { let treeChild1 = document.createElement("treechildren"); let treeElement = addTreeItemToTreeChild(treeChild1, label, value, addTwistie); parentTree.appendChild(treeChild1); return treeElement; } function addTreeItemToTreeChild(treeChild, label, value, addTwistie) { let treeElem1 = document.createElement("treeitem"); if (addTwistie) { treeElem1.setAttribute("container", "true"); treeElem1.setAttribute("open", "true"); } let treeRow = document.createElement("treerow"); let treeCell = document.createElement("treecell"); treeCell.setAttribute("label", label); if (value) { treeCell.setAttribute("display", value); } treeRow.appendChild(treeCell); treeElem1.appendChild(treeRow); treeChild.appendChild(treeElem1); return treeElem1; } function displaySelected() { var asn1Tree = document.getElementById('prettyDumpTree') .view.QueryInterface(nsIASN1Tree); var items = asn1Tree.selection; var certDumpVal = document.getElementById('certDumpVal'); if (items.currentIndex != -1) { var value = asn1Tree.getDisplayData(items.currentIndex); certDumpVal.value = value; } else { certDumpVal.value = ""; } } function BuildPrettyPrint(cert) { var certDumpTree = Components.classes[nsASN1Tree]. createInstance(nsIASN1Tree); certDumpTree.loadASN1Structure(cert.ASN1Structure); document.getElementById('prettyDumpTree').view = certDumpTree; } function addAttributeFromCert(nodeName, value) { var node = document.getElementById(nodeName); if (!value) { value = bundle.getString('notPresent'); } node.setAttribute('value', value); } /** * Displays information about a cert in the "General" tab of the cert viewer. * * @param {nsIX509Cert} cert * Cert to display information about. */ function DisplayGeneralDataFromCert(cert) { addAttributeFromCert("commonname", cert.commonName); addAttributeFromCert("organization", cert.organization); addAttributeFromCert("orgunit", cert.organizationalUnit); addAttributeFromCert("serialnumber", cert.serialNumber); addAttributeFromCert("sha256fingerprint", cert.sha256Fingerprint); addAttributeFromCert("sha1fingerprint", cert.sha1Fingerprint); addAttributeFromCert("validitystart", cert.validity.notBeforeLocalDay); addAttributeFromCert("validityend", cert.validity.notAfterLocalDay); addAttributeFromCert("issuercommonname", cert.issuerCommonName); addAttributeFromCert("issuerorganization", cert.issuerOrganization); addAttributeFromCert("issuerorgunit", cert.issuerOrganizationUnit); } function updateCertDump() { var asn1Tree = document.getElementById('prettyDumpTree') .view.QueryInterface(nsIASN1Tree); var tree = document.getElementById('treesetDump'); if (tree.currentIndex < 0) { doPrompt("No items are selected."); //This should never happen. } else { var item = tree.contentView.getItemAtIndex(tree.currentIndex); var dbKey = item.firstChild.firstChild.getAttribute('display'); // Get the cert from the cert database var certdb = Components.classes[nsX509CertDB].getService(nsIX509CertDB); var cert = certdb.findCertByDBKey(dbKey); asn1Tree.loadASN1Structure(cert.ASN1Structure); } displaySelected(); } function getCurrentCert() { var realIndex; var tree = document.getElementById('treesetDump'); if (tree.view.selection.isSelected(tree.currentIndex) && document.getElementById('prettyprint_tab').selected) { /* if the user manually selected a cert on the Details tab, then take that one */ realIndex = tree.currentIndex; } else { /* otherwise, take the one at the bottom of the chain (i.e. the one of the end-entity, unless we're displaying CA certs) */ realIndex = tree.view.rowCount - 1; } if (realIndex >= 0) { var item = tree.contentView.getItemAtIndex(realIndex); var dbKey = item.firstChild.firstChild.getAttribute('display'); var certdb = Components.classes[nsX509CertDB].getService(nsIX509CertDB); var cert = certdb.findCertByDBKey(dbKey); return cert; } /* shouldn't really happen */ return null; }