/* 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/. */ /* Copyright © 2015, Deutsche Telekom, Inc. */ "use strict"; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/systemlibs.js"); XPCOMUtils.defineLazyServiceGetter(this, "UiccConnector", "@mozilla.org/secureelement/connector/uicc;1", "nsISecureElementConnector"); XPCOMUtils.defineLazyModuleGetter(this, "SEUtils", "resource://gre/modules/SEUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "SE", function() { let obj = {}; Cu.import("resource://gre/modules/se_consts.js", obj); return obj; }); XPCOMUtils.defineLazyGetter(this, "GP", function() { let obj = {}; Cu.import("resource://gre/modules/gp_consts.js", obj); return obj; }); var DEBUG = SE.DEBUG_ACE; function debug(msg) { if (DEBUG) { dump("-*- GPAccessRulesManager " + msg); } } /** * Based on [1] - "GlobalPlatform Device Technology * Secure Element Access Control Version 1.0". * GPAccessRulesManager reads and parses access rules from SE file system * as defined in section #7 of [1]: "Structure of Access Rule Files (ARF)". * Rules retrieval from ARA-M applet is not implmented due to lack of * commercial implemenations of ARA-M. * @todo Bug 1137537: Implement ARA-M support according to section #4 of [1] */ function GPAccessRulesManager() {} GPAccessRulesManager.prototype = { // source [1] section 7.1.3 PKCS#15 Selection PKCS_AID: "a000000063504b43532d3135", // APDUs (ISO 7816-4) for accessing rules on SE file system // see for more details: http://www.cardwerk.com/smartcards/ // smartcard_standard_ISO7816-4_6_basic_interindustry_commands.aspx READ_BINARY: [GP.CLA_SM, GP.INS_RB, GP.P1_RB, GP.P2_RB], GET_RESPONSE: [GP.CLA_SM, GP.INS_GR, GP.P1_GR, GP.P2_GR], SELECT_BY_DF: [GP.CLA_SM, GP.INS_SF, GP.P1_SF_DF, GP.P2_SF_FCP], // Non-null if there is a channel open channel: null, // Refresh tag path in the acMain file as described in GPD spec, // sections 7.1.5 and C.1. REFRESH_TAG_PATH: [GP.TAG_SEQUENCE, GP.TAG_OCTETSTRING], refreshTag: null, // Contains rules as read from the SE rules: [], // Returns the latest rules. Results are cached. getAccessRules: function getAccessRules() { debug("getAccessRules"); return new Promise((resolve, reject) => { this._readAccessRules(() => resolve(this.rules)); }); }, _readAccessRules: Task.async(function*(done) { try { yield this._openChannel(this.PKCS_AID); let odf = yield this._readODF(); let dodf = yield this._readDODF(odf); let acmf = yield this._readACMF(dodf); let refreshTag = acmf[this.REFRESH_TAG_PATH[0]] [this.REFRESH_TAG_PATH[1]]; // Update cached rules based on refreshTag. if (SEUtils.arraysEqual(this.refreshTag, refreshTag)) { debug("_readAccessRules: refresh tag equals to the one saved."); yield this._closeChannel(); return done(); } this.refreshTag = refreshTag; debug("_readAccessRules: refresh tag saved: " + this.refreshTag); let acrf = yield this._readACRules(acmf); let accf = yield this._readACConditions(acrf); this.rules = yield this._parseRules(acrf, accf); DEBUG && debug("_readAccessRules: " + JSON.stringify(this.rules, 0, 2)); yield this._closeChannel(); done(); } catch (error) { debug("_readAccessRules: " + error); this.rules = []; yield this._closeChannel(); done(); } }), _openChannel: function _openChannel(aid) { if (this.channel !== null) { debug("_openChannel: Channel already opened, rejecting."); return Promise.reject(); } return new Promise((resolve, reject) => { UiccConnector.openChannel(aid, { notifyOpenChannelSuccess: (channel, openResponse) => { debug("_openChannel/notifyOpenChannelSuccess: Channel " + channel + " opened, open response: " + openResponse); this.channel = channel; resolve(); }, notifyError: (error) => { debug("_openChannel/notifyError: failed to open channel, error: " + error); reject(error); } }); }); }, _closeChannel: function _closeChannel() { if (this.channel === null) { debug("_closeChannel: Channel not opened, rejecting."); return Promise.reject(); } return new Promise((resolve, reject) => { UiccConnector.closeChannel(this.channel, { notifyCloseChannelSuccess: () => { debug("_closeChannel/notifyCloseChannelSuccess: chanel " + this.channel + " closed"); this.channel = null; resolve(); }, notifyError: (error) => { debug("_closeChannel/notifyError: error closing channel, error" + error); reject(error); } }); }); }, _exchangeAPDU: function _exchangeAPDU(bytes) { DEBUG && debug("apdu " + JSON.stringify(bytes)); let apdu = this._bytesToAPDU(bytes); return new Promise((resolve, reject) => { UiccConnector.exchangeAPDU(this.channel, apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data, apdu.le, { notifyExchangeAPDUResponse: (sw1, sw2, data) => { debug("APDU response is " + sw1.toString(16) + sw2.toString(16) + " data: " + data); // 90 00 is "success" if (sw1 !== 0x90 && sw2 !== 0x00) { debug("rejecting APDU response"); reject(new Error("Response " + sw1 + "," + sw2)); return; } resolve(this._parseTLV(data)); }, notifyError: (error) => { debug("_exchangeAPDU/notifyError " + error); reject(error); } } ); }); }, _readBinaryFile: function _readBinaryFile(selectResponse) { DEBUG && debug("Select response: " + JSON.stringify(selectResponse)); // 0x80 tag parameter - get the elementary file (EF) length // without structural information. let fileLength = selectResponse[GP.TAG_FCP][0x80]; // If file is empty, no need to attempt to read it. if (fileLength[0] === 0 && fileLength[1] === 0) { return Promise.resolve(null); } // TODO READ BINARY with filelength not supported // let readApdu = this.READ_BINARY.concat(fileLength); return this._exchangeAPDU(this.READ_BINARY); }, _selectAndRead: function _selectAndRead(df) { return this._exchangeAPDU(this.SELECT_BY_DF.concat(df.length & 0xFF, df)) .then((resp) => this._readBinaryFile(resp)); }, _readODF: function _readODF() { debug("_readODF"); return this._selectAndRead(GP.ODF_DF); }, _readDODF: function _readDODF(odfFile) { debug("_readDODF, ODF file: " + odfFile); // Data Object Directory File (DODF) is used as an entry point to the // Access Control data. It is specified in PKCS#15 section 6.7.6. // DODF is referenced by the ODF file, which looks as follows: // A7 06 // 30 04 // 04 02 XY WZ // where [0xXY, 0xWZ] is a DF of DODF file. let DODF_DF = odfFile[GP.TAG_EF_ODF][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; return this._selectAndRead(DODF_DF); }, _readACMF: function _readACMF(dodfFile) { debug("_readACMF, DODF file: " + dodfFile); // ACMF file DF is referenced in DODF file, which looks like this: // // A1 29 // 30 00 // 30 0F // 0C 0D 47 50 20 53 45 20 41 63 63 20 43 74 6C // A1 14 // 30 12 // 06 0A 2A 86 48 86 FC 6B 81 48 01 01 <-- GPD registered OID // 30 04 // 04 02 AB CD <-- ACMF DF // A1 2B // 30 00 // 30 0F // 0C 0D 53 41 54 53 41 20 47 54 4F 20 31 2E 31 // A1 16 // 30 14 // 06 0C 2B 06 01 04 01 2A 02 6E 03 01 01 01 <-- some other OID // 30 04 // 04 02 XY WZ <-- some other file's DF // // DODF file consists of DataTypes with oidDO entries. Entry with OID // equal to "1.2.840.114283.200.1.1" ("2A 86 48 86 FC 6B 81 48 01 01") // contains DF of the ACMF. In the file above, it means that ACMF DF // equals to [0xAB, 0xCD], and not [0xXY, 0xWZ]. // // Algorithm used to encode OID to an byte array: // http://www.snmpsharpnet.com/?p=153 let gpdOid = [0x2A, // 1.2 0x86, 0x48, // 840 0x86, 0xFC, 0x6B, // 114283 0x81, 0x48, // 129 0x01, // 1 0x01]; // 1 let records = SEUtils.ensureIsArray(dodfFile[GP.TAG_EXTERNALDO]); // Look for the OID registered for GPD SE. let gpdRecords = records.filter((record) => { let oid = record[GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE][GP.TAG_OID]; return SEUtils.arraysEqual(oid, gpdOid); }); // [1] 7.1.5: "There shall be only one ACMF file per Secure Element. // If a Secure Element contains several ACMF files, then the security shall // be considered compromised and the Access Control enforcer shall forbid // access to all (...) apps." if (gpdRecords.length !== 1) { return Promise.reject(new Error(gpdRecords.length + " ACMF files found")); } let ACMain_DF = gpdRecords[0][GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE] [GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; return this._selectAndRead(ACMain_DF); }, _readACRules: function _readACRules(acMainFile) { debug("_readACRules, ACMain file: " + acMainFile); // ACMF looks like this: // // 30 10 // 04 08 XX XX XX XX XX XX XX XX // 30 04 // 04 02 XY WZ // // where [XY, WZ] is a DF of ACRF, and XX XX XX XX XX XX XX XX is a refresh // tag. let ACRules_DF = acMainFile[GP.TAG_SEQUENCE][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; return this._selectAndRead(ACRules_DF); }, _readACConditions: function _readACConditions(acRulesFile) { debug("_readACCondition, ACRules file: " + acRulesFile); let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]); if (acRules.length === 0) { debug("No rules found in ACRules file."); return Promise.reject(new Error("No rules found in ACRules file")); } // We first read all the condition files referenced in the ACRules file, // because ACRules file might reference one ACCondition file more than // once. Since reading it isn't exactly fast, we optimize here. let acReadQueue = Promise.resolve({}); acRules.forEach((ruleEntry) => { let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; // Promise chain read condition entries: let readAcCondition = (acConditionFiles) => { if (acConditionFiles[df] !== undefined) { debug("Skipping previously read acCondition df: " + df); return acConditionFiles; } return this._selectAndRead(df) .then((acConditionFileContents) => { acConditionFiles[df] = acConditionFileContents; return acConditionFiles; }); } acReadQueue = acReadQueue.then(readAcCondition); }); return acReadQueue; }, _parseRules: function _parseRules(acRulesFile, acConditionFiles) { DEBUG && debug("_parseRules: acConditionFiles " + JSON.stringify(acConditionFiles)); let rules = []; let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]); acRules.forEach((ruleEntry) => { DEBUG && debug("Parsing one rule: " + JSON.stringify(ruleEntry)); let rule = {}; // 0xA0 and 0x82 tags as per GPD spec sections C.1 - C.3. 0xA0 means // that rule describes access to one SE applet only (and its AID is // given). 0x82 means that rule describes acccess to all SE applets. let oneApplet = ruleEntry[GP.TAG_GPD_AID]; let allApplets = ruleEntry[GP.TAG_GPD_ALL]; if (oneApplet) { rule.applet = oneApplet[GP.TAG_OCTETSTRING]; } else if (allApplets) { rule.applet = Ci.nsIAccessRulesManager.ALL_APPLET; } else { throw Error("Unknown applet definition"); } let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; let condition = acConditionFiles[df]; if (condition === null) { rule.application = Ci.nsIAccessRulesManager.DENY_ALL; } else if (condition[GP.TAG_SEQUENCE]) { if (!Array.isArray(condition[GP.TAG_SEQUENCE]) && !condition[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]) { rule.application = Ci.nsIAccessRulesManager.ALLOW_ALL; } else { rule.application = SEUtils.ensureIsArray(condition[GP.TAG_SEQUENCE]) .map((conditionEntry) => { return conditionEntry[GP.TAG_OCTETSTRING]; }); } } else { throw Error("Unknown application definition"); } DEBUG && debug("Rule parsed, adding to the list: " + JSON.stringify(rule)); rules.push(rule); }); DEBUG && debug("All rules parsed, we have those in total: " + JSON.stringify(rules)); return rules; }, _parseTLV: function _parseTLV(bytes) { let containerTags = [ GP.TAG_SEQUENCE, GP.TAG_FCP, GP.TAG_GPD_AID, GP.TAG_EXTERNALDO, GP.TAG_INDIRECT, GP.TAG_EF_ODF ]; return SEUtils.parseTLV(bytes, containerTags); }, // TODO consider removing if better format for storing // APDU consts will be introduced _bytesToAPDU: function _bytesToAPDU(arr) { let apdu = { cla: arr[0] & 0xFF, ins: arr[1] & 0xFF, p1: arr[2] & 0xFF, p2: arr[3] & 0xFF, p3: arr[4] & 0xFF, le: 0 }; let data = (apdu.p3 > 0) ? (arr.slice(5)) : []; apdu.data = (data.length) ? SEUtils.byteArrayToHexString(data) : null; return apdu; }, classID: Components.ID("{3e046b4b-9e66-439a-97e0-98a69f39f55f}"), contractID: "@mozilla.org/secureelement/access-control/rules-manager;1", QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessRulesManager]) }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GPAccessRulesManager]);