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