blob: e5455d0345cc0b9231354358547ec595c5daec73 [file] [log] [blame]
Harald Welte21caf322022-07-16 14:06:46 +02001# coding=utf-8
2"""APDU definitions/decoders of ETSI TS 102 221, the core UICC spec.
3
4(C) 2022 by Harald Welte <laforge@osmocom.org>
5
6This program is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 2 of the License, or
9(at your option) any later version.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program. If not, see <http://www.gnu.org/licenses/>.
18"""
19
20import logging
21from pySim.construct import *
22from pySim.filesystem import *
23from pySim.apdu import ApduCommand, ApduCommandSet
24from typing import Optional, Dict, Tuple
25
26logger = logging.getLogger(__name__)
27
28# TS 102 221 Section 11.1.1
29class UiccSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['0X', '4X', '6X']):
30 _apdu_case = 4
31 _construct_p1 = Enum(Byte, df_ef_or_mf_by_file_id=0, child_df_of_current_df=1, parent_df_of_current_df=3,
32 df_name=4, path_from_mf=8, path_from_current_df=9)
33 _construct_p2 = BitStruct(Flag,
34 'app_session_control'/Enum(BitsInteger(2), activation_reset=0, termination=2),
35 'return'/Enum(BitsInteger(3), fcp=1, no_data=3),
36 'aid_control'/Enum(BitsInteger(2), first_or_only=0, last=1, next=2, previous=3))
37
38 @staticmethod
39 def _find_aid_substr(selectables, aid) -> Optional[CardADF]:
40 # full-length match
41 if aid in selectables:
42 return selectables[aid]
43 # sub-string match
44 for s in selectables.keys():
45 if aid[:len(s)] == s:
46 return selectables[s]
47 return None
48
49 def process_on_lchan(self, lchan: RuntimeLchan):
50 mode = self.cmd_dict['p1']
51 if mode in ['path_from_mf', 'path_from_current_df']:
52 # rewind to MF, if needed
53 if mode == 'path_from_mf':
54 lchan.selected_file = lchan.rs.mf
55 path = [self.cmd_data[i:i+2] for i in range(0, len(self.cmd_data), 2)]
56 for file in path:
57 file_hex = b2h(file)
58 if file_hex == '7fff': # current application
59 if not lchan.selected_adf:
60 sels = lchan.rs.mf.get_app_selectables(['ANAMES'])
61 # HACK: Assume USIM
62 logger.warning('SELECT relative to current ADF, but no ADF selected. Assuming ADF.USIM')
63 lchan.selected_adf = sels['ADF.USIM']
64 lchan.selected_file = lchan.selected_adf
65 #print("\tSELECT CUR_ADF %s" % lchan.selected_file)
66 continue
67 else:
68 sels = lchan.selected_file.get_selectables(['FIDS'])
69 if file_hex in sels:
70 if self.successful:
71 #print("\tSELECT %s" % sels[file_hex])
72 lchan.selected_file = sels[file_hex]
73 else:
74 #print("\tSELECT %s FAILED" % sels[file_hex])
75 pass
76 continue
77 logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path])))
78 elif mode == 'df_ef_or_mf_by_file_id':
79 if len(self.cmd_data) != 2:
80 raise ValueError('Expecting a 2-byte FID')
81 elif mode == 'df_name':
82 # Select by AID (can be sub-string!)
83 aid = self.cmd_dict['body']
84 sels = lchan.rs.mf.get_app_selectables(['AIDS'])
85 adf = self._find_aid_substr(sels, aid)
86 if adf:
87 lchan.selected_adf = adf
88 lchan.selected_file = lchan.selected_adf
89 #print("\tSELECT AID %s" % adf)
90 else:
91 logger.warning('SELECT UNKNOWN AID %s' % aid)
92 pass
93 else:
94 raise ValueError('Select Mode %s not implemented' % mode)
95 # decode the SELECT response
96 if self.successful:
97 self.file = lchan.selected_file
Harald Weltec61fbf42022-07-24 09:44:31 +020098 if 'body' in self.rsp_dict:
99 # not every SELECT is asking for the FCP in response...
100 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
Harald Welte21caf322022-07-16 14:06:46 +0200101 return None
102
103
104
105# TS 102 221 Section 11.1.2
106class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
107 _apdu_case = 2
108 _construct_p1 = Enum(Byte, no_indication=0, current_app_is_initialized=1, terminal_will_terminate_current_app=2)
109 _construct_p2 = Enum(Byte, response_like_select=0, response_df_name_tlv=1, response_no_data=0x0c)
110
111 def process_on_lchan(self, lchan):
112 if self.cmd_dict['p2'] == 'response_like_select':
113 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
114
115def _decode_binary_p1p2(p1, p2) -> Dict:
116 ret = {}
117 if p1 & 0x80:
118 ret['file'] = 'sfi'
119 ret['sfi'] = p1 & 0x1f
120 ret['offset'] = p2
121 else:
122 ret['file'] = 'currently_selected_ef'
123 ret['offset'] = ((p1 & 0x7f) << 8) & p2
124 return ret
125
126# TS 102 221 Section 11.1.3
127class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['0X', '4X', '6X']):
128 _apdu_case = 2
129 def _decode_p1p2(self):
130 return _decode_binary_p1p2(self.p1, self.p2)
131
132 def process_on_lchan(self, lchan):
133 self._determine_file(lchan)
134 if not isinstance(self.file, TransparentEF):
135 return b2h(self.rsp_data)
136 # our decoders don't work for non-zero offsets / short reads
137 if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
138 return b2h(self.rsp_data)
139 method = getattr(self.file, 'decode_bin', None)
140 if self.successful and callable(method):
141 return method(self.rsp_data)
142
143# TS 102 221 Section 11.1.4
144class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['0X', '4X', '6X']):
145 _apdu_case = 3
146 def _decode_p1p2(self):
147 return _decode_binary_p1p2(self.p1, self.p2)
148
149 def process_on_lchan(self, lchan):
150 self._determine_file(lchan)
151 if not isinstance(self.file, TransparentEF):
152 return b2h(self.rsp_data)
153 # our decoders don't work for non-zero offsets / short writes
154 if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]:
155 return b2h(self.cmd_data)
156 method = getattr(self.file, 'decode_bin', None)
157 if self.successful and callable(method):
158 return method(self.cmd_data)
159
160def _decode_record_p1p2(p1, p2):
161 ret = {}
162 ret['record_number'] = p1
163 if p2 >> 3 == 0:
164 ret['file'] = 'currently_selected_ef'
165 else:
166 ret['file'] = 'sfi'
167 ret['sfi'] = p2 >> 3
168 mode = p2 & 0x7
169 if mode == 2:
170 ret['mode'] = 'next_record'
171 elif mode == 3:
172 ret['mode'] = 'previous_record'
173 elif mode == 8:
174 ret['mode'] = 'absolute_current'
175 return ret
176
177# TS 102 221 Section 11.1.5
178class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['0X', '4X', '6X']):
179 _apdu_case = 2
180 def _decode_p1p2(self):
181 r = _decode_record_p1p2(self.p1, self.p2)
182 self.col_id = '%02u' % r['record_number']
183 return r
184
185 def process_on_lchan(self, lchan):
186 self._determine_file(lchan)
187 if not isinstance(self.file, LinFixedEF):
188 return b2h(self.rsp_data)
189 method = getattr(self.file, 'decode_record_bin', None)
190 if self.successful and callable(method):
191 return method(self.rsp_data)
192
193# TS 102 221 Section 11.1.6
194class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6X']):
195 _apdu_case = 3
196 def _decode_p1p2(self):
197 r = _decode_record_p1p2(self.p1, self.p2)
198 self.col_id = '%02u' % r['record_number']
199 return r
200
201 def process_on_lchan(self, lchan):
202 self._determine_file(lchan)
203 if not isinstance(self.file, LinFixedEF):
204 return b2h(self.cmd_data)
205 method = getattr(self.file, 'decode_record_bin', None)
206 if self.successful and callable(method):
207 return method(self.cmd_data)
208
209# TS 102 221 Section 11.1.7
210class SearchRecord(ApduCommand, n='SEARCH RECORD', ins=0xA2, cla=['0X', '4X', '6X']):
211 _apdu_case = 4
212 _construct_rsp = GreedyRange(Int8ub)
213
214 def _decode_p1p2(self):
215 ret = {}
216 sfi = self.p2 >> 3
217 if sfi == 0:
218 ret['file'] = 'currently_selected_ef'
219 else:
220 ret['file'] = 'sfi'
221 ret['sfi'] = sfi
222 mode = self.p2 & 0x7
223 if mode in [0x4, 0x5]:
224 if mode == 0x4:
225 ret['mode'] = 'forward_search'
226 else:
227 ret['mode'] = 'backward_search'
228 ret['record_number'] = self.p1
229 self.col_id = '%02u' % ret['record_number']
230 elif mode == 6:
231 ret['mode'] = 'enhanced_search'
232 # TODO: further decode
233 elif mode == 7:
234 ret['mode'] = 'proprietary_search'
235 return ret
236
237 def _decode_cmd(self):
238 ret = self._decode_p1p2()
239 if self.cmd_data:
240 if ret['mode'] == 'enhanced_search':
241 ret['search_indication'] = b2h(self.cmd_data[:2])
242 ret['search_string'] = b2h(self.cmd_data[2:])
243 else:
244 ret['search_string'] = b2h(self.cmd_data)
245 return ret
246
247 def process_on_lchan(self, lchan):
248 self._determine_file(lchan)
249 return self.to_dict()
250
251# TS 102 221 Section 11.1.8
252class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['8X', 'CX', 'EX']):
253 _apdu_case = 4
254
255PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1),
256 BitsInteger(2), 'reference_data_nr'/BitsInteger(5))
257# TS 102 221 Section 11.1.9
258class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
259 _apdu_case = 3
260 _construct_p2 = PinConstructP2
261
262 @staticmethod
263 def _pin_process(apdu):
264 processed = {
265 'scope': apdu.cmd_dict['p2']['scope'],
266 'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'],
267 }
268 if apdu.lc == 0:
269 # this is just a question on the counters remaining
270 processed['mode'] = 'check_remaining_attempts'
271 else:
272 processed['pin'] = b2h(apdu.cmd_data)
273 if apdu.sw[0] == 0x63:
274 processed['remaining_attempts'] = apdu.sw[1] & 0xf
275 return processed
276
277 @staticmethod
278 def _pin_is_success(sw):
279 if sw[0] == 0x63:
280 return True
281 else:
282 return False
283
284 def process_on_lchan(self, lchan: RuntimeLchan):
285 return VerifyPin._pin_process(self)
286
287 def _is_success(self):
288 return VerifyPin._pin_is_success(self.sw)
289
290
291# TS 102 221 Section 11.1.10
292class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
293 _apdu_case = 3
294 _construct_p2 = PinConstructP2
295
296 def process_on_lchan(self, lchan: RuntimeLchan):
297 return VerifyPin._pin_process(self)
298
299 def _is_success(self):
300 return VerifyPin._pin_is_success(self.sw)
301
302
303# TS 102 221 Section 11.1.11
304class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X']):
305 _apdu_case = 3
306 _construct_p2 = PinConstructP2
307
308 def process_on_lchan(self, lchan: RuntimeLchan):
309 return VerifyPin._pin_process(self)
310
311 def _is_success(self):
312 return VerifyPin._pin_is_success(self.sw)
313
314
315# TS 102 221 Section 11.1.12
316class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
317 _apdu_case = 3
318 _construct_p2 = PinConstructP2
319 def process_on_lchan(self, lchan: RuntimeLchan):
320 return VerifyPin._pin_process(self)
321
322 def _is_success(self):
323 return VerifyPin._pin_is_success(self.sw)
324
325
326# TS 102 221 Section 11.1.13
327class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X']):
328 _apdu_case = 3
329 _construct_p2 = PinConstructP2
330
331 def process_on_lchan(self, lchan: RuntimeLchan):
332 return VerifyPin._pin_process(self)
333
334 def _is_success(self):
335 return VerifyPin._pin_is_success(self.sw)
336
337
338# TS 102 221 Section 11.1.14
339class DeactivateFile(ApduCommand, n='DEACTIVATE FILE', ins=0x04, cla=['0X', '4X', '6X']):
340 _apdu_case = 1
341 _construct_p1 = BitStruct(BitsInteger(4),
342 'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0,
343 path_from_mf=8, path_from_current_df=9))
344
345# TS 102 221 Section 11.1.15
346class ActivateFile(ApduCommand, n='ACTIVATE FILE', ins=0x44, cla=['0X', '4X', '6X']):
347 _apdu_case = 1
348 _construct_p1 = DeactivateFile._construct_p1
349
350# TS 102 221 Section 11.1.16
351auth_p2_construct = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1),
352 BitsInteger(2),
353 'reference_data_nr'/BitsInteger(5))
354class Authenticate88(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
355 _apdu_case = 4
356 _construct_p2 = auth_p2_construct
357
358# TS 102 221 Section 11.1.16
359class Authenticate89(ApduCommand, n='AUTHENTICATE', ins=0x89, cla=['0X', '4X', '6X']):
360 _apdu_case = 4
361 _construct_p2 = auth_p2_construct
362
363# TS 102 221 Section 11.1.17
364class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X', '6X']):
365 _apdu_case = 2
366 _construct_p1 = Enum(Flag, open_channel=0, close_channel=1)
367 _construct_p2 = Struct('logical_channel_number'/Int8ub)
368 _construct_rsp = Struct('logical_channel_number'/Int8ub)
369
370 def process_global(self, rs):
371 if not self.successful:
372 return
373 mode = self.cmd_dict['p1']
374 if mode == 'open_channel':
375 created_channel_nr = self.cmd_dict['p2']['logical_channel_number']
376 if created_channel_nr == 0:
377 # auto-assignment by UICC
378 # pylint: disable=unsubscriptable-object
379 created_channel_nr = self.rsp_data[0]
380 manage_channel = rs.get_lchan_by_cla(self.cla)
381 manage_channel.add_lchan(created_channel_nr)
382 self.col_id = '%02u' % created_channel_nr
383 elif mode == 'close_channel':
384 closed_channel_nr = self.cmd_dict['p2']
385 rs.del_lchan(closed_channel_nr)
386 self.col_id = '%02u' % closed_channel_nr
387 else:
388 raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
389
390# TS 102 221 Section 11.1.18
391class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
392 _apdu_case = 2
393
394# TS 102 221 Section 11.1.19
395class TerminalCapability(ApduCommand, n='TERMINAL CAPABILITY', ins=0xAA, cla=['8X', 'CX', 'EX']):
396 _apdu_case = 3
397
398# TS 102 221 Section 11.1.20
399class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=['0X', '4X', '6X']):
400 @classmethod
401 def _get_apdu_case(cls, hdr:bytes) -> int:
402 p1 = hdr[2]
403 p2 = hdr[3]
404 if p1 & 0x7 == 0: # retrieve UICC Endpoints
405 return 2
406 elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
407 p2_cmd = p2 >> 5
408 if p2_cmd in [0,2,4]: # command data
409 return 3
410 elif p2_cmd in [1,3,5]: # response data
411 return 2
412 elif p1 & 0xf == 4: # terminate secure channel SA
413 return 3
414 raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
415
416# TS 102 221 Section 11.1.21
417class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6X']):
418 @classmethod
419 def _get_apdu_case(cls, hdr:bytes) -> int:
420 p1 = hdr[2]
421 if p1 & 0x04:
422 return 3
423 else:
424 return 2
425
426# TS 102 221 Section 11.1.22
427class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
428 _apdu_case = 4
429 _construct_p1 = BitStruct('rfu'/BitsInteger(7), 'mode'/Enum(Flag, suspend=0, resume=1))
430
431# TS 102 221 Section 11.1.23
432class GetIdentity(ApduCommand, n='GET IDENTITY', ins=0x78, cla=['8X', 'CX', 'EX']):
433 _apdu_case = 4
434 _construct_p2 = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1), BitsInteger(7))
435
436# TS 102 221 Section 11.1.24
437class ExchangeCapabilities(ApduCommand, n='EXCHANGE CAPABILITIES', ins=0x7A, cla=['80']):
438 _apdu_case = 4
439
440# TS 102 221 Section 11.2.1
441class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
442 _apdu_case = 3
443
444# TS 102 221 Section 11.2.2 / TS 102 223
445class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
446 _apdu_case = 4
447
448# TS 102 221 Section 11.2.3 / TS 102 223
449class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
450 _apdu_case = 2
451
452# TS 102 221 Section 11.2.3 / TS 102 223
453class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
454 _apdu_case = 3
455
456# TS 102 221 Section 11.3.1
457class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
458 _apdu_case = 4
459
460 @staticmethod
461 def _tlv_decode_cmd(self : ApduCommand) -> Dict:
462 c = {}
463 if self.p2 & 0xc0 == 0x80:
464 c['mode'] = 'first_block'
465 sfi = self.p2 & 0x1f
466 if sfi == 0:
467 c['file'] = 'currently_selected_ef'
468 else:
469 c['file'] = 'sfi'
470 c['sfi'] = sfi
471 c['tag'] = i2h([self.cmd_data[0]])
472 elif self.p2 & 0xdf == 0x00:
473 c['mode'] = 'next_block'
474 elif self.p2 & 0xdf == 0x40:
475 c['mode'] = 'retransmit_previous_block'
476 else:
477 logger.warning('%s: invalid P2=%02x' % (self, self.p2))
478 return c
479
480 def _decode_cmd(self):
481 return RetrieveData._tlv_decode_cmd(self)
482
483 def _decode_rsp(self):
484 # TODO: parse tag/len/val?
485 return b2h(self.rsp_data)
486
487
488# TS 102 221 Section 11.3.2
489class SetData(ApduCommand, n='SET DATA', ins=0xDB, cla=['8X', 'CX', 'EX']):
490 _apdu_case = 3
491
492 def _decode_cmd(self):
493 c = RetrieveData._tlv_decode_cmd(self)
494 if c['mode'] == 'first_block':
495 if len(self.cmd_data) == 0:
496 c['delete'] = True
497 # TODO: parse tag/len/val?
498 c['data'] = b2h(self.cmd_data)
499 return c
500
501
502# TS 102 221 Section 12.1.1
503class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['0X', '4X', '6X']):
504 _apdu_case = 2
505
506ApduCommands = ApduCommandSet('TS 102 221', cmds=[UiccSelect, UiccStatus, ReadBinary, UpdateBinary, ReadRecord,
507 UpdateRecord, SearchRecord, Increase, VerifyPin, ChangePin, DisablePin,
508 EnablePin, UnblockPin, DeactivateFile, ActivateFile, Authenticate88,
509 Authenticate89, ManageChannel, GetChallenge, TerminalCapability,
510 ManageSecureChannel, TransactData, SuspendUicc, GetIdentity,
511 ExchangeCapabilities, TerminalProfile, Envelope, Fetch, TerminalResponse,
512 RetrieveData, SetData, GetResponse])