mirror of https://gerrit.osmocom.org/pysim
413 lines
15 KiB
Python
413 lines
15 KiB
Python
# -*- 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):
|
|
"""Perform GET DATA [Config] on the ARA-M Applet: Tell it our version and retrieve its version."""
|
|
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)
|