FreeRDP
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Modules Pages
smartcard-interpreter.py
1#!/usr/bin/env python3
2#
3# Copyright 2022 David Fort <contact@hardening-consulting.com>
4#
5# This script is meant to parse some FreeRDP logs in DEBUG mode (WLOG_LEVEL=DEBUG) and interpret the
6# smartcard traffic, dissecting the PIV or GIDS commands
7#
8# usage:
9# * live: WLOG_LEVEL=DEBUG xfreerdp <args with smartcard> | python3 smartcard-interpreter.py
10# * on an existing log file: python3 smartcard-interpreter.py <log file>
11#
12import sys
13import codecs
14
15
16CMD_NAMES = {
17 0x04: "DEACTIVATE FILE",
18 0x0C: "ERASE RECORD",
19 0x0E: "ERASE BINARY",
20 0x0F: "ERASE BINARY",
21 0x20: "VERIFY",
22 0x21: "VERIFY",
23 0x22: "MSE",
24 0x24: "CHANGE REFERENCE DATA",
25 0x25: "MSE",
26 0x26: "DISABLE VERIFICATION REQUIREMENT",
27 0x28: "ENABLE VERIFICATION REQUIREMENT",
28 0x2A: "PSO",
29 0x2C: "RESET RETRY COUNTER",
30 0x2D: "RESET RETRY COUNTER",
31 0x44: "ACTIVATE FILE",
32 0x46: "GENERATE ASYMMETRIC KEY PAIR",
33 0x47: "GENERATE ASYMMETRIC KEY PAIR",
34 0x84: "GET CHALLENGE",
35 0x86: "GENERAL AUTHENTICATE",
36 0x87: "GENERAL AUTHENTICATE",
37 0x88: "INTERNAL AUTHENTICATE",
38 0xA0: "SEARCH BINARY",
39 0xA1: "SEARCH BINARY",
40 0xA2: "SEARCH RECORD",
41 0xA4: "SELECT",
42 0xB0: "READ BINARY",
43 0xB1: "READ BINARY",
44 0xB2: "READ RECORD",
45 0xB3: "READ RECORD",
46 0xC0: "GET RESPONSE",
47 0xC2: "ENVELOPE",
48 0xC3: "ENVELOPE",
49 0xCA: "GET DATA",
50 0xCB: "GET DATA",
51 0xD0: "WRITE BINARY",
52 0xD1: "WRITE BINARY",
53 0xD2: "WRITE RECORD",
54 0xD6: "UPDATE BINARY",
55 0xD7: "UPDATE BINARY",
56 0xDA: "PUT DATA",
57 0xDB: "PUT DATA",
58 0xDC: "UPDATE RECORD",
59 0xDD: "UPDATE RECORD",
60 0xE0: "CREATE FILE",
61 0xE2: "APPEND RECORD",
62 0xE4: "DELETE FILE",
63 0xE6: "TERMINATE DF",
64 0xE8: "TERMINATE EF",
65 0xFE: "TERMINATE CARD USAGE",
66}
67
68AIDs = {
69 "a00000039742544659": "MsGidsAID",
70 "a000000308": "PIV",
71 "a0000003974349445f0100": "SC PNP",
72 "a0000001510000": "GPC",
73}
74
75FIDs = {
76 0x0000: "Current EF",
77 0x2F00: "EF.DIR",
78 0x2F01: "EF.ATR",
79 0x3FFF: "Current application(ADF)",
80}
81
82DOs = {
83 "df1f": "DO_FILESYSTEMTABLE",
84 "df20": "DO_CARDID",
85 "df21": "DO_CARDAPPS",
86 "df22": "DO_CARDCF",
87 "df23": "DO_CMAPFILE",
88 "df24": "DO_KXC00",
89}
90
91ERROR_CODES = {
92 0x9000: "success",
93 0x6100: "more bytes",
94 0x6282: "end of file or record",
95 0x6283: "card locked",
96 0x63C0: "warning counter 0",
97 0x63C1: "warning counter 1",
98 0x63C2: "warning counter 2",
99 0x63C3: "warning counter 3",
100 0x63C4: "warning counter 4",
101 0x63C5: "warning counter 5",
102 0x63C6: "warning counter 6",
103 0x63C7: "warning counter 7",
104 0x63C8: "warning counter 8",
105 0x63C9: "warning counter 9",
106 0x6982: "security status not satisfied",
107 0x6882: "Secure messaging not supported",
108 0x6985: "condition of use not satisfied",
109 0x6A80: "incorrect parameter cmd data field",
110 0x6A81: "function not supported",
111 0x6A82: "file or application not found",
112 0x6A83: "record not found",
113 0x6A88: "REFERENCE DATA NOT FOUND",
114 0x6D00: "unsupported",
115}
116
117PIV_OIDs = {
118 "5fc101": "X.509 Certificate for Card Authentication",
119 "5fc102": "Card Holder Unique Identifier",
120 "5fc103": "Cardholder Fingerprints",
121 "5fc105": "X.509 Certificate for PIV Authentication",
122 "5fc106": "Security Object",
123 "5fc107": "Card Capability Container",
124 "5fc108": "Cardholder Facial Image",
125 "5fc10a": "X.509 Certificate for Digital Signature",
126 "5fc10b": "X.509 Certificate for Key Management",
127 "5fc10d": "Retired X.509 Certificate for Key Management 1",
128 "5fc10e": "Retired X.509 Certificate for Key Management 2",
129 "5fc10f": "Retired X.509 Certificate for Key Management 3",
130}
131
132class ApplicationDummy(object):
133 """Base application"""
134
135 def __init__(self, aid):
136 self.aid = aid
137
138 def getAID(self):
139 return self.aid
140
141 def selectResult(self, fci, status, body):
142 return 'selectResult(fci=%s, status=0x%x) = %s\n' %(fci, status, body.hex())
143
144 def getData(self, fileId, bytes):
145 return 'getData(status=0x%x) = %s\n' %(fileId, bytes.hex())
146
147 def getDataResult(self, status, body):
148 return 'getDataResult(status=0x%x) = %s\n' %(status, body.hex())
149
150 def mse(self, body):
151 return body.hex()
152
153 def mseResult(self, status, body):
154 return body.hex()
155
156 def pso(self, body):
157 return body.hex()
158
159 def psoResult(self, status, body):
160 return body.hex()
161
162 def getResponse(self, status, body):
163 return body.hex()
164
165 def getResponseResult(self, status, body):
166 return body.hex()
167
168 def verify(self, status, body):
169 return "verify(%s)" % body.hex()
170
171 def verifyResult(self, status, body):
172 return "verify(%s)" % body.hex()
173
174
175'''
176'''
177class ApplicationGpc(object):
178 """GlobalPlatform application"""
179
180 def __init__(self, aid):
181 self.aid = aid
182 self.lastGetData = None
183
184 def getAID(self):
185 return self.aid
186
187 def selectResult(self, fci, status, body):
188 return 'selectResult(fci=%s, status=0x%x) = %s\n' %(fci, status, body.hex())
189
190 def getData(self, fileId, bytes):
191 tags = {
192 0x42: 'Issuer Identification Number',
193 0x45: 'Card Image Number',
194 0x66: 'Card Data',
195 0x67: 'Card Capability Information' # ???
196 }
197 self.lastGetData = fileId
198 return 'getData(%s)\n' % tags.get(fileId, '<unknown 0x%x>' % fileId)
199
200 def getDataResult(self, status, body):
201 if self.lastGetData == 0x66:
202 # Card Data
203 pass
204 return 'getDataResult(0x%x) = %s\n' %(status, body.hex())
205
206 def mse(self, body):
207 return body.hex()
208
209 def mseResult(self, status, body):
210 return body.hex()
211
212 def pso(self, body):
213 return body.hex()
214
215 def psoResult(self, status, body):
216 return body.hex()
217
218 def getResponse(self, status, body):
219 return body.hex()
220
221 def getResponseResult(self, status, body):
222 return body.hex()
223
224
225class ApplicationPIV(object):
226 """PIV application"""
227
228 def __init__(self, aid):
229 self.lastGet = None
230 self.aid = aid
231
232 def getAID(self):
233 return self.aid
234
235 def selectResult(self, selectT, status, body):
236 ret = ''
237 appTag = body[0]
238 appLen = body[1]
239
240 body = body[2:2+appLen]
241 while len(body) > 2:
242 tag = body[0]
243 tagLen = body[1]
244 if tagLen != 1:
245 pass
246 if selectT == "FCI":
247 if tag == 0x4f:
248 ret += "\tpiv version: %s\n" % body[2:2 + tagLen].hex()
249 elif tag == 0x79:
250 subBody = body[2:2 + tagLen]
251
252 subTag = subBody[0]
253 subLen = subBody[1]
254
255 content = subBody.hex()
256 if subTag == 0x4f:
257 v = content[4:]
258 if v.startswith('a000000308'):
259 content = 'NIST RID'
260 ret += '\tCoexistent tag allocation authority: %s\n' % content
261
262 elif tag == 0x50:
263 ret += '\tapplication label: %s\n' % body[2:2+tagLen].decode('utf8')
264 elif tag == 0xac:
265 ret += '\tCryptographic algorithms supported: %s\n' % body[2:2+tagLen].hex()
266 else:
267 rety += '\tunknown tag 0x%x\n' % tag
268
269 else:
270 ret += "\tTODO: selectType %s\n" % selectT
271
272 body = body[2+tagLen:]
273
274 return ret
275
276 def getData(self, fileId, bytes):
277 ret = "\tfileId=%s(%0.4x)\n" % (FIDs.get(fileId, ""), fileId)
278
279 if len(bytes) < 7:
280 return ret + "\t/!\\ too short !!!!"
281
282 lc = bytes[4]
283 tag = bytes[5]
284 tagLen = bytes[6]
285
286 if lc == 4:
287 ret += "\tdoId=%0.4x\n"% (bytes[7] * 256 + bytes[8])
288
289 elif lc == 0xa:
290 keyStr = ''
291 # TLV
292 i = 7
293 tag = bytes[i]
294 tagLen = bytes[i+1]
295 keyRef = bytes[i+3]
296 keyStr = "key(tag=0x%x len=%d ref=0x%x)=" % (tag, tagLen, keyRef)
297 i = i + 2 + tagLen
298
299 tag = bytes[i]
300 tagLen = bytes[i+1]
301 keyStr += "value(tag=0x%x len=%d)"
302 elif lc == 3:
303 ret += "\tDiscovery Object\n"
304 elif lc == 4:
305 ret += "\tBiometric Information Templates (BIT) Group Template\n"
306 elif lc == 5:
307 if tag == 0x5C:
308 tagStr = bytes[7:10].hex()
309 ret += '\ttag: %s(%s)\n' % (tagStr, PIV_OIDs.get(tagStr, '<unknown>'))
310 self.lastGet = tagStr
311 else:
312 ret += "\tunknown key access(lc=0x%x)\n" % lc
313
314 return ret
315
316 def getDataResult(self, status, body):
317 ret = ''
318 if not len(body):
319 return ''
320 appTag = body[0]
321 appLen = body[1]
322
323 body = body[2:2+appLen]
324 while len(body) > 2:
325 tag = body[0]
326 tagLen = body[1]
327 tagBody = body[2:2+tagLen]
328
329 if self.lastGet in ('5fc102',):
330 # Card holder Unique Identifier
331 if tag == 0x30:
332 ret += '\tFASC-N: %s\n' % tagBody.hex()
333 elif tag == 0x34:
334 ret += '\tGUID: %s\n' % tagBody.hex()
335 elif tag == 0x35:
336 ret += '\texpirationDate: %s\n' % tagBody.decode('utf8')
337 elif tag == 0x3e:
338 ret += '\tIssuer Asymmetric Signature: %s\n' % tagBody.hex()
339 else:
340 ret += "\tunknown tag=0x%x len=%d content=%s\n" % (tag, tagLen, tagBody.hex())
341
342 elif self.lastGet in ('5fc107',):
343 # Card Capability Container
344 capas = {
345 0xf0: "Card Identifier",
346 0xf1: "Capability Container version number",
347 0xf2: "Capability Grammar version number",
348 0xf3: "Applications CardURL",
349 0xf4: "PKCS#15",
350 0xf5: "Registered Data Model number",
351 0xf6: "Access Control Rule Table",
352 0xf7: "Card APDUs",
353 0xfa: "Redirection Tag",
354 0xfb: "Capability Tuples (CTs)",
355 0xfc: "Status Tuples (STs)",
356 0xfd: "Next CCC",
357 0xe3: "Extended Application CardURL",
358 0xb4: "Security Object Buffer",
359 0xfe: "Error Detection Code"
360 }
361
362 if tag in capas.keys():
363 if tagLen:
364 ret += "\t%s: len=%d %s\n" % (capas[tag], tagLen, tagBody.hex())
365 else:
366 ret += "\tunknown capa tag 0x%x: %s\n" % (tag, tagBody.hex())
367
368 elif self.lastGet == '5fc105':
369 # X.509 Certificate for PIV Authentication
370 pass
371
372 else:
373 ret += "\t%s: unimplemented tag=0x%x len=%d content=%s\n" % (self.lastGet, tag, tagLen, tagBody.hex())
374
375 body = body[2+tagLen:]
376
377 return ret
378
379 def getResponse(self, status, body):
380 return body.hex()
381
382 def getResponseResult(self, status, body):
383 return body.hex()
384
385 def mse(self, body):
386 return body.hex()
387
388 def mseResult(self, status, body):
389 return body.hex()
390
391 def pso(self, body):
392 return body.hex()
393
394 def psoResult(self, status, body):
395 return body.hex()
396
397 def verify(self, status, body):
398 return "verify(%s)" % body.hex()
399
400 def verifyResult(self, status, body):
401 return "verify(%s)" % body.hex()
402
403
404
405
406class ApplicationGids(object):
407 """GIDS application"""
408
409 def __init__(self, aid):
410 self.aid = aid
411 self.lastDo = None
412
413 def getAID(self):
414 return self.aid
415
416 def parseFcp(self, bytes):
417 ret = ''
418 tag = bytes[0]
419 tagLen = bytes[1]
420
421 body = bytes[2:2+tagLen]
422
423 if tag == 0x62:
424 ret += '\tFCP\n'
425
426 while len(body) > 2:
427 tag2 = body[0]
428 tag2Len = body[1]
429 tag2Body = body[2:2+tag2Len]
430
431 if tag2 == 0x82:
432 ret += '\t\tFileDescriptor: %s\n' % tag2Body.hex()
433 elif tag2 == 0x8a:
434 ret += '\t\tLifeCycleByte: %s\n' % tag2Body.hex()
435 elif tag2 == 0x84:
436 ret += '\t\tDF name: %s\n' % tag2Body.encode('utf8')
437 elif tag2 == 0x8C:
438 ret += '\t\tSecurityAttributes: %s\n' % tag2Body.hex()
439 else:
440 ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex())
441
442 body = body[2+tag2Len:]
443
444 return ret
445
446 def parseFci(self, bytes):
447 ret = ''
448 tag = bytes[0]
449 tagLen = bytes[1]
450
451 body = bytes[2:2+tagLen]
452
453 if tag == 0x61:
454 ret += '\tFCI\n'
455
456 while len(body) > 2:
457 tag2 = body[0]
458 tag2Len = body[1]
459 tag2Body = body[2:2+tag2Len]
460
461 if tag2 == 0x4F:
462 ret += '\t\tApplication AID: %s\n' % tag2Body.hex()
463
464 elif tag2 == 0x50:
465 ret += '\t\tApplication label: %s\n' % tag2Body.encode('utf8')
466
467 elif tag2 == 0x73:
468 body2 = tag2Body
469 tokens = []
470 while len(body2) > 2:
471 tag3 = body2[0]
472 tag3Len = body2[1]
473
474 if tag3 == 0x40:
475 v = body2[2]
476 if v & 0x80:
477 tokens.append('mutualAuthSymAlgo')
478 if v & 0x40:
479 tokens.append('extAuthSymAlgo')
480 if v & 0x20:
481 tokens.append('keyEstabIntAuthECC')
482
483
484 body2 = body2[2+tag3Len:]
485
486 ret += '\t\tDiscretionary data objects: %s\n' % ",".join(tokens)
487 else:
488 ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex())
489
490 body = body[2+tag2Len:]
491
492 return ret
493
494
495 def selectResult(self, selectT, status, body):
496 if not len(body):
497 return ''
498
499 if selectT == 'FCP':
500 return self.parseFcp(body)
501 elif selectT == 'FCI':
502 return self.parseFci(body)
503 else:
504 return '\tselectResult(fci=%s, status=0x%x) = %s\n' % (selectT, status, body.hex())
505
506 def getData(self, fileId, bytes):
507 lc = bytes[4]
508 tag = bytes[5]
509 tagLen = bytes[6]
510
511 if tag == 0x5c:
512 doStr = bytes[7:7+tagLen].hex()
513 ret = '\tDO=%s\n' % DOs.get(doStr, "<%s>" % doStr)
514 self.lastDo = doStr
515 else:
516 ret = '\tunknown tag=0%x len=%d v=%s' % (tag, tagLen, bytes[7:7+tagLen].hex())
517
518 return ret
519
520 def getDataResult(self, status, body):
521 ret = ''
522 '''
523 while len(body) > 2:
524 tag = body[0]
525 tagLen = body[1]
526
527 ret += '\ttag=0x%x len=%d content=%s\n' % (tag, tagLen, body[2:2+tagLen].hex())
528
529 body = body[2+tagLen:]
530 '''
531 return ret
532
533 def mse(self, body):
534 return body.hex()
535
536 def mseResult(self, status, body):
537 return body.hex()
538
539 def getResponse(self, status, body):
540 return body.hex()
541
542 def getResponseResult(self, status, body):
543 return body.hex()
544
545 def pso(self, body):
546 return body.hex()
547
548 def psoResult(self, status, body):
549 return body.hex()
550
551
552
553def createAppByAid(aid):
554 if aid in ("a000000308", 'a00000030800001000',):
555 return ApplicationPIV(aid)
556
557 elif aid in ('a00000039742544659',):
558 return ApplicationGids(aid)
559
560 elif aid in ('a0000001510000',):
561 return ApplicationGpc(aid)
562
563 return ApplicationDummy(aid)
564
565
566def getErrorCode(status):
567 if status & 0x6100:
568 return "%d more bytes" % (status & 0xff)
569
570 return ERROR_CODES.get(status, "<unknown>")
571
572if __name__ == '__main__':
573 if len(sys.argv) > 1:
574 fin = open(sys.argv[1], "r")
575 else:
576 fin = sys.stdin
577
578 lineno = 0
579 lastCmd = 0
580 lastSelect = None
581 lastSelectFCI = False
582 lastGetItem = None
583 currentApp = None
584
585 for l in fin.readlines():
586 lineno += 1
587
588 if not len(l):
589 continue
590
591 # smartcard loggers have changed
592 #if l.find("[DEBUG][com.freerdp.channels.smartcard.client]") == -1:
593 # continue
594
595 body = ''
596 recvKey = 'pbRecvBuffer: { '
597
598 pos = l.find(recvKey)
599 if pos != -1:
600 toCard = False
601
602 pos += len(recvKey)
603 pos2 = l.find(' }', pos)
604 if pos2 == -1:
605 print("line %d: invalid recvBuffer")
606 continue
607
608 else:
609 toCard = True
610 sendKey = 'pbSendBuffer: { '
611 pos = l.find(sendKey)
612 if pos == -1:
613 continue
614 pos += len(sendKey)
615
616 pos2 = l.find(' }', pos)
617 if pos2 == -1:
618 print("line %d: invalid sendBuffer")
619 continue
620
621 body = l[pos:pos2]
622
623 print(l[0:-1])
624 bytes = codecs.decode(body, 'hex')
625 if toCard:
626 (cla, ins, p1, p2) = bytes[0:4]
627 cmdName = CMD_NAMES.get(ins, "<COMMAND 0x%x>" % ins)
628 print(cmdName + ":")
629
630 if cmdName == "SELECT":
631 lc = bytes[4]
632 i = 5
633
634 if p1 == 0x00:
635 print("\tselectByFID: %0.2x%0.2x" % (bytes[i], bytes[i+1]))
636 i = i + lc
637
638 elif p1 == 0x4:
639 aid = bytes[i:i+lc].hex()
640 lastSelect = AIDs.get(aid, '<unknown %s>' % aid)
641 print("\tselectByAID: %s(%s)" % (aid, lastSelect))
642
643 if p2 == 0x00:
644 lastSelectT = "FCI"
645 print('\tFCI')
646 elif p2 == 0x04:
647 print('\tFCP')
648 lastSelectT = "FCP"
649 elif p2 == 0x08:
650 print('\tFMD')
651 lastSelectT = "FMD"
652
653 if not currentApp or currentApp.getAID() != aid:
654 currentApp = createAppByAid(aid)
655
656
657 elif cmdName == "VERIFY":
658 lc = bytes[4]
659 P2_DATA_QUALIFIER = {
660 0x00: "Card global password",
661 0x01: "RFU",
662 0x80: "Application password",
663 0x81: "Application resetting password",
664 0x82: "Application security status resetting code",
665 }
666
667 pin=''
668 if lc:
669 pin = ", pin='" + bytes[5:5+lc-2].decode('utf8)') + "'"
670
671 print("\t%s%s" % (P2_DATA_QUALIFIER.get(p2, "<unknown>"), pin))
672
673 elif cmdName == "GET DATA":
674 lc = bytes[4]
675 fileId = p1 * 256 + p2
676
677 ret = currentApp.getData(fileId, bytes)
678 print("%s" % ret)
679
680 elif cmdName == "GET RESPONSE":
681 #lc = bytes[4]
682 #fileId = p1 * 256 + p2
683
684 #ret = currentApp.getResponse(fileId, bytes)
685 #print("%s" % ret)
686 pass
687 elif cmdName == "MSE":
688 ret = currentApp.mse(bytes[5:5+lc])
689 print("%s" % ret)
690
691 elif cmdName == "PSO":
692 ret = currentApp.pso(bytes[5:5+lc])
693 print("%s" % ret)
694 else:
695 print('handle %s' % cmdName)
696
697 lastCmd = cmdName
698
699 else:
700 # Responses
701 if not len(bytes):
702 continue
703
704 status = bytes[-1] + bytes[-2] * 256
705 body = bytes[0:-2]
706 print("status=0x%0.4x(%s)" % (status, getErrorCode(status)))
707
708 if not len(body):
709 continue
710
711 ret = ''
712 if lastCmd == "SELECT":
713 ret = currentApp.selectResult(lastSelectT, status, body)
714 elif lastCmd == "GET DATA":
715 ret = currentApp.getDataResult(status, body)
716 elif lastCmd == "MSE":
717 ret = currentApp.mseResult(status, body)
718 elif lastCmd == "PSO":
719 ret = currentApp.psoResult(status, body)
720 elif lastCmd == "GET RESPONSE":
721 ret = currentApp.getResponseResult(status, body)
722 elif lastCmd == "VERIFY":
723 ret = currentApp.verifyResult(status, body)
724
725 if ret:
726 print("%s" % ret)