aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Welte <laforge@osmocom.org>2021-10-20 18:40:54 +0200
committerlaforge <laforge@osmocom.org>2021-11-11 09:07:57 +0000
commit95ce6b170864a0d109fd7233c261a4d1ae3d0820 (patch)
tree7d67a1430adf2834993295de811a4e2288361d88
parenta4df942fe67d0d6a40dd4ab86c5f8d1ad8092fc7 (diff)
ARA-M related command support
This introduces support for talking to the ARA-M application on a card, as specified in the GlobalPlatform "Secure Element Access Control" specification v1.1. Change-Id: Ia9107a4629c3d68320f32bbd4dd26e1f430717da
-rw-r--r--docs/shell.rst88
-rwxr-xr-xpySim-shell.py2
-rw-r--r--pySim/ara_m.py364
3 files changed, 454 insertions, 0 deletions
diff --git a/docs/shell.rst b/docs/shell.rst
index e5d70a0..3ab1113 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -443,6 +443,94 @@ authenticate
:func: ADF_USIM.AddlShellCommands.authenticate_parser
+ARA-M commands
+--------------
+
+The ARA-M commands exist to manage the access rules stored in an ARA-M applet on the card.
+
+ARA-M in the context of SIM cards is primarily used to enable Android UICC Carrier Privileges,
+please see https://source.android.com/devices/tech/config/uicc for more details on the background.
+
+
+aram_get_all
+~~~~~~~~~~~~
+
+Obtain and decode all access rules from the ARA-M applet on the card.
+
+NOTE: if the total size of the access rules exceeds 255 bytes, this command will fail, as
+it doesn't yet implement fragmentation/reassembly on rule retrieval. YMMV
+
+::
+
+ pySIM-shell (MF/ADF.ARA-M)> aram_get_all
+ [
+ {
+ "ResponseAllRefArDO": [
+ {
+ "RefArDO": [
+ {
+ "RefDO": [
+ {
+ "AidRefDO": "ffffffffffff"
+ },
+ {
+ "DevAppIdRefDO": "e46872f28b350b7e1f140de535c2a8d5804f0be3"
+ }
+ ]
+ },
+ {
+ "ArDO": [
+ {
+ "ApduArDO": {
+ "generic_access_rule": "always"
+ }
+ },
+ {
+ "PermArDO": {
+ "permissions": "0000000000000001"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+
+aram_get_config
+~~~~~~~~~~~~~~~
+Perform Config handshake with ARA-M applet: Tell it our version and retrieve its version.
+
+NOTE: Not supported in all ARA-M implementations.
+
+.. argparse::
+ :module: pySim.ara_m
+ :func: ADF_ARAM.AddlShellCommands.get_config_parser
+
+
+aram_store_ref_ar_do
+~~~~~~~~~~~~~~~~~~~~
+Store a [new] access rule on the ARA-M applet.
+
+.. argparse::
+ :module: pySim.ara_m
+ :func: ADF_ARAM.AddlShellCommands.store_ref_ar_do_parse
+
+For example, to store an Android UICC carrier privilege rule for the SHA1 hash of the certificate used to sign the CoIMS android app of Supreeth Herle (https://github.com/herlesupreeth/CoIMS_Wiki) you can use the following command:
+
+::
+
+ pySIM-shell (MF/ADF.ARA-M)> aram_store_ref_ar_do --aid FFFFFFFFFFFF --device-app-id E46872F28B350B7E1F140DE535C2A8D5804F0BE3 --android-permissions 0000000000000001 --apdu-always
+
+
+aram_delete_all
+~~~~~~~~~~~~~~~
+This command will request deletion of all access rules stored within the
+ARA-M applet. Use it with caution, there is no undo. Any rules later
+intended must be manually inserted again using `aram_store_ref_ar_do`
+
+
cmd2 settable parameters
------------------------
diff --git a/pySim-shell.py b/pySim-shell.py
index 037b843..128c0ea 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -49,6 +49,7 @@ from pySim.ts_51_011 import CardProfileSIM, DF_TELECOM, DF_GSM
from pySim.ts_102_221 import CardProfileUICC
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
+from pySim.ara_m import CardApplicationARAM
from pySim.gsm_r import DF_EIRENE
# we need to import this module so that the SysmocomSJA2 sub-class of
@@ -86,6 +87,7 @@ def init_card(sl):
profile = CardProfileUICC()
profile.add_application(CardApplicationUSIM())
profile.add_application(CardApplicationISIM())
+ profile.add_application(CardApplicationARAM())
rs = RuntimeState(card, profile)
# FIXME: do this dynamically
diff --git a/pySim/ara_m.py b/pySim/ara_m.py
new file mode 100644
index 0000000..5dee3e0
--- /dev/null
+++ b/pySim/ara_m.py
@@ -0,0 +1,364 @@
+# -*- coding: utf-8 -*-
+
+# without this, pylint will fail when inner classes are used
+# within the 'nested' kwarg of our TlvMeta metaclass on python 3.7 :(
+# pylint: disable=undefined-variable
+
+"""
+Support for the Secure Element Access Control, specifically the ARA-M inside an UICC.
+"""
+
+#
+# Copyright (C) 2021 Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+from construct import *
+from construct import Optional as COptional
+from pySim.construct import *
+from pySim.filesystem import *
+from pySim.tlv import *
+
+# various BER-TLV encoded Data Objects (DOs)
+
+class AidRefDO(BER_TLV_IE, tag=0x4f):
+ # SEID v1.1 Table 6-3
+ _construct = HexAdapter(GreedyBytes)
+
+class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
+ # SEID v1.1 Table 6-3
+ pass
+
+class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
+ # SEID v1.1 Table 6-4
+ _construct = HexAdapter(GreedyBytes)
+
+class PkgRefDO(BER_TLV_IE, tag=0xca):
+ # Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
+ _construct = Struct('package_name_string'/GreedyString("ascii"))
+
+class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO,AidRefEmptyDO,DevAppIdRefDO,PkgRefDO]):
+ # SEID v1.1 Table 6-5
+ pass
+
+class ApduArDO(BER_TLV_IE, tag=0xd0):
+ # SEID v1.1 Table 6-8
+ def _from_bytes(self, do:bytes):
+ if len(do) == 1:
+ if do[0] == 0x00:
+ self.decoded = {'generic_access_rule': 'never'}
+ return self.decoded
+ elif do[0] == 0x01:
+ self.decoded = {'generic_access_rule': 'always'}
+ return self.decoded
+ else:
+ return ValueError('Invalid 1-byte generic APDU access rule')
+ else:
+ if len(do) % 8:
+ return ValueError('Invalid non-modulo-8 length of APDU filter: %d' % len(do))
+ self.decoded['apdu_filter'] = []
+ offset = 0
+ while offset < len(do):
+ self.decoded['apdu_filter'] += {'header': b2h(do[offset:offset+4]),
+ 'mask': b2h(do[offset+4:offset+8])}
+ self.decoded = res
+ return res
+ def _to_bytes(self):
+ if 'generic_access_rule' in self.decoded:
+ if self.decoded['generic_access_rule'] == 'never':
+ return b'\x00'
+ elif self.decoded['generic_access_rule'] == 'always':
+ return b'\x01'
+ else:
+ return ValueError('Invalid 1-byte generic APDU access rule')
+ else:
+ if not 'apdu_filter' in self.decoded:
+ return ValueError('Invalid APDU AR DO')
+ filters = self.decoded['apdu_filter']
+ res = b''
+ for f in filters:
+ if not 'header' in f or not 'mask' in f:
+ return ValueError('APDU filter must contain header and mask')
+ header_b = h2b(f['header'])
+ mask_b = h2b(f['mask'])
+ if len(header_b) != 4 or len(mask_b) != 4:
+ return ValueError('APDU filter header and mask must each be 4 bytes')
+ res += header_b + mask_b
+ return res
+
+class NfcArDO(BER_TLV_IE, tag=0xd1):
+ # SEID v1.1 Table 6-9
+ _construct = Struct('nfc_event_access_rule'/Enum(Int8ub, never=0, always=1))
+
+class PermArDO(BER_TLV_IE, tag=0xdb):
+ # Android UICC Carrier Privileges specific extension, see https://source.android.com/devices/tech/config/uicc
+ _construct = Struct('permissions'/HexAdapter(Bytes(8)))
+
+class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
+ # SEID v1.1 Table 6-7
+ pass
+
+class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
+ # SEID v1.1 Table 6-6
+ pass
+
+class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
+ # SEID v1.1 Table 4-2
+ pass
+
+class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
+ # SEID v1.1 Table 4-3
+ pass
+
+class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
+ # SEID v1.1 Table 4-4
+ _construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
+
+class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
+ # SEID v1.1 Table 6-12
+ _construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
+
+class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
+ # SEID v1.1 Table 6-10
+ pass
+
+class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
+ # SEID v1.1 Table 5-14
+ pass
+
+class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
+ # SEID v1.1 Table 6-11
+ pass
+
+class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
+ # SEID v1.1 Table 4-5
+ pass
+
+class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
+ # SEID v1.1 Table 5-2
+ pass
+
+class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
+ # SEID v1.1 Table 5-4
+ pass
+
+class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
+ # SEID V1.1 Table 5-6
+ pass
+
+class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
+ # SEID v1.1 Table 5-7
+ pass
+
+class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
+ # SEID v1.1 Table 5-8
+ pass
+
+class CommandGetAll(BER_TLV_IE, tag=0xf4):
+ # SEID v1.1 Table 5-9
+ pass
+
+class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
+ # SEID v1.1 Table 5-10
+ pass
+
+class CommandGetNext(BER_TLV_IE, tag=0xf5):
+ # SEID v1.1 Table 5-11
+ pass
+
+class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
+ # SEID v1.1 Table 5-12
+ pass
+
+class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
+ # SEID v1.1 Table 5-13
+ pass
+
+class BlockDO(BER_TLV_IE, tag=0xe7):
+ # SEID v1.1 Table 6-13
+ _construct = Struct('offset'/Int16ub, 'length'/Int8ub)
+
+
+# SEID v1.1 Table 4-1
+class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
+ pass
+
+# SEID v1.1 Table 4-2
+class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
+ ResponseRefreshTagDO, ResponseAramConfigDO]):
+ pass
+
+# SEID v1.1 Table 5-1
+class StoreCommandDoCollection(TLV_IE_Collection,
+ nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
+ CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
+ CommandGet, CommandGetAll, CommandGetClientAidsDO,
+ CommandGetNext, CommandGetDeviceConfigDO]):
+ pass
+
+
+# SEID v1.1 Section 5.1.2
+class StoreResponseDoCollection(TLV_IE_Collection,
+ nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
+ pass
+
+class ADF_ARAM(CardADF):
+ def __init__(self, aid='a00000015141434c00', name='ADF.ARA-M', fid=None, sfid=None,
+ desc='ARA-M Application'):
+ super().__init__(aid=aid, fid=fid, sfid=sfid, name=name, desc=desc)
+ self.shell_commands += [self.AddlShellCommands()]
+ files = []
+ self.add_files(files)
+
+ @staticmethod
+ def xceive_apdu_tlv(tp, hdr:Hexstr, cmd_do, resp_cls, exp_sw='9000'):
+ """Transceive an APDU with the card, transparently encoding the command data from TLV
+ and decoding the response data tlv."""
+ if cmd_do:
+ cmd_do_enc = cmd_do.to_ie()
+ cmd_do_len = len(cmd_do_enc)
+ if cmd_do_len > 255:
+ return ValueError('DO > 255 bytes not supported yet')
+ else:
+ cmd_do_enc = b''
+ cmd_do_len = 0
+ c_apdu = hdr + ('%02x' % cmd_do_len) + b2h(cmd_do_enc)
+ (data, sw) = tp.send_apdu_checksw(c_apdu, exp_sw)
+ if data:
+ if resp_cls:
+ resp_do = resp_cls()
+ resp_do.from_tlv(h2b(data))
+ return resp_do
+ else:
+ return data
+ else:
+ return None
+
+ @staticmethod
+ def store_data(tp, do) -> bytes:
+ """Build the Command APDU for STORE DATA."""
+ return ADF_ARAM.xceive_apdu_tlv(tp, '80e29000', do, StoreResponseDoCollection)
+
+ @staticmethod
+ def get_all(tp):
+ return ADF_ARAM.xceive_apdu_tlv(tp, '80caff40', None, GetResponseDoCollection)
+
+ @staticmethod
+ def get_config(tp, v_major=0, v_minor=0, v_patch=1):
+ cmd_do = DeviceConfigDO()
+ cmd_do.from_dict([{'DeviceInterfaceVersionDO': {'major': v_major, 'minor': v_minor, 'patch': v_patch }}])
+ return ADF_ARAM.xceive_apdu_tlv(tp, '80cadf21', cmd_do, ResponseAramConfigDO)
+
+ @with_default_category('Application-Specific Commands')
+ class AddlShellCommands(CommandSet):
+ def __init(self):
+ super().__init__()
+
+ def do_aram_get_all(self, opts):
+ """GET DATA [All] on the ARA-M Applet"""
+ res_do = ADF_ARAM.get_all(self._cmd.card._scc._tp)
+ if res_do:
+ self._cmd.poutput_json(res_do.to_dict())
+
+ def do_aram_get_config(self, opts):
+ """GET DATA [Config] on the ARA-M Applet"""
+ res_do = ADF_ARAM.get_config(self._cmd.card._scc._tp)
+ if res_do:
+ self._cmd.poutput_json(res_do.to_dict())
+
+ store_ref_ar_do_parse = argparse.ArgumentParser()
+ # REF-DO
+ store_ref_ar_do_parse.add_argument('--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
+ aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
+ aid_grp.add_argument('--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
+ aid_grp.add_argument('--aid-empty', action='store_true', help='No specific SE application, applies to all applications')
+ store_ref_ar_do_parse.add_argument('--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
+ # AR-DO
+ apdu_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
+ apdu_grp.add_argument('--apdu-never', action='store_true', help='APDU access is not allowed')
+ apdu_grp.add_argument('--apdu-always', action='store_true', help='APDU access is allowed')
+ apdu_grp.add_argument('--apdu-filter', help='APDU filter: 4 byte CLA/INS/P1/P2 followed by 4 byte mask (8 hex bytes)')
+ nfc_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
+ nfc_grp.add_argument('--nfc-always', action='store_true', help='NFC event access is allowed')
+ nfc_grp.add_argument('--nfc-never', action='store_true', help='NFC event access is not allowed')
+ store_ref_ar_do_parse.add_argument('--android-permissions', help='Android UICC Carrier Privilege Permissions (8 hex bytes)')
+
+ @cmd2.with_argparser(store_ref_ar_do_parse)
+ def do_aram_store_ref_ar_do(self, opts):
+ """Perform STORE DATA [Command-Store-REF-AR-DO] to store a new access rule."""
+ # REF
+ ref_do_content = []
+ if opts.aid:
+ ref_do_content += [{'AidRefDO': opts.aid}]
+ elif opts.aid_empty:
+ ref_do_content += [{'AidRefEmptyDO': None}]
+ ref_do_content += [{'DevAppIdRefDO': opts.device_app_id}]
+ if opts.pkg_ref:
+ ref_do_content += [{'PkgRefDO': opts.pkg_ref}]
+ # AR
+ ar_do_content = []
+ if opts.apdu_never:
+ ar_do_content += [{'ApduArDO': {'generic_access_rule': 'never'}}]
+ elif opts.apdu_always:
+ ar_do_content += [{'ApduArDO': {'generic_access_rule': 'always'}}]
+ elif opts.apdu_filter:
+ # TODO: multiple filters
+ ar_do_content += [{'ApduArDO': {'apdu_filter': [opts.apdu_filter]}}]
+ if opts.nfc_always:
+ ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'always'}}]
+ elif opts.nfc_never:
+ ar_do_content += [{'NfcArDO': {'nfc_event_access_rule': 'never'}}]
+ if opts.android_permissions:
+ ar_do_content += [{'PermArDO': {'permissions': opts.android_permissions}}]
+ d = [{'RefArDO': [{ 'RefDO': ref_do_content}, {'ArDO': ar_do_content }]}]
+ csrado = CommandStoreRefArDO()
+ csrado.from_dict(d)
+ res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, csrado)
+ if res_do:
+ self._cmd.poutput_json(res_do.to_dict())
+
+ def do_aram_delete_all(self, opts):
+ """Perform STORE DATA [Command-Delete[all]] to delete all access rules."""
+ deldo = CommandDelete()
+ res_do = ADF_ARAM.store_data(self._cmd.card._scc._tp, deldo)
+ if res_do:
+ self._cmd.poutput_json(res_do.to_dict())
+
+
+# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
+sw_aram = {
+ 'ARA-M': {
+ '6381': 'Rule successfully stored but an access rule already exists',
+ '6382': 'Rule successfully stored bu contained at least one unknown (discarded) BER-TLV',
+ '6581': 'Memory Problem',
+ '6700': 'Wrong Length in Lc',
+ '6981': 'DO is not supported by the ARA-M/ARA-C',
+ '6982': 'Security status not satisfied',
+ '6984': 'Rules have been updated and must be read again / logical channels in use',
+ '6985': 'Conditions not satisfied',
+ '6a80': 'Incorrect values in the command data',
+ '6a84': 'Rules have been updated and must be read again',
+ '6a86': 'Incorrect P1 P2',
+ '6a88': 'Referenced data not found',
+ '6a89': 'Conflicting access rule already exists in the Secure Element',
+ '6d00': 'Invalid instruction',
+ '6e00': 'Invalid class',
+ }
+}
+
+class CardApplicationARAM(CardApplication):
+ def __init__(self):
+ super().__init__('ARA-M', adf=ADF_ARAM(), sw=sw_aram)