blob: 9ecc2c976fcd85a1d4abfde4ca24f6087110df0d [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
98 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
99 return None
100
101
102
103# TS 102 221 Section 11.1.2
104class UiccStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['8X', 'CX', 'EX']):
105 _apdu_case = 2
106 _construct_p1 = Enum(Byte, no_indication=0, current_app_is_initialized=1, terminal_will_terminate_current_app=2)
107 _construct_p2 = Enum(Byte, response_like_select=0, response_df_name_tlv=1, response_no_data=0x0c)
108
109 def process_on_lchan(self, lchan):
110 if self.cmd_dict['p2'] == 'response_like_select':
111 return lchan.selected_file.decode_select_response(self.rsp_dict['body'])
112
113def _decode_binary_p1p2(p1, p2) -> Dict:
114 ret = {}
115 if p1 & 0x80:
116 ret['file'] = 'sfi'
117 ret['sfi'] = p1 & 0x1f
118 ret['offset'] = p2
119 else:
120 ret['file'] = 'currently_selected_ef'
121 ret['offset'] = ((p1 & 0x7f) << 8) & p2
122 return ret
123
124# TS 102 221 Section 11.1.3
125class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['0X', '4X', '6X']):
126 _apdu_case = 2
127 def _decode_p1p2(self):
128 return _decode_binary_p1p2(self.p1, self.p2)
129
130 def process_on_lchan(self, lchan):
131 self._determine_file(lchan)
132 if not isinstance(self.file, TransparentEF):
133 return b2h(self.rsp_data)
134 # our decoders don't work for non-zero offsets / short reads
135 if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]:
136 return b2h(self.rsp_data)
137 method = getattr(self.file, 'decode_bin', None)
138 if self.successful and callable(method):
139 return method(self.rsp_data)
140
141# TS 102 221 Section 11.1.4
142class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['0X', '4X', '6X']):
143 _apdu_case = 3
144 def _decode_p1p2(self):
145 return _decode_binary_p1p2(self.p1, self.p2)
146
147 def process_on_lchan(self, lchan):
148 self._determine_file(lchan)
149 if not isinstance(self.file, TransparentEF):
150 return b2h(self.rsp_data)
151 # our decoders don't work for non-zero offsets / short writes
152 if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]:
153 return b2h(self.cmd_data)
154 method = getattr(self.file, 'decode_bin', None)
155 if self.successful and callable(method):
156 return method(self.cmd_data)
157
158def _decode_record_p1p2(p1, p2):
159 ret = {}
160 ret['record_number'] = p1
161 if p2 >> 3 == 0:
162 ret['file'] = 'currently_selected_ef'
163 else:
164 ret['file'] = 'sfi'
165 ret['sfi'] = p2 >> 3
166 mode = p2 & 0x7
167 if mode == 2:
168 ret['mode'] = 'next_record'
169 elif mode == 3:
170 ret['mode'] = 'previous_record'
171 elif mode == 8:
172 ret['mode'] = 'absolute_current'
173 return ret
174
175# TS 102 221 Section 11.1.5
176class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['0X', '4X', '6X']):
177 _apdu_case = 2
178 def _decode_p1p2(self):
179 r = _decode_record_p1p2(self.p1, self.p2)
180 self.col_id = '%02u' % r['record_number']
181 return r
182
183 def process_on_lchan(self, lchan):
184 self._determine_file(lchan)
185 if not isinstance(self.file, LinFixedEF):
186 return b2h(self.rsp_data)
187 method = getattr(self.file, 'decode_record_bin', None)
188 if self.successful and callable(method):
189 return method(self.rsp_data)
190
191# TS 102 221 Section 11.1.6
192class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['0X', '4X', '6X']):
193 _apdu_case = 3
194 def _decode_p1p2(self):
195 r = _decode_record_p1p2(self.p1, self.p2)
196 self.col_id = '%02u' % r['record_number']
197 return r
198
199 def process_on_lchan(self, lchan):
200 self._determine_file(lchan)
201 if not isinstance(self.file, LinFixedEF):
202 return b2h(self.cmd_data)
203 method = getattr(self.file, 'decode_record_bin', None)
204 if self.successful and callable(method):
205 return method(self.cmd_data)
206
207# TS 102 221 Section 11.1.7
208class SearchRecord(ApduCommand, n='SEARCH RECORD', ins=0xA2, cla=['0X', '4X', '6X']):
209 _apdu_case = 4
210 _construct_rsp = GreedyRange(Int8ub)
211
212 def _decode_p1p2(self):
213 ret = {}
214 sfi = self.p2 >> 3
215 if sfi == 0:
216 ret['file'] = 'currently_selected_ef'
217 else:
218 ret['file'] = 'sfi'
219 ret['sfi'] = sfi
220 mode = self.p2 & 0x7
221 if mode in [0x4, 0x5]:
222 if mode == 0x4:
223 ret['mode'] = 'forward_search'
224 else:
225 ret['mode'] = 'backward_search'
226 ret['record_number'] = self.p1
227 self.col_id = '%02u' % ret['record_number']
228 elif mode == 6:
229 ret['mode'] = 'enhanced_search'
230 # TODO: further decode
231 elif mode == 7:
232 ret['mode'] = 'proprietary_search'
233 return ret
234
235 def _decode_cmd(self):
236 ret = self._decode_p1p2()
237 if self.cmd_data:
238 if ret['mode'] == 'enhanced_search':
239 ret['search_indication'] = b2h(self.cmd_data[:2])
240 ret['search_string'] = b2h(self.cmd_data[2:])
241 else:
242 ret['search_string'] = b2h(self.cmd_data)
243 return ret
244
245 def process_on_lchan(self, lchan):
246 self._determine_file(lchan)
247 return self.to_dict()
248
249# TS 102 221 Section 11.1.8
250class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['8X', 'CX', 'EX']):
251 _apdu_case = 4
252
253PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1),
254 BitsInteger(2), 'reference_data_nr'/BitsInteger(5))
255# TS 102 221 Section 11.1.9
256class VerifyPin(ApduCommand, n='VERIFY PIN', ins=0x20, cla=['0X', '4X', '6X']):
257 _apdu_case = 3
258 _construct_p2 = PinConstructP2
259
260 @staticmethod
261 def _pin_process(apdu):
262 processed = {
263 'scope': apdu.cmd_dict['p2']['scope'],
264 'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'],
265 }
266 if apdu.lc == 0:
267 # this is just a question on the counters remaining
268 processed['mode'] = 'check_remaining_attempts'
269 else:
270 processed['pin'] = b2h(apdu.cmd_data)
271 if apdu.sw[0] == 0x63:
272 processed['remaining_attempts'] = apdu.sw[1] & 0xf
273 return processed
274
275 @staticmethod
276 def _pin_is_success(sw):
277 if sw[0] == 0x63:
278 return True
279 else:
280 return False
281
282 def process_on_lchan(self, lchan: RuntimeLchan):
283 return VerifyPin._pin_process(self)
284
285 def _is_success(self):
286 return VerifyPin._pin_is_success(self.sw)
287
288
289# TS 102 221 Section 11.1.10
290class ChangePin(ApduCommand, n='CHANGE PIN', ins=0x24, cla=['0X', '4X', '6X']):
291 _apdu_case = 3
292 _construct_p2 = PinConstructP2
293
294 def process_on_lchan(self, lchan: RuntimeLchan):
295 return VerifyPin._pin_process(self)
296
297 def _is_success(self):
298 return VerifyPin._pin_is_success(self.sw)
299
300
301# TS 102 221 Section 11.1.11
302class DisablePin(ApduCommand, n='DISABLE PIN', ins=0x26, cla=['0X', '4X', '6X']):
303 _apdu_case = 3
304 _construct_p2 = PinConstructP2
305
306 def process_on_lchan(self, lchan: RuntimeLchan):
307 return VerifyPin._pin_process(self)
308
309 def _is_success(self):
310 return VerifyPin._pin_is_success(self.sw)
311
312
313# TS 102 221 Section 11.1.12
314class EnablePin(ApduCommand, n='ENABLE PIN', ins=0x28, cla=['0X', '4X', '6X']):
315 _apdu_case = 3
316 _construct_p2 = PinConstructP2
317 def process_on_lchan(self, lchan: RuntimeLchan):
318 return VerifyPin._pin_process(self)
319
320 def _is_success(self):
321 return VerifyPin._pin_is_success(self.sw)
322
323
324# TS 102 221 Section 11.1.13
325class UnblockPin(ApduCommand, n='UNBLOCK PIN', ins=0x2C, cla=['0X', '4X', '6X']):
326 _apdu_case = 3
327 _construct_p2 = PinConstructP2
328
329 def process_on_lchan(self, lchan: RuntimeLchan):
330 return VerifyPin._pin_process(self)
331
332 def _is_success(self):
333 return VerifyPin._pin_is_success(self.sw)
334
335
336# TS 102 221 Section 11.1.14
337class DeactivateFile(ApduCommand, n='DEACTIVATE FILE', ins=0x04, cla=['0X', '4X', '6X']):
338 _apdu_case = 1
339 _construct_p1 = BitStruct(BitsInteger(4),
340 'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0,
341 path_from_mf=8, path_from_current_df=9))
342
343# TS 102 221 Section 11.1.15
344class ActivateFile(ApduCommand, n='ACTIVATE FILE', ins=0x44, cla=['0X', '4X', '6X']):
345 _apdu_case = 1
346 _construct_p1 = DeactivateFile._construct_p1
347
348# TS 102 221 Section 11.1.16
349auth_p2_construct = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1),
350 BitsInteger(2),
351 'reference_data_nr'/BitsInteger(5))
352class Authenticate88(ApduCommand, n='AUTHENTICATE', ins=0x88, cla=['0X', '4X', '6X']):
353 _apdu_case = 4
354 _construct_p2 = auth_p2_construct
355
356# TS 102 221 Section 11.1.16
357class Authenticate89(ApduCommand, n='AUTHENTICATE', ins=0x89, cla=['0X', '4X', '6X']):
358 _apdu_case = 4
359 _construct_p2 = auth_p2_construct
360
361# TS 102 221 Section 11.1.17
362class ManageChannel(ApduCommand, n='MANAGE CHANNEL', ins=0x70, cla=['0X', '4X', '6X']):
363 _apdu_case = 2
364 _construct_p1 = Enum(Flag, open_channel=0, close_channel=1)
365 _construct_p2 = Struct('logical_channel_number'/Int8ub)
366 _construct_rsp = Struct('logical_channel_number'/Int8ub)
367
368 def process_global(self, rs):
369 if not self.successful:
370 return
371 mode = self.cmd_dict['p1']
372 if mode == 'open_channel':
373 created_channel_nr = self.cmd_dict['p2']['logical_channel_number']
374 if created_channel_nr == 0:
375 # auto-assignment by UICC
376 # pylint: disable=unsubscriptable-object
377 created_channel_nr = self.rsp_data[0]
378 manage_channel = rs.get_lchan_by_cla(self.cla)
379 manage_channel.add_lchan(created_channel_nr)
380 self.col_id = '%02u' % created_channel_nr
381 elif mode == 'close_channel':
382 closed_channel_nr = self.cmd_dict['p2']
383 rs.del_lchan(closed_channel_nr)
384 self.col_id = '%02u' % closed_channel_nr
385 else:
386 raise ValueError('Unsupported MANAGE CHANNEL P1=%02X' % self.p1)
387
388# TS 102 221 Section 11.1.18
389class GetChallenge(ApduCommand, n='GET CHALLENGE', ins=0x84, cla=['0X', '4X', '6X']):
390 _apdu_case = 2
391
392# TS 102 221 Section 11.1.19
393class TerminalCapability(ApduCommand, n='TERMINAL CAPABILITY', ins=0xAA, cla=['8X', 'CX', 'EX']):
394 _apdu_case = 3
395
396# TS 102 221 Section 11.1.20
397class ManageSecureChannel(ApduCommand, n='MANAGE SECURE CHANNEL', ins=0x73, cla=['0X', '4X', '6X']):
398 @classmethod
399 def _get_apdu_case(cls, hdr:bytes) -> int:
400 p1 = hdr[2]
401 p2 = hdr[3]
402 if p1 & 0x7 == 0: # retrieve UICC Endpoints
403 return 2
404 elif p1 & 0xf in [1,2,3]: # establish sa, start secure channel SA
405 p2_cmd = p2 >> 5
406 if p2_cmd in [0,2,4]: # command data
407 return 3
408 elif p2_cmd in [1,3,5]: # response data
409 return 2
410 elif p1 & 0xf == 4: # terminate secure channel SA
411 return 3
412 raise ValueError('%s: Unable to detect APDU case for %s' % (cls.__name__, b2h(hdr)))
413
414# TS 102 221 Section 11.1.21
415class TransactData(ApduCommand, n='TRANSACT DATA', ins=0x75, cla=['0X', '4X', '6X']):
416 @classmethod
417 def _get_apdu_case(cls, hdr:bytes) -> int:
418 p1 = hdr[2]
419 if p1 & 0x04:
420 return 3
421 else:
422 return 2
423
424# TS 102 221 Section 11.1.22
425class SuspendUicc(ApduCommand, n='SUSPEND UICC', ins=0x76, cla=['80']):
426 _apdu_case = 4
427 _construct_p1 = BitStruct('rfu'/BitsInteger(7), 'mode'/Enum(Flag, suspend=0, resume=1))
428
429# TS 102 221 Section 11.1.23
430class GetIdentity(ApduCommand, n='GET IDENTITY', ins=0x78, cla=['8X', 'CX', 'EX']):
431 _apdu_case = 4
432 _construct_p2 = BitStruct('scope'/Enum(Flag, mf=0, df_adf_specific=1), BitsInteger(7))
433
434# TS 102 221 Section 11.1.24
435class ExchangeCapabilities(ApduCommand, n='EXCHANGE CAPABILITIES', ins=0x7A, cla=['80']):
436 _apdu_case = 4
437
438# TS 102 221 Section 11.2.1
439class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['80']):
440 _apdu_case = 3
441
442# TS 102 221 Section 11.2.2 / TS 102 223
443class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['80']):
444 _apdu_case = 4
445
446# TS 102 221 Section 11.2.3 / TS 102 223
447class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['80']):
448 _apdu_case = 2
449
450# TS 102 221 Section 11.2.3 / TS 102 223
451class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['80']):
452 _apdu_case = 3
453
454# TS 102 221 Section 11.3.1
455class RetrieveData(ApduCommand, n='RETRIEVE DATA', ins=0xCB, cla=['8X', 'CX', 'EX']):
456 _apdu_case = 4
457
458 @staticmethod
459 def _tlv_decode_cmd(self : ApduCommand) -> Dict:
460 c = {}
461 if self.p2 & 0xc0 == 0x80:
462 c['mode'] = 'first_block'
463 sfi = self.p2 & 0x1f
464 if sfi == 0:
465 c['file'] = 'currently_selected_ef'
466 else:
467 c['file'] = 'sfi'
468 c['sfi'] = sfi
469 c['tag'] = i2h([self.cmd_data[0]])
470 elif self.p2 & 0xdf == 0x00:
471 c['mode'] = 'next_block'
472 elif self.p2 & 0xdf == 0x40:
473 c['mode'] = 'retransmit_previous_block'
474 else:
475 logger.warning('%s: invalid P2=%02x' % (self, self.p2))
476 return c
477
478 def _decode_cmd(self):
479 return RetrieveData._tlv_decode_cmd(self)
480
481 def _decode_rsp(self):
482 # TODO: parse tag/len/val?
483 return b2h(self.rsp_data)
484
485
486# TS 102 221 Section 11.3.2
487class SetData(ApduCommand, n='SET DATA', ins=0xDB, cla=['8X', 'CX', 'EX']):
488 _apdu_case = 3
489
490 def _decode_cmd(self):
491 c = RetrieveData._tlv_decode_cmd(self)
492 if c['mode'] == 'first_block':
493 if len(self.cmd_data) == 0:
494 c['delete'] = True
495 # TODO: parse tag/len/val?
496 c['data'] = b2h(self.cmd_data)
497 return c
498
499
500# TS 102 221 Section 12.1.1
501class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['0X', '4X', '6X']):
502 _apdu_case = 2
503
504ApduCommands = ApduCommandSet('TS 102 221', cmds=[UiccSelect, UiccStatus, ReadBinary, UpdateBinary, ReadRecord,
505 UpdateRecord, SearchRecord, Increase, VerifyPin, ChangePin, DisablePin,
506 EnablePin, UnblockPin, DeactivateFile, ActivateFile, Authenticate88,
507 Authenticate89, ManageChannel, GetChallenge, TerminalCapability,
508 ManageSecureChannel, TransactData, SuspendUicc, GetIdentity,
509 ExchangeCapabilities, TerminalProfile, Envelope, Fetch, TerminalResponse,
510 RetrieveData, SetData, GetResponse])