├── .gitignore ├── LICENSE ├── README.md ├── certs ├── AK_AUT.der ├── NK_VPN.der └── SAK_AUT.der ├── patch_card ├── __init__.py ├── cards.py └── vpc.py ├── save_400m_euro.py └── virtualsmartcard ├── CardGenerator.py ├── ConstantDefinitions.py ├── CryptoUtils.py ├── SEutils.py ├── SWutils.py ├── SmartcardFilesystem.py ├── SmartcardSAM.py ├── TLVutils.py ├── VirtualSmartcard.py ├── __init__.py ├── cards ├── Relay.py ├── RelayMiddleman.py ├── __init__.py ├── cryptoflex.py ├── ePass.py └── nPA.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp 7 | * ---------------------------------------------------------------------------- 8 | */ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TI Konnektor Patch 2 | 3 | Jeder [Konnektor](https://fachportal.gematik.de/hersteller-anbieter/komponenten-dienste/konnektor), der zum Zugriff auf die **Telematik Infrastruktur** (TI) der *gematik* benötigt wird, enthält mehrere Smartcards, auf denen Zertifikate gespeichert werden. 4 | Aus Sicherheitsgründen haben diese Zertifikate ein Verfallsdatum. Daher müssen sie regelmäßig erneut werden. 5 | Nicht alle Hersteller*innen haben einen Mechanismus zur Verlängerung der Zertifikate implementert. Nach Auslaufen der Zertifikate muss also der gesamte Konnektor getauscht werden. 6 | Derzeit laufen die ersten Zertifikate aus. 7 | 8 | Verantwortlich für die TI ist die gematik. Deren Gesellschafter [haben entschieden](https://www.gematik.de/newsroom/news-detail/aktuelles-erste-konnektoren-laufen-im-september-aus), diese Konnektoren auszutauschen. Das erzeugt Kosten im Bereich vom **300 bis 400 Millionen Euro**. 9 | 10 | Wir zeigen hier eine **kostenlose** Softwarelösung für das Problem, von der die Hersteller*innen behaupten, dass sie unmöglich sei. 11 | 12 | ## Funktionsweise 13 | 14 | Unser Patch klinkt sich in die Kommunkation zwischen der Software auf dem Konnektor und der Smartcard ein. Das geht, weil die Kommunikation zur Smartcard nicht abgesichert ist. Unter anderem deshalb war auch der [Angriff auf den "sicheren" Speicher](https://twitter.com/fluepke/status/1576584063896256513) im *Secunet* Adapter möglich. 15 | 16 | > **Wichtig**: Unser Patch stellt keinen Angriff auf die Integrität des Konnektors dar. Zwar klinkt sich der Patch in die Kommunikation zur Smartcard ein, es werden aber nur öffentliche, also nicht geheime Daten verändert. Die privaten Schlüssel verbleiben unverändert auf der besonders abgesicherten Smartcard. 17 | 18 | Sowohl bei dem Produkt der *Secunet* als auch bei der *CompuGroup Medical* kommt die Software *PC/SC Smart Card Daemon* ([`pcscd`](https://github.com/LudovicRousseau/PCSC)) zur Kommunikation mit den SmartCards zum Einsatz. Diese Software öffnet einen [Unix Domain Socket](https://de.wikipedia.org/wiki/Unix_Domain_Socket) (z. B. in `/var/run/pcscd.comm`), über den Anwendungen mit dem `pcscd` interagieren, um der Smartcard Befehle zu senden und Antworten von dieser zu empfangen. 19 | 20 | Unsere Referenzimplementierung für die Verlängerung der Zertifikatslaufzeiten besteht darin, an der Stelle des ursprünglichen `pcscd` eine modifizierte Version zu starten, die alle Befehle an die SmartCard wie gewohnt weiterleitet. Wird jedoch der Befehl zum Auslesen eines der drei vom Auslaufen betroffenen Zertifikate gesendet, antwortet unser `pcscd` mit einem verlängerten Zertifikat aus dem Dateisystem. 21 | 22 | Dadurch sind keine Veränderungen an der bestehenden Software der Hersteller notwendig. Es muss lediglich eine Teilkomponente des Linux-Systems, auf dem die Hersteller ihre TI Konnektoren basiert haben, umkonfiguriert bzw. ausgetauscht werden. 23 | 24 | ### Detaillierte Beschreibung 25 | 26 | Wir verwenden einen handelsüblichen *PC/SC Smart Card Daemon* ([`pcscd`](https://github.com/LudovicRousseau/PCSC)) und simulieren diesem SmartCards mithilfe des Projekts [Virtual Smart Card](https://frankmorgner.github.io/vsmartcard/virtualsmartcard/README.html). 27 | 28 | 1. Auf dem Konnektor bleibt der bestehende `pcscd` des Herstellers im Hintergrund aktiv, sein Socket wird aber an einen neuen Ort im Dateisystem verschoben: `mv /var/run/pcscd.comm /var/run/old_pcscd.comm` 29 | 2. Dann wird ein handelsüblicher `pcscd` sowie `virtualsmartcard` installiert und gestartet. Wichtig ist, dass der `pcscd` so konfiguriert wird, lediglich die Treiber für `virtualsmartcard` zu laden. 30 | 3. In dem Verzeichnis `./certs` sind die verlängerten Zertifikate mit folgenden Dateinamen zu hinterlegen: 31 | 1. `AK_AUT.der` 32 | 2. `NK_VPN.der` 33 | 3. `SAK_AUT.der` 34 | 4. Zuletzt muss das Python-Skript `./save_400m_euro.py` aus diesem Projekt gestartet werden. Dieses verfolgt in der `execute()` Methode alle APDUs und prüft, ob ein Zugriff auf die Zertifikatsdateien erfolgt. 35 | 36 | ## Installation 37 | 38 | Die Hersteller müssen dieses Skript in eine Update-Datei verpacken, diese signieren und bereitstellen, denn schließlich darf nur Software vom Hersteller auf dem Konnektor ausgeführt werden. 39 | 40 | Für die Verlängerung der Zertifikatslaufzeiten braucht es die *gematik*, denn sie verantwortet und betreibt die dazu notwendige Certificate Authority (CA). 41 | 42 | ## Forderungen 43 | 44 | - Wir fordern die **gematik** auf, ihre CA für die Verlängerung der Laufzeiten einzusetzen. 45 | - Wir fordern **alle Hersteller** ([CompuGroup Medical](https://www.cgm.com/deu_de), [secunet](https://www.secunet.com/), [RISE](https://www.rise-konnektor.de/)) auf, die Laufzeitverlängerung umzusetzen, statt das Gesundheitssystem durch die aufgerufenen astronomischen Preise auszubeuten. 46 | - Wir fordern das **Bundesgesundheitsministerium** auf, die Hersteller endlich an die Leine zu nehmen und der Geldverbrennung in der TI ein Ende zu setzen. 47 | - Wir fordern das **Umweltministerium** auf, die allein schon aus Nachhaltigkeitsgesichtspunkten völlig sinnlose tausendfache Vernichtung einsatzfähiger Hardware zu verhindern. 48 | -------------------------------------------------------------------------------- /certs/AK_AUT.der: -------------------------------------------------------------------------------- 1 | AK_AUT_CERT_INSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTC 2 | -------------------------------------------------------------------------------- /certs/NK_VPN.der: -------------------------------------------------------------------------------- 1 | NK_VPN_CERT_INSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTC 2 | -------------------------------------------------------------------------------- /certs/SAK_AUT.der: -------------------------------------------------------------------------------- 1 | SAK_AUT_CERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINSERTCERTHEREINS 2 | -------------------------------------------------------------------------------- /patch_card/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fluepke/konnektor-patch/1e6a5bec11a09875366d6bbaf9bef38c942b9807/patch_card/__init__.py -------------------------------------------------------------------------------- /patch_card/cards.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | 4 | from virtualsmartcard.SWutils import SW 5 | from virtualsmartcard.ConstantDefinitions import FDB 6 | from virtualsmartcard.cards.Relay import RelayOS 7 | from virtualsmartcard.CardGenerator import CardGenerator 8 | from virtualsmartcard.utils import C_APDU, R_APDU, hexdump, inttostring 9 | from virtualsmartcard.VirtualSmartcard import Iso7816OS 10 | from virtualsmartcard.SmartcardFilesystem import MF, DF, TransparentStructureEF 11 | 12 | CMD_SELECT_FILE = 0xA4 13 | 14 | # Directory Files 15 | DF_AK = bytes([0xD2, 0x76, 0x00, 0x01, 0x44, 0x02]) 16 | DF_NK = bytes([0xD2, 0x76, 0x00, 0x01, 0x44, 0x03]) 17 | DF_SAK = bytes([0xD2, 0x76, 0x00, 0x01, 0x44, 0x04]) 18 | 19 | # Elementary Files 20 | EF_C_AK_AUT_R2048 = 0xC503 # bytes([0xC5, 0x03]) 21 | EF_C_NK_VPN_R2048 = 0xC505 # bytes([0xC5, 0x05]) 22 | EF_C_SAK_AUT_R2048 = 0xC506 # bytes([0xC5, 0x06]) 23 | 24 | 25 | def is_seekable(ins): 26 | return ins in [ 27 | 0xb0, 0xb1, 0xd0, 0xd1, 0xd6, 0xd7, 0xa0, 0xa1, 0xb2, 0xb3, 28 | 0xdc, 0xdd, 29 | ] 30 | 31 | def read_file(filename): 32 | with open(filename, mode='rb') as f: 33 | return f.read() 34 | 35 | 36 | def create_filesystem(): 37 | """ 38 | Create the card fileystem with the certificate files 39 | """ 40 | mf = MF(filedescriptor=FDB["DF"]) 41 | 42 | # AK_AUT 43 | df = DF(parent=mf, fid=0x01, dfname=DF_AK) 44 | ef = TransparentStructureEF( 45 | parent=df, fid=EF_C_AK_AUT_R2048, shortfid=0x03, 46 | data=read_file("certs/AK_AUT.der")) 47 | df.append(ef) 48 | mf.append(df) 49 | 50 | # NK_VPN 51 | df = DF(parent=mf, fid=0xAA00, dfname=DF_NK) 52 | ef = TransparentStructureEF( 53 | parent=df, fid=EF_C_NK_VPN_R2048, shortfid=0x05, 54 | data=read_file("certs/NK_VPN.der")) 55 | df.append(ef) 56 | mf.append(df) 57 | 58 | # DF_SAK 59 | df = DF(parent=mf, fid=0x02, dfname=DF_SAK) 60 | ef = TransparentStructureEF( 61 | parent=df, fid=EF_C_SAK_AUT_R2048, shortfid=0x06, 62 | data=read_file("certs/SAK_AUT.der")) 63 | df.append(ef) 64 | mf.append(df) 65 | 66 | return mf 67 | 68 | 69 | class PatchCard(RelayOS): 70 | """ 71 | We are using the RelayOS virtual smartcard and patch some pdus 72 | """ 73 | def __init__(self, *args, **kwargs): 74 | """Initialize interceptor card""" 75 | self.intercept_file = False 76 | self.intercept_mf = create_filesystem() 77 | self.intercept_handlers = { 78 | 0x0c: self.intercept_mf.eraseRecord, 79 | 0x0e: self.intercept_mf.eraseBinaryPlain, 80 | 0x0f: self.intercept_mf.eraseBinaryEncapsulated, 81 | 0xa0: self.intercept_mf.searchBinaryPlain, 82 | 0xa1: self.intercept_mf.searchBinaryEncapsulated, 83 | 0xa4: self.intercept_mf.selectFile, 84 | 0xb0: self.intercept_mf.readBinaryPlain, 85 | 0xb1: self.intercept_mf.readBinaryEncapsulated, 86 | 0xb2: self.intercept_mf.readRecordPlain, 87 | 0xb3: self.intercept_mf.readRecordEncapsulated, 88 | 0xca: self.intercept_mf.getDataPlain, 89 | 0xcb: self.intercept_mf.getDataEncapsulated, 90 | 0xd0: self.intercept_mf.writeBinaryPlain, 91 | 0xd1: self.intercept_mf.writeBinaryEncapsulated, 92 | 0xd2: self.intercept_mf.writeRecord, 93 | 0xd6: self.intercept_mf.updateBinaryPlain, 94 | 0xd7: self.intercept_mf.updateBinaryEncapsulated, 95 | 0xda: self.intercept_mf.putDataPlain, 96 | 0xdb: self.intercept_mf.putDataEncapsulated, 97 | 0xdc: self.intercept_mf.updateRecordPlain, 98 | 0xdd: self.intercept_mf.updateRecordEncapsulated, 99 | 0xe0: self.intercept_mf.createFile, 100 | 0xe2: self.intercept_mf.appendRecord, 101 | 0xe4: self.intercept_mf.deleteFile, 102 | } 103 | 104 | self.last_command_offcut = b"" 105 | self.last_command_sw = SW["NORMAL"] 106 | 107 | super().__init__(*args, **kwargs) 108 | 109 | def format_result(self, seekable, le, data, sw): 110 | """See Iso7816O implementation""" 111 | if not seekable: 112 | self.last_command_offcut = data[le:] 113 | l = len(self.last_command_offcut) 114 | if l == 0: 115 | self.last_command_sw = SW["NORMAL"] 116 | else: 117 | self.last_command_sw = sw 118 | sw = SW["NORMAL_REST"] + min(0xff, l) 119 | else: 120 | if le > len(data): 121 | sw = SW["WARN_EOFBEFORENEREAD"] 122 | 123 | if le is not None: 124 | result = data[:le] 125 | else: 126 | result = data[:0] 127 | 128 | return R_APDU(result, inttostring(sw)).render() 129 | 130 | def execute(self, msg): 131 | """Handle messages and intercept filesystem handler""" 132 | # Parse PDU 133 | try: 134 | c = C_APDU(msg) 135 | logging.debug("%s", str(c)) 136 | except ValueError as e: 137 | logging.warning(str(e)) 138 | # Pass to card and return 139 | return super().execute(msg) 140 | 141 | # Run on SC 142 | reply = super().execute(msg) 143 | 144 | # Run on virtual FS 145 | handler = self.intercept_handlers.get(c.ins) 146 | if handler and c.ins == CMD_SELECT_FILE: 147 | try: 148 | # File not found will raise an error 149 | self.intercept_mf.selectFile(c.p1, c.p2, c.data) 150 | self.intercept_file = True 151 | except: 152 | self.intercept_file = False 153 | 154 | elif handler and self.intercept_file: 155 | sw, result = handler(c.p1, c.p2, c.data) 156 | return self.format_result( 157 | is_seekable(c.ins), 158 | c.effective_Le, 159 | result, 160 | sw) 161 | 162 | return reply 163 | -------------------------------------------------------------------------------- /patch_card/vpc.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is based upon virtualsmartcard/VirtualSmartcard.py 3 | and is a wrapper for the communications protocoll in use by 4 | the daemon. 5 | """ 6 | 7 | import socket 8 | import errno 9 | import struct 10 | import logging 11 | 12 | _Csizeof_short = len(struct.pack('h', 0)) 13 | 14 | def vpc_connect(host, port): 15 | """Connect to vpcd""" 16 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | sock.connect((host, port)) 18 | return sock 19 | 20 | 21 | def vpc_send(sock, msg): 22 | """ Send a message to the vpcd """ 23 | if isinstance(msg, str): 24 | sock.sendall(struct.pack('!H', len(msg)) + bytes(map(ord,msg))) 25 | else: 26 | sock.sendall(struct.pack('!H', len(msg)) + msg) 27 | 28 | 29 | def vpc_recv(sock): 30 | """ Receive a message from the vpcd """ 31 | # receive message size 32 | while True: 33 | try: 34 | sizestr = sock.recv(_Csizeof_short) 35 | except socket.error as err: 36 | if err.errno == errno.EINTR: 37 | continue 38 | break 39 | 40 | if len(sizestr) == 0: 41 | logging.error("Virtual PCD shut down") 42 | raise socket.error 43 | 44 | size = struct.unpack('!H', sizestr)[0] 45 | 46 | # receive and return message 47 | if size: 48 | while True: 49 | try: 50 | msg = sock.recv(size) 51 | except socket.error as err: 52 | if err.errno == errno.EINTR: 53 | continue 54 | break 55 | if len(msg) == 0: 56 | logging.error("Virtual PCD shut down") 57 | raise socket.error 58 | else: 59 | msg = None 60 | 61 | return size, msg 62 | -------------------------------------------------------------------------------- /save_400m_euro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os.path import dirname, realpath 4 | import os 5 | import sys 6 | import socket 7 | import errno 8 | import struct 9 | import logging 10 | from argparse import ArgumentParser 11 | 12 | sys.path.append(dirname(realpath(__file__))) 13 | 14 | from virtualsmartcard.utils import inttostring, hexdump 15 | from patch_card.vpc import ( 16 | vpc_connect, 17 | vpc_send, 18 | vpc_recv, 19 | ) 20 | 21 | def parse_args(): 22 | """Parse commandline arguments""" 23 | parser = ArgumentParser() 24 | parser.add_argument( 25 | "-H", "--host", default="localhost") 26 | parser.add_argument( 27 | "-p", "--port", type=int, default=35963) 28 | parser.add_argument( 29 | "-s", "--pcsc-sock-name", 30 | default="/var/run/old_pcscd.comm") 31 | parser.add_argument( 32 | "-r", "--reader-num", type=int, default=0) 33 | return parser.parse_args() 34 | 35 | 36 | # From VirtualSmartcard.py 37 | VPCD_CTRL_LEN = 1 38 | VPCD_CTRL_OFF = 0 39 | VPCD_CTRL_ON = 1 40 | VPCD_CTRL_RESET = 2 41 | VPCD_CTRL_ATR = 4 42 | 43 | 44 | def main(args): 45 | """ 46 | Virtual Smartcard with Filesystem Interception 47 | """ 48 | # Setup logging 49 | logging.basicConfig( 50 | level=logging.DEBUG, 51 | format="%(asctime)s [%(levelname)s] %(message)s", 52 | datefmt="%d.%m.%Y %H:%M:%S") 53 | 54 | # Patch environment 55 | os.environ["PCSCLITE_CSOCK_NAME"] = args.pcsc_sock_name 56 | 57 | # Connect to virtual card server 58 | sock = vpc_connect(args.host, args.port) 59 | 60 | # Import here, so the environment is patched before 61 | # loading the python `smartcard` package 62 | from patch_card.cards import PatchCard 63 | card_os = PatchCard(args.reader_num) 64 | 65 | while True: 66 | try: 67 | (size, msg) = vpc_recv(sock) 68 | except socket.error as err: 69 | logging.info(err) 70 | sys.exit() 71 | 72 | if not size: 73 | logging.error( 74 | "error in communication protocol (missing size parameter)") 75 | elif size == VPCD_CTRL_LEN: 76 | if msg == inttostring(VPCD_CTRL_OFF): 77 | logging.info("power down") 78 | card_os.powerDown() 79 | elif msg == inttostring(VPCD_CTRL_ON): 80 | logging.info("power up") 81 | card_os.powerUp() 82 | elif msg == inttostring(VPCD_CTRL_RESET): 83 | logging.info("reset") 84 | card_os.reset() 85 | elif msg == inttostring(VPCD_CTRL_ATR): 86 | vpc_send(sock, card_os.getATR()) 87 | else: 88 | logging.info("unknown control command") 89 | else: 90 | if size != len(msg): 91 | logging.error( 92 | "expected %u bytes, but received only %u", 93 | size, len(msg)) 94 | 95 | answer = card_os.execute(msg) 96 | logging.info("response APDU (%d bytes):\n %s\n", 97 | len(answer), hexdump(answer, indent=2)) 98 | vpc_send(sock, answer) 99 | 100 | 101 | if __name__ == "__main__": 102 | args = parse_args() 103 | main(args) 104 | -------------------------------------------------------------------------------- /virtualsmartcard/ConstantDefinitions.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Frank Morgner, Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | 19 | MAX_SHORT_LE = 0xff+1 20 | MAX_EXTENDED_LE = 0xffff+1 21 | 22 | # Life cycle status byte 23 | LCB = {} 24 | LCB["NOINFORMATION"] = 0x00 25 | LCB["CREATION"] = 0x01 26 | LCB["INITIALISATION"] = 0x03 27 | LCB["ACTIVATED"] = 0x05 28 | LCB["DEACTIVATED"] = 0x04 29 | LCB["TERMINATION"] = 0x0C 30 | 31 | # Channel security attribute 32 | CS = {} 33 | CS["NOTSHAREABLE"] = 0x01 34 | CS["SECURED"] = 0x02 35 | CS["USERAUTHENTICATED"] = 0x03 36 | 37 | # Security attribute 38 | # Security attribute, Access mode byte for DFs/EFs 39 | SA = {} 40 | SA["AM_DF_DELETESELF"] = SA["AM_EF_DELETEFILE"] = 0x40 41 | SA["AM_DF_TERMINATEDF"] = SA["AM_EF_TERMINATEFILE"] = 0x20 42 | SA["AM_DF_ACTIVATEFILE"] = SA["AM_EF_ACTIVATEFILE"] = 0x10 43 | SA["AM_DF_DEACTIVATEFILE"] = SA["AM_EF_DEACTIVATEFILE"] = 0x08 44 | SA["AM_DF_CREATEDF"] = SA["AM_EF_WRITEBINARY"] = 0x04 45 | SA["AM_DF_CREATEEF"] = SA["AM_EF_UPDATEBINARY"] = 0x02 46 | SA["AM_DF_DELETECHILD"] = SA["AM_EF_READBINARY"] = 0x01 47 | 48 | # Security attribute in compact format, Security condition byte 49 | SA["CF_SC_NOCONDITION"] = 0x00 50 | SA["CF_SC_NEVER"] = 0xFF 51 | SA["__CF_SC_ATLEASTONE"] = 0x80 52 | SA["__CF_SC_ALLCONDITIONS"] = 0x00 53 | SA["__CF_SC_SECUREMESSAGING"] = 0x40 54 | SA["__CF_SC_EXTERNALAUTHENTICATION"] = 0x40 55 | SA["__CF_SC_USERAUTHENTICATION"] = 0x40 56 | SA["CF_SC_ONE_SECUREMESSAGING"] = SA["__CF_SC_ATLEASTONE"] | \ 57 | SA["__CF_SC_SECUREMESSAGING"] 58 | SA["CF_SC_ONE_EXTERNALAUTHENTICATION"] = SA["__CF_SC_ATLEASTONE"] | \ 59 | SA["__CF_SC_EXTERNALAUTHENTICATION"] 60 | SA["CF_SC_ONE_USERAUTHENTICATION"] = SA["__CF_SC_ATLEASTONE"] | \ 61 | SA["__CF_SC_USERAUTHENTICATION"] 62 | SA["CF_SC_ALL_SECUREMESSAGING"] = SA["__CF_SC_ALLCONDITIONS"] | \ 63 | SA["__CF_SC_SECUREMESSAGING"] 64 | SA["CF_SC_ALL_EXTERNALAUTHENTICATION"] = SA["__CF_SC_ALLCONDITIONS"] | \ 65 | SA["__CF_SC_EXTERNALAUTHENTICATION"] 66 | SA["CF_SC_ALL_USERAUTHENTICATION"] = SA["__CF_SC_ALLCONDITIONS"] | \ 67 | SA["__CF_SC_USERAUTHENTICATION"] 68 | 69 | 70 | # Data coding byte 71 | DCB = {} 72 | DCB["ONETIMEWRITE"] = 0x00 73 | DCB["PROPRIETARY"] = 0x20 # we use it for XOR 74 | DCB["WRITEOR"] = 0x40 75 | DCB["WRITEAND"] = 0x60 76 | DCB["BERTLV_FFVALID"] = 0x10 77 | DCB["BERTLV_FFINVALID"] = 0x10 78 | 79 | # File descriptor byte 80 | FDB = {} 81 | FDB["NOTSHAREABLEFILE"] = 0x00 82 | FDB["SHAREABLEFILE"] = 0x40 83 | FDB["WORKINGEF"] = 0x00 84 | FDB["INTERNALEF"] = 0x80 85 | FDB["DF"] = 0x38 86 | FDB["EFSTRUCTURE_NOINFORMATIONGIVEN"] = 0x00 87 | FDB["EFSTRUCTURE_TRANSPARENT"] = 0x01 88 | FDB["EFSTRUCTURE_LINEAR_FIXED_NOFURTHERINFO"] = 0x02 89 | FDB["EFSTRUCTURE_LINEAR_FIXED_SIMPLETLV"] = 0x03 90 | FDB["EFSTRUCTURE_LINEAR_VARIABLE_NOFURTHERINFO"] = 0x04 91 | FDB["EFSTRUCTURE_LINEAR_VARIABLESIMPLETLV"] = 0x05 92 | FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"] = 0x06 93 | FDB["EFSTRUCTURE_CYCLIC_SIMPLETLV"] = 0x07 94 | 95 | # File identifier 96 | FID = {} 97 | FID["EFDIR"] = 0x2F00 98 | FID["EFATR"] = 0x2F00 99 | FID["MF"] = 0x3F00 100 | FID["PATHSELECTION"] = 0x3FFF 101 | FID["RESERVED"] = 0x3FFF 102 | 103 | 104 | # Secure Messaging constants 105 | SM_Class = {} 106 | 107 | # Basic secure messaging objects 108 | # '80', '81' Plain value not encoded in BER-TLV 109 | SM_Class["PLAIN_VALUE_NO_TLV"] = 0x80 110 | SM_Class["PLAIN_VALUE_NO_TLV_ODD"] = 0x81 111 | # '82', '83' Cryptogram (plain value encoded in BER-TLV and including SM data 112 | # objects, i.e. a SM field) 113 | SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM"] = 0x82 114 | SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM_ODD"] = 0x83 115 | # '84', '85' Cryptogram (plain value encoded in BER-TLV, but not including SM 116 | # data objects) 117 | SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM"] = 0x84 118 | SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM_ODD"] = 0x85 119 | # '86', '87' Padding-content indicator byte followed by cryptogram (plain 120 | # value not encoded in BER-TLV) 121 | SM_Class["CRYPTOGRAM_PADDING_INDICATOR"] = 0x86 122 | SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"] = 0x87 123 | # '89' Command header (CLA INS P1 P2, four bytes) 124 | SM_Class["PLAIN_COMMAND_HEADER"] = 0x89 125 | # '8E' Cryptographic checksum (at least four bytes) 126 | SM_Class["CHECKSUM"] = 0x8E 127 | # '90', '91' Hash-code 128 | SM_Class["HASH_CODE"] = 0x90 129 | SM_Class["HASH_CODE_ODD"] = 0x91 130 | # '92', '93' Certificate (data not encoded in BER-TLV) 131 | SM_Class["CERTIFICATE"] = 0x92 132 | SM_Class["CERTIFICATE_ODD"] = 0x93 133 | # '94', '95' Security environment identifier (SEID byte) 134 | SM_Class["SECURITY_ENIVRONMENT_ID"] = 0x94 135 | SM_Class["SECURITY_ENIVRONMENT_ID_ODD"] = 0x95 136 | # '96', '97' One or two bytes encoding Ne in the unsecured command-response 137 | # pair (possibly empty) 138 | SM_Class["Ne"] = 0x96 139 | SM_Class["Ne_ODD"] = 0x97 140 | # '99' Processing status (SW1-SW2, two bytes; possibly empty) 141 | SM_Class["PLAIN_PROCESSING_STATUS"] = 0x99 142 | # '9A', '9B' Input data element for the computation of a digital signature 143 | # (the value field is signed) 144 | SM_Class["INPUT_DATA_SIGNATURE_COMPUTATION"] = 0x9A 145 | SM_Class["INPUT_DATA_SIGNATURE_COMPUTATION_ODD"] = 0x9B 146 | # '9C', '9D' Public key 147 | SM_Class["PUBLIC_KEY"] = 0x9C 148 | SM_Class["PUBLIC_KEY_ODD"] = 0x9D 149 | # '9E' Digital signature 150 | SM_Class["DIGITAL_SIGNATURE"] = 0x9E 151 | 152 | # Auxiliary secure messaging objects 153 | # Input template for the computation of a hash-code (the template is hashed) 154 | SM_Class["HASH_COMPUTATION_TEMPLATE"] = 0xA0 155 | SM_Class["HASH_COMPUTATION_TEMPLATE_ODD"] = 0xA1 156 | # Input template for the verification of a cryptographic checksum (the 157 | # template is included) 158 | SM_Class["CHECKSUM_VERIFICATION_TEMPLATE"] = 0xA2 159 | # Control reference template for authentication (AT) 160 | SM_Class["AUTH_CRT"] = 0xA4 161 | SM_Class["AUTH_CRT_ODD"] = 0xA5 162 | # Control reference template for key agreement (KAT) 163 | SM_Class["KEY_AGREEMENT_CRT"] = 0xA6 164 | SM_Class["KEY_AGREEMENT_CRT_ODD"] = 0xA7 165 | # Input template for the verification of a digital signature (the template is 166 | # signed) 167 | SM_Class[0xA8] = "SIGNATURE_VERIFICATION_TEMPLATE" 168 | # Control reference template for hash-code (HT) 169 | SM_Class["HASH_CRT"] = 0xAA 170 | SM_Class["HASH_CRT_ODD"] = 0xAB 171 | # Input template for the computation of a digital signature (the concatenated 172 | # value fields are signed) 173 | SM_Class["SIGNATURE_COMPUTATION_TEMPLATE"] = 0xAC 174 | SM_Class["SIGNATURE_COMPUTATION_TEMPLATE_ODD"] = 0xAD 175 | # Input template for the verification of a certificate (the concatenated value 176 | # fields are certified) 177 | SM_Class["CERTIFICATE_VERIFICATION_TEMPLATE"] = 0xAE 178 | SM_Class["CERTIFICATE_VERIFICATION_TEMPLATE_ODD"] = 0xAF 179 | # Plain value encoded in BER-TLV and including SM data objects, i.e. a SM 180 | # field 181 | SM_Class["PLAIN_VALUE_TLV_INCULDING_SM"] = 0xB0 182 | SM_Class["PLAIN_VALUE_TLV_INCULDING_SM_ODD"] = 0xB1 183 | # Plain value encoded in BER-TLV, but not including SM data objects 184 | SM_Class["PLAIN_VALUE_TLV_NO_SM"] = 0xB2 185 | SM_Class["PLAIN_VALUE_TLV_NO_SM_ODD"] = 0xB3 186 | # Control reference template for cryptographic checksum (CCT) 187 | SM_Class["CHECKSUM_CRT"] = 0xB4 188 | SM_Class["CHECKSUM_CRT_ODD"] = 0xB5 189 | # Control reference template for digital signature (DST) 190 | SM_Class["SIGNATURE_CRT"] = 0xB6 191 | SM_Class["SIGNATURE_CRT_ODD"] = 0xB7 192 | # Control reference template for confidentiality (CT) 193 | SM_Class["CONFIDENTIALITY_CRT"] = 0xB8 194 | SM_Class["CONFIDENTIALITY_CRT_ODD"] = 0xB9 195 | # Response descriptor template 196 | SM_Class["RESPONSE_DESCRIPTOR_TEMPLATE"] = 0xBA 197 | SM_Class["RESPONSE_DESCRIPTOR_TEMPLATE_ODD"] = 0xBB 198 | # Input template for the computation of a digital signature (the template is 199 | # signed) 200 | SM_Class["SIGNATURE_COMPUTATION_TEMPLATE"] = 0xBC 201 | SM_Class["SIGNATURE_COMPUTATION_TEMPLATE_ODD"] = 0xBD 202 | # Input template for the verification of a certificate (the template is 203 | # certified) 204 | SM_Class["CERTIFICATE_VERIFICATION_TEMPLATE"] = 0xBE 205 | # }}} 206 | 207 | # Tags of control reference templates (CRT) 208 | CRT_TEMPLATE = {} 209 | CRT_TEMPLATE["AT"] = 0xA4 210 | CRT_TEMPLATE["KAT"] = 0xA6 211 | CRT_TEMPLATE["HT"] = 0xAA 212 | CRT_TEMPLATE["CCT"] = 0xB4 # Template for Cryptographic Checksum 213 | CRT_TEMPLATE["DST"] = 0xB6 214 | CRT_TEMPLATE["CT"] = 0xB8 # Template for Confidentiality 215 | 216 | # This table maps algorithms to numbers. It is used in the security environment 217 | ALGO_MAPPING = {} 218 | ALGO_MAPPING[0x00] = "DES3-ECB" 219 | ALGO_MAPPING[0x01] = "DES3-CBC" 220 | ALGO_MAPPING[0x02] = "AES-ECB" 221 | ALGO_MAPPING[0x03] = "AES-CBC" 222 | ALGO_MAPPING[0x04] = "DES-ECB" 223 | ALGO_MAPPING[0x05] = "DES-CBC" 224 | ALGO_MAPPING[0x06] = "MD5" 225 | ALGO_MAPPING[0x07] = "SHA" 226 | ALGO_MAPPING[0x08] = "SHA256" 227 | ALGO_MAPPING[0x09] = "MAC" 228 | ALGO_MAPPING[0x0A] = "HMAC" 229 | ALGO_MAPPING[0x0B] = "CC" 230 | ALGO_MAPPING[0x0C] = "RSA" 231 | ALGO_MAPPING[0x0D] = "DSA" 232 | 233 | # Recerence control for select command, and record handling commands 234 | REF = { 235 | "IDENTIFIER_FIRST": 0x00, 236 | "IDENTIFIER_LAST": 0x01, 237 | "IDENTIFIER_NEXT": 0x02, 238 | "IDENTIFIER_PREVIOUS": 0x03, 239 | "IDENTIFIER_CONTROL": 0x03, 240 | "NUMBER": 0x04, 241 | "NUMBER_TO_LAST": 0x05, 242 | "NUMBER_FROM_LAST": 0x06, 243 | "NUMBER_CONTROL": 0x07, 244 | "REFERENCE_CONTROL_RECORD": 0x07, 245 | "REFERENCE_CONTROL_SELECT": 0x03, 246 | } 247 | -------------------------------------------------------------------------------- /virtualsmartcard/CryptoUtils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2014 Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | 20 | import logging 21 | import re 22 | from struct import pack 23 | from base64 import b64encode 24 | from random import randint 25 | from virtualsmartcard.utils import inttostring 26 | 27 | try: 28 | # Use PyCrypto (if available) 29 | from Crypto.Cipher import DES3, DES, AES, ARC4 # @UnusedImport 30 | from Crypto.Hash import HMAC, SHA as SHA1 31 | 32 | except ImportError: 33 | # PyCrypto not available. Use the Python standard library. 34 | import hmac as HMAC 35 | import sha as SHA1 36 | 37 | CYBERFLEX_IV = b'\x00' * 8 38 | 39 | 40 | # ******************************************************************* 41 | # * Generic methods * 42 | # ******************************************************************* 43 | def get_cipher(cipherspec, key, iv=None): 44 | cipherparts = cipherspec.split("-") 45 | 46 | if len(cipherparts) > 2: 47 | raise ValueError('cipherspec must be of the form "cipher-mode" or' 48 | '"cipher"') 49 | elif len(cipherparts) == 1: 50 | cipherparts[1] = "ecb" 51 | 52 | c_class = globals().get(cipherparts[0].upper(), None) 53 | if c_class is None: 54 | validCiphers = ",".join([e.lower() for e in dir() if e.isupper()]) 55 | raise ValueError("Cipher '%s' not known, must be one of %s" % 56 | (cipherparts[0], validCiphers)) 57 | 58 | mode = getattr(c_class, "MODE_" + cipherparts[1].upper(), None) 59 | if mode is None: 60 | validModes = ", ".join([e.split("_")[1].lower() for e in dir(c_class) 61 | if e.startswith("MODE_")]) 62 | raise ValueError("Mode '%s' not known, must be one of %s" % 63 | (cipherparts[1], validModes)) 64 | 65 | cipher = None 66 | if cipherparts[1].upper() == "ECB": 67 | cipher = c_class.new(key, mode) 68 | elif iv is None: 69 | cipher = c_class.new(key, mode, b'\x00'*get_cipher_blocklen(cipherspec)) 70 | else: 71 | cipher = c_class.new(key, mode, iv) 72 | 73 | return cipher 74 | 75 | 76 | def get_cipher_keylen(cipherspec): 77 | cipherparts = cipherspec.split("-") 78 | 79 | if len(cipherparts) > 2: 80 | raise ValueError('cipherspec must be of the form "cipher-mode" or' 81 | '"cipher"') 82 | 83 | cipher = cipherparts[0].upper() 84 | # cipher = globals().get(cipherparts[0].upper(), None) 85 | # Note: return cipher.key_size does not work on Ubuntu, because e.g. 86 | # AES.key_size == 0 87 | if cipher == "AES": # Pycrypto uses AES128 88 | return 16 89 | elif cipher == "DES": 90 | return 8 91 | elif cipher == "DES3": 92 | return 16 93 | else: 94 | raise ValueError("Unsupported Encryption Algorithm: " + cipher) 95 | 96 | 97 | def get_cipher_blocklen(cipherspec): 98 | cipherparts = cipherspec.split("-") 99 | 100 | if len(cipherparts) > 2: 101 | raise ValueError('cipherspec must be of the form "cipher-mode" or' 102 | '"cipher"') 103 | 104 | cipher = globals().get(cipherparts[0].upper(), None) 105 | return cipher.block_size 106 | 107 | 108 | def append_padding(blocklen, data, padding_class=0x01): 109 | """Append padding to the data. 110 | Length of padding depends on length of data and the block size of the 111 | specified encryption algorithm. 112 | Different types of padding may be selected via the padding_class parameter 113 | """ 114 | 115 | if padding_class == 0x01: # ISO padding 116 | last_block_length = len(data) % blocklen 117 | padding_length = blocklen - last_block_length 118 | if padding_length == 0: 119 | padding = b'\x80' + b'\x00' * (blocklen - 1) 120 | else: 121 | padding = b'\x80' + b'\x00' * (blocklen - last_block_length - 1) 122 | 123 | return data + padding 124 | 125 | 126 | def strip_padding(blocklen, data, padding_class=0x01): 127 | """ 128 | Strip the padding of decrypted data. Returns data without padding 129 | """ 130 | 131 | if padding_class == 0x01: 132 | b = data 133 | if isinstance(b, str): 134 | b = map(ord, b) 135 | tail = len(b) - 1 136 | while b[tail] != 0x80: 137 | tail = tail - 1 138 | return data[:tail] 139 | 140 | 141 | def crypto_checksum(algo, key, data, iv=None, ssc=None): 142 | """ 143 | Compute various types of cryptographic checksums. 144 | 145 | :param algo: 146 | A string specifying the algorithm to use. Currently supported 147 | algorithms are \"MAX\" \"HMAC\" and \"CC\" (Meaning a cryptographic 148 | checksum as used by the ICAO passports) 149 | :param key: They key used to computed the cryptographic checksum 150 | :param data: The data for which to calculate the checksum 151 | :param iv: 152 | Optional. An initialization vector. Only used by the \"MAC\" algorithm 153 | :param ssc: 154 | Optional. A send sequence counter to be prepended to the data. 155 | Only used by the \"CC\" algorithm 156 | 157 | """ 158 | 159 | if algo not in ("HMAC", "MAC", "CC"): 160 | raise ValueError("Unknown Algorithm %s" % algo) 161 | 162 | if algo == "MAC": 163 | checksum = calculate_MAC(key, data, iv) 164 | elif algo == "HMAC": 165 | hmac = HMAC.new(key, data) 166 | checksum = hmac.hexdigest() 167 | del hmac 168 | elif algo == "CC": 169 | if ssc is not None: 170 | data = inttostring(ssc) + data 171 | a = cipher(True, "des-cbc", key[:8], data) 172 | b = cipher(False, "des-ecb", key[8:16], a[-8:]) 173 | c = cipher(True, "des-ecb", key[:8], b) 174 | checksum = c 175 | 176 | return checksum 177 | 178 | 179 | def cipher(do_encrypt, cipherspec, key, data, iv=None): 180 | """Do a cryptographic operation. 181 | operation = do_encrypt ? encrypt : decrypt, 182 | cipherspec must be of the form "cipher-mode", or "cipher\"""" 183 | 184 | cipher = get_cipher(cipherspec, key, iv) 185 | 186 | result = None 187 | if do_encrypt: 188 | result = cipher.encrypt(data) 189 | else: 190 | result = cipher.decrypt(data) 191 | 192 | del cipher 193 | return result 194 | 195 | 196 | def encrypt(cipherspec, key, data, iv=None): 197 | return cipher(True, cipherspec, key, data, iv) 198 | 199 | 200 | def decrypt(cipherspec, key, data, iv=None): 201 | return cipher(False, cipherspec, key, data, iv) 202 | 203 | 204 | def hash(hashmethod, data): 205 | from Crypto.Hash import SHA, MD5 # , RIPEMD 206 | hash_class = locals().get(hashmethod.upper(), None) 207 | if hash_class is None: 208 | logging.error("Unknown Hash method %s" % hashmethod) 209 | raise ValueError 210 | hash = hash_class.new() 211 | hash.update(data) 212 | return hash.digest() 213 | 214 | 215 | def operation_on_string(string1, string2, op): 216 | if len(string1) != len(string2): 217 | raise ValueError("string1 and string2 must be of equal length") 218 | result = [] 219 | for i in range(len(string1)): 220 | result.append(chr(op(ord(string1[i]), ord(string2[i])))) 221 | return "".join(result) 222 | 223 | 224 | # ******************************************************************* 225 | # * Cyberflex specific methods * 226 | # ******************************************************************* 227 | def calculate_MAC(session_key, message, iv=CYBERFLEX_IV): 228 | """ 229 | Cyberflex MAC is the last Block of the input encrypted with DES3-CBC 230 | """ 231 | 232 | cipher = DES3.new(session_key, DES3.MODE_CBC, iv) 233 | padded = append_padding(8, message, 0x01) 234 | crypted = cipher.encrypt(padded) 235 | 236 | return crypted[len(padded) - cipher.block_size:] 237 | 238 | def _makesalt(): 239 | """Return a 48-bit pseudorandom salt for crypt(). 240 | 241 | This function is not suitable for generating cryptographic secrets. 242 | """ 243 | binarysalt = "".join([pack("@H", randint(0, 0xffff)) for i in range(3)]) 244 | return b64encode(binarysalt, "./") 245 | -------------------------------------------------------------------------------- /virtualsmartcard/SEutils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | 19 | from time import time 20 | from random import seed, randint 21 | 22 | from virtualsmartcard.TLVutils import pack, unpack, bertlv_pack 23 | from virtualsmartcard.ConstantDefinitions import CRT_TEMPLATE, ALGO_MAPPING 24 | from virtualsmartcard.ConstantDefinitions import SM_Class, MAX_EXTENDED_LE 25 | from virtualsmartcard.utils import inttostring, stringtoint, C_APDU 26 | from virtualsmartcard.SWutils import SwError, SW 27 | import virtualsmartcard.CryptoUtils as vsCrypto 28 | 29 | 30 | class ControlReferenceTemplate: 31 | """ 32 | Control Reference Templates are used to configure the Security 33 | Environments. They specify which algorithms to use in which mode of 34 | operation and with which keys. There are six different types of Control 35 | Reference Template: 36 | HT, AT, KT, CCT, DST, CT-sym, CT-asym. 37 | """ 38 | def __init__(self, tag, config=""): 39 | """ 40 | Generates a new CRT 41 | 42 | :param tag: Indicates the type of the CRT (HT, AT, KT, CCT, DST, 43 | CT-sym, CT-asym) 44 | :param config: A string containing TLV encoded Security Environment 45 | parameters 46 | """ 47 | if tag not in (CRT_TEMPLATE["AT"], CRT_TEMPLATE["HT"], 48 | CRT_TEMPLATE["KAT"], CRT_TEMPLATE["CCT"], 49 | CRT_TEMPLATE["DST"], CRT_TEMPLATE["CT"]): 50 | raise ValueError("Unknown control reference tag.") 51 | else: 52 | self.type = tag 53 | 54 | self.iv = None 55 | self.keyref_secret_key = None 56 | self.keyref_public_key = None 57 | self.keyref_private_key = None 58 | self.key = None 59 | self.fileref = None 60 | self.DFref = None 61 | self.keylength = None 62 | self.algorithm = None 63 | self.blocklength = None 64 | self.usage_qualifier = None 65 | if config != "": 66 | self.parse_SE_config(config) 67 | self.__config_string = config 68 | 69 | def parse_SE_config(self, config): 70 | """ 71 | Parse a control reference template as given e.g. in an MSE APDU. 72 | 73 | :param config: a TLV string containing the configuration for the CRT. 74 | """ 75 | error = False 76 | structure = unpack(config) 77 | for tlv in structure: 78 | tag, length, value = tlv 79 | if tag == 0x80: 80 | self.__set_algo(value) 81 | elif tag in (0x81, 0x82, 0x83, 0x84): 82 | self.__set_key(tag, value) 83 | elif tag in range(0x85, 0x93): 84 | self.__set_iv(tag, length, value) 85 | elif tag == 0x95: 86 | self.usage_qualifier = value 87 | else: 88 | error = True 89 | 90 | if error: 91 | raise SwError(SW["ERR_REFNOTUSABLE"]) 92 | else: 93 | return SW["NORMAL"], "" 94 | 95 | def __set_algo(self, data): 96 | """ 97 | Set the algorithm to be used by this CRT. The algorithms are specified 98 | in a global dictionary. New cards may add or modify this table in order 99 | to support new or different algorithms. 100 | 101 | :param data: reference to an algorithm 102 | """ 103 | 104 | if data not in ALGO_MAPPING: 105 | raise SwError(SW["ERR_REFNOTUSABLE"]) 106 | else: 107 | self.algorithm = ALGO_MAPPING[data] 108 | self.__replace_tag(0x80, data) 109 | 110 | def __set_key(self, tag, value): 111 | if tag == 0x81: 112 | self.fileref = value 113 | elif tag == 0x82: 114 | self.DFref = value 115 | elif tag == 0x83: 116 | self.keyref_secret_key = value 117 | self.keyref_public_key = value 118 | elif tag == 0x84: 119 | self.keyref_private_key = value 120 | 121 | def __set_iv(self, tag, length, value): 122 | iv = None 123 | if tag == 0x85: 124 | iv = 0x00 125 | elif tag == 0x86: 126 | pass # What is initial chaining block? 127 | elif tag == 0x87 or tag == 0x93: 128 | if length != 0: 129 | iv = value 130 | else: 131 | iv = self.iv + 1 132 | elif tag == 0x91: 133 | if length != 0: 134 | iv = value 135 | else: 136 | seed(time()) 137 | iv = hex(randint(0, 255)) 138 | elif tag == 0x92: 139 | if length != 0: 140 | iv = value 141 | else: 142 | iv = int(time()) 143 | self.iv = iv 144 | 145 | def __replace_tag(self, tag, data): 146 | """ 147 | Adjust the config string using a given tag, value combination. If the 148 | config string already contains a tag, value pair for the given tag, 149 | replace it. Otherwise append tag, length and value to the config 150 | string. 151 | """ 152 | position = 0 153 | while position < len(self.__config_string) and \ 154 | self.__config_string[position] != tag: 155 | length = stringtoint(self.__config_string[position+1]) 156 | position += length + 3 157 | 158 | if position < len(self.__config_string): # Replace Tag 159 | length = stringtoint(self.__config_string[position+1]) 160 | self.__config_string = self.__config_string[:position] +\ 161 | inttostring(tag) + inttostring(len(data)) + data +\ 162 | self.__config_string[position+2+length:] 163 | else: # Add new tag 164 | self.__config_string += inttostring(tag) + inttostring(len(data)) + data 165 | 166 | def to_string(self): 167 | """ 168 | Return the content of the CRT, encoded as TLV data in a string 169 | """ 170 | return self.__config_string 171 | 172 | def __str__(self): 173 | return self.__config_string 174 | 175 | 176 | class Security_Environment(object): 177 | 178 | def __init__(self, MF, SAM): 179 | self.sam = SAM 180 | self.mf = MF 181 | 182 | self.SEID = None 183 | 184 | # Control Reference Tables 185 | self.at = ControlReferenceTemplate(CRT_TEMPLATE["AT"]) 186 | self.kat = ControlReferenceTemplate(CRT_TEMPLATE["KAT"]) 187 | self.ht = ControlReferenceTemplate(CRT_TEMPLATE["HT"]) 188 | self.cct = ControlReferenceTemplate(CRT_TEMPLATE["CCT"]) 189 | self.dst = ControlReferenceTemplate(CRT_TEMPLATE["DST"]) 190 | self.ct = ControlReferenceTemplate(CRT_TEMPLATE["CT"]) 191 | 192 | self.capdu_sm = False 193 | self.rapdu_sm = False 194 | self.internal_auth = False 195 | self.external_auth = False 196 | 197 | def manage_security_environment(self, p1, p2, data): 198 | """ 199 | This method is used to store, restore or erase Security Environments 200 | or to manipulate the various parameters of the current SE. 201 | P1 specifies the operation to perform, p2 is either the SEID for the 202 | referred SE or the tag of a control reference template 203 | 204 | :param p1: 205 | Bitmask according to this table 206 | 207 | == == == == == == == == ======================================= 208 | b8 b7 b6 b5 b4 b3 b2 b1 Meaning 209 | == == == == == == == == ======================================= 210 | - - - 1 - - - - Secure messaging in command data field 211 | - - 1 - - - - - Secure messaging in response data field 212 | - 1 - - - - - - Computation, decipherment, internal 213 | authentication and key agreement 214 | 1 - - - - - - - Verification, encipherment, external 215 | authentication and key agreement 216 | - - - - 0 0 0 1 SET 217 | 1 1 1 1 0 0 1 0 STORE 218 | 1 1 1 1 0 0 1 1 RESTORE 219 | 1 1 1 1 0 1 0 0 ERASE 220 | == == == == == == == == ======================================= 221 | 222 | """ 223 | 224 | cmd = p1 & 0x0F 225 | se = p1 >> 4 226 | if(cmd == 0x01): 227 | # Secure messaging in command data field 228 | if se & 0x01: 229 | self.capdu_sm = True 230 | # Secure messaging in response data field 231 | if se & 0x02: 232 | self.rapdu_sm = True 233 | # Computation, decryption, internal authentication and key 234 | # agreement 235 | if se & 0x04: 236 | self.internal_auth = True 237 | # Verification, encryption, external authentication and key 238 | # agreement 239 | if se & 0x08: 240 | self.external_auth = True 241 | return self._set_SE(p2, data) 242 | elif cmd == 0x02: 243 | return self.sam.store_SE(p2) 244 | elif cmd == 0x03: 245 | return self.sam.restore_SE(p2) 246 | elif cmd == 0x04: 247 | return self.sam.erase_SE(p2) 248 | else: 249 | raise SwError(SW["ERR_INCORRECTP1P2"]) 250 | 251 | def _set_SE(self, p2, data): 252 | """ 253 | Manipulate the current Security Environment. P2 is the tag of a 254 | control reference template, data contains control reference objects 255 | """ 256 | 257 | if p2 == 0xA4: 258 | return self.at.parse_SE_config(data) 259 | elif p2 == 0xA6: 260 | return self.kat.parse_SE_config(data) 261 | elif p2 == 0xAA: 262 | return self.ht.parse_SE_config(data) 263 | elif p2 == 0xB4: 264 | return self.cct.parse_SE_config(data) 265 | elif p2 == 0xB6: 266 | return self.dst.parse_SE_config(data) 267 | elif p2 == 0xB8: 268 | return self.ct.parse_SE_config(data) 269 | 270 | raise SwError(SW["ERR_INCORRECTP1P2"]) 271 | 272 | def parse_SM_CAPDU(self, CAPDU, authenticate_header): 273 | """ 274 | This methods parses a data field including Secure Messaging objects. 275 | SM_header indicates whether or not the header of the message shall be 276 | authenticated. It returns an unprotected command APDU 277 | 278 | :param CAPDU: The protected CAPDU to be parsed 279 | :param authenticate_header: Whether or not the header should be 280 | included in authentication mechanisms 281 | :returns: Unprotected command APDU 282 | """ 283 | 284 | structure = unpack(CAPDU.data) 285 | return_data = ["", ] 286 | 287 | cla = None 288 | ins = None 289 | p1 = None 290 | p2 = None 291 | le = None 292 | 293 | if authenticate_header: 294 | to_authenticate = inttostring(CAPDU.cla) +\ 295 | inttostring(CAPDU.ins) + inttostring(CAPDU.p1) +\ 296 | inttostring(CAPDU.p2) 297 | to_authenticate = vsCrypto.append_padding(self.cct.blocklength, 298 | to_authenticate) 299 | else: 300 | to_authenticate = "" 301 | 302 | for tlv in structure: 303 | tag, length, value = tlv 304 | 305 | if tag % 2 == 1: # Include object in checksum calculation 306 | to_authenticate += bertlv_pack([[tag, length, value]]) 307 | 308 | # SM data objects for encapsulating plain values 309 | if tag in (SM_Class["PLAIN_VALUE_NO_TLV"], 310 | SM_Class["PLAIN_VALUE_NO_TLV_ODD"]): 311 | return_data.append(value) # FIXME: Need TLV coding? 312 | # Encapsulated SM objects. Parse them 313 | # FIXME: Need to pack value into a dummy CAPDU 314 | elif tag in (SM_Class["PLAIN_VALUE_TLV_INCULDING_SM"], 315 | SM_Class["PLAIN_VALUE_TLV_INCULDING_SM_ODD"]): 316 | return_data.append(self.parse_SM_CAPDU(value, 317 | authenticate_header)) 318 | # Encapsulated plaintext BER-TLV objects 319 | elif tag in (SM_Class["PLAIN_VALUE_TLV_NO_SM"], 320 | SM_Class["PLAIN_VALUE_TLV_NO_SM_ODD"]): 321 | return_data.append(value) 322 | elif tag in (SM_Class["Ne"], SM_Class["Ne_ODD"]): 323 | le = value 324 | elif tag == SM_Class["PLAIN_COMMAND_HEADER"]: 325 | if len(value) != 8: 326 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 327 | else: 328 | cla = value[:2] 329 | ins = value[2:4] 330 | p1 = value[4:6] 331 | p2 = value[6:8] 332 | 333 | # SM data objects for confidentiality 334 | if tag in (SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM"], 335 | SM_Class["CRYPTOGRAM_PLAIN_TLV_INCLUDING_SM_ODD"]): 336 | # The cryptogram includes SM objects. 337 | # We decrypt them and parse the objects. 338 | plain = self.decipher(tag, 0x80, value) 339 | # TODO: Need Le = length 340 | return_data.append(self.parse_SM_CAPDU(plain, 341 | authenticate_header)) 342 | elif tag in (SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM"], 343 | SM_Class["CRYPTOGRAM_PLAIN_TLV_NO_SM_ODD"]): 344 | # The cryptogram includes BER-TLV encoded plaintext. 345 | # We decrypt them and return the objects. 346 | plain = self.decipher(tag, 0x80, value) 347 | return_data.append(plain) 348 | elif tag in (SM_Class["CRYPTOGRAM_PADDING_INDICATOR"], 349 | SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"]): 350 | # The first byte of the data field indicates the padding to use 351 | """ 352 | Value Meaning 353 | '00' No further indication 354 | '01' Padding as specified in 6.2.3.1 355 | '02' No padding 356 | '1X' One to four secret keys for enciphering information, 357 | not keys ('X' is a bitmap with any value from '0' to 358 | 'F') 359 | '11' indicates the first key (e.g., an "even" control word 360 | in a pay TV system) 361 | '12' indicates the second key (e.g., an "odd" control word 362 | in a pay TV system) 363 | '13' indicates the first key followed by the second key 364 | (e.g., a pair of control words in a pay TV system) 365 | '2X' Secret key for enciphering keys, not information 366 | ('X' is a reference with any value from '0' to 'F') 367 | (e.g., in a pay TV system, either an operational key 368 | for enciphering control words, or a management key for 369 | enciphering operational keys) 370 | '3X' Private key of an asymmetric key pair ('X' is a 371 | reference with any value from '0' to 'F') 372 | '4X' Password ('X' is a reference with any value from '0' to 373 | 'F') 374 | '80' to '8E' Proprietary 375 | """ 376 | padding_indicator = stringtoint(value[0]) 377 | plain = self.decipher(tag, 0x80, value[1:]) 378 | plain = vsCrypto.strip_padding(self.ct.blocklength, plain, 379 | padding_indicator) 380 | return_data.append(plain) 381 | 382 | # SM data objects for authentication 383 | if tag == SM_Class["CHECKSUM"]: 384 | auth = vsCrypto.append_padding(self.cct.blocklength, 385 | to_authenticate) 386 | checksum = self.compute_cryptographic_checksum(0x8E, 0x80, 387 | auth) 388 | if checksum != value: 389 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 390 | elif tag == SM_Class["DIGITAL_SIGNATURE"]: 391 | auth = to_authenticate # FIXME: Need padding? 392 | signature = self.compute_digital_signature(0x9E, 0x9A, auth) 393 | if signature != value: 394 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 395 | elif tag in (SM_Class["HASH_CODE"], SM_Class["HASH_CODE_ODD"]): 396 | hash = self.hash(p1, p2, to_authenticate) 397 | if hash != value: 398 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 399 | 400 | # Form unprotected CAPDU 401 | if cla is None: 402 | cla = CAPDU.cla 403 | if ins is None: 404 | ins = CAPDU.ins 405 | if p1 is None: 406 | p1 = CAPDU.p1 407 | if p2 is None: 408 | p2 = CAPDU.p2 409 | # FIXME: 410 | # if expected != "": 411 | # raise SwError(SW["ERR_SECMESSOBJECTSMISSING"]) 412 | 413 | if isinstance(le, bytes): 414 | # FIXME: C_APDU only handles le with strings of length 1. 415 | # Better patch utils.py to support extended length apdus 416 | le_int = stringtoint(le) 417 | if le_int == 0 and len(le) > 1: 418 | le_int = MAX_EXTENDED_LE 419 | le = le_int 420 | 421 | c = C_APDU(cla=cla, ins=ins, p1=p1, p2=p2, le=le, 422 | data="".join(return_data)) 423 | return c 424 | 425 | def protect_response(self, sw, result): 426 | """ 427 | This method protects a response APDU using secure messaging mechanisms 428 | 429 | :returns: the protected data and the SW bytes 430 | """ 431 | 432 | return_data = "" 433 | # if sw == SW["NORMAL"]: 434 | # sw = inttostring(sw) 435 | # length = len(sw) 436 | # tag = SM_Class["PLAIN_PROCESSING_STATUS"] 437 | # tlv_sw = pack([(tag,length,sw)]) 438 | # return_data += tlv_sw 439 | 440 | if result != "": # Encrypt the data included in the RAPDU 441 | encrypted = self.encipher(0x82, 0x80, result) 442 | encrypted = "\x01" + encrypted 443 | encrypted_tlv = pack([( 444 | SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"], 445 | len(encrypted), 446 | encrypted)]) 447 | return_data += encrypted_tlv 448 | 449 | if sw == SW["NORMAL"]: 450 | if self.cct.algorithm is None: 451 | raise SwError(SW["CONDITIONSNOTSATISFIED"]) 452 | elif self.cct.algorithm == "CC": 453 | tag = SM_Class["CHECKSUM"] 454 | padded = vsCrypto.append_padding(self.cct.blocklength, 455 | return_data) 456 | auth = self.compute_cryptographic_checksum(0x8E, 0x80, padded) 457 | length = len(auth) 458 | return_data += pack([(tag, length, auth)]) 459 | elif self.cct.algorithm == "SIGNATURE": 460 | tag = SM_Class["DIGITAL_SIGNATURE"] 461 | hash = self.hash(0x90, 0x80, return_data) 462 | auth = self.compute_digital_signature(0x9E, 0x9A, hash) 463 | length = len(auth) 464 | return_data += pack([(tag, length, auth)]) 465 | 466 | return sw, return_data 467 | 468 | # The following commands implement ISO 7816-8 {{{ 469 | def perform_security_operation(self, p1, p2, data): 470 | """ 471 | In the end this command is nothing but a big switch for all the other 472 | commands in ISO 7816-8. It will invoke the appropriate command and 473 | return its result 474 | """ 475 | 476 | allowed_P1P2 = ((0x90, 0x80), (0x90, 0xA0), (0x9E, 0x9A), (0x9E, 0xAC), 477 | (0x9E, 0xBC), (0x00, 0xA2), (0x00, 0xA8), (0x00, 0x92), 478 | (0x00, 0xAE), (0x00, 0xBE), (0x82, 0x80), (0x84, 0x80), 479 | (0x86, 0x80), (0x80, 0x82), (0x80, 0x84), (0x80, 0x86)) 480 | 481 | if (p1, p2) not in allowed_P1P2: 482 | raise SwError(SW["INCORRECTP1P2"]) 483 | 484 | if((p2 in (0x80, 0xA0)) and (p1 == 0x90)): 485 | response_data = self.hash(p1, p2, data) 486 | elif(p2 in (0x9A, 0xAC, 0xBC) and p1 == 0x9E): 487 | response_data = self.compute_digital_signature(p1, p2, data) 488 | elif(p2 == 0xA2 and p1 == 0x00): 489 | response_data = self.verify_cryptographic_checksum(p1, p2, data) 490 | elif(p2 == 0xA8 and p1 == 0x00): 491 | response_data = self.verify_digital_signature(p1, p2, data) 492 | elif(p2 in (0x92, 0xAE, 0xBE) and p1 == 0x00): 493 | response_data = self.verify_certificate(p1, p2, data) 494 | elif (p2 == 0x80 and p1 in (0x82, 0x84, 0x86)): 495 | response_data = self.encipher(p1, p2, data) 496 | elif (p2 in (0x82, 0x84, 0x86) and p1 == 0x80): 497 | response_data = self.decipher(p1, p2, data) 498 | 499 | if p1 == 0x00: 500 | assert response_data == "" 501 | 502 | return SW["NORMAL"], response_data 503 | 504 | def compute_cryptographic_checksum(self, p1, p2, data): 505 | """ 506 | Compute a cryptographic checksum (e.g. MAC) for the given data. 507 | Algorithm and key are specified in the current SE 508 | """ 509 | if p1 != 0x8E or p2 != 0x80: 510 | raise SwError(SW["ERR_INCORRECTP1P2"]) 511 | if self.cct.key is None: 512 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 513 | 514 | checksum = vsCrypto.crypto_checksum(self.cct.algorithm, self.cct.key, 515 | data, self.cct.iv) 516 | return checksum 517 | 518 | def compute_digital_signature(self, p1, p2, data): 519 | """ 520 | Compute a digital signature for the given data. 521 | Algorithm and key are specified in the current SE 522 | 523 | :param p1: Must be 0x9E = Secure Messaging class for digital signatures 524 | :param p2: Must be one of 0x9A, 0xAC, 0xBC. Indicates what kind of data 525 | is included in the data field. 526 | """ 527 | 528 | if p1 != 0x9E or p2 not in (0x9A, 0xAC, 0xBC): 529 | raise SwError(SW["ERR_INCORRECTP1P2"]) 530 | 531 | if self.dst.key is None: 532 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 533 | 534 | to_sign = "" 535 | if p2 == 0x9A: # Data to be signed 536 | to_sign = data 537 | elif p2 == 0xAC: # Data objects, sign values 538 | to_sign = "" 539 | structure = unpack(data) 540 | for tag, length, value in structure: 541 | to_sign += value 542 | elif p2 == 0xBC: # Data objects to be signed 543 | pass 544 | 545 | signature = self.dst.key.sign(to_sign, "") 546 | return signature 547 | 548 | def hash(self, p1, p2, data): 549 | """ 550 | Hash the given data using the algorithm specified by the current 551 | Security environment. 552 | 553 | :return: raw data (no TLV coding). 554 | """ 555 | if p1 != 0x90 or p2 not in (0x80, 0xA0): 556 | raise SwError(SW["ERR_INCORRECTP1P2"]) 557 | algo = self.ht.algorithm 558 | if algo is None: 559 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 560 | try: 561 | hash = vsCrypto.hash(algo, data) 562 | except ValueError: 563 | raise SwError(SW["ERR_EXECUTION"]) 564 | 565 | return hash 566 | 567 | def verify_cryptographic_checksum(self, p1, p2, data): 568 | """ 569 | Verify the cryptographic checksum contained in the data field. 570 | Data field must contain a cryptographic checksum (tag 0x8E) and a plain 571 | value (tag 0x80) 572 | """ 573 | plain = "" 574 | cct = "" 575 | 576 | algo = self.cct.algorithm 577 | key = self.cct.key 578 | iv = self.cct.iv 579 | if algo is None or key is None: 580 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 581 | 582 | structure = unpack(data) 583 | for tag, length, value in structure: 584 | if tag == 0x80: 585 | plain = value 586 | elif tag == 0x8E: 587 | cct = value 588 | if plain == "" or cct == "": 589 | raise SwError(SW["ERR_SECMESSOBJECTSMISSING"]) 590 | else: 591 | my_cct = vsCrypto.crypto_checksum(algo, key, plain, iv) 592 | if my_cct == cct: 593 | return "" 594 | else: 595 | raise SwError["ERR_SECMESSOBJECTSINCORRECT"] 596 | 597 | def verify_digital_signature(self, p1, p2, data): 598 | """ 599 | Verify the digital signature contained in the data field. Data must 600 | contain a data to sign (tag 0x9A, 0xAC or 0xBC) and a digital signature 601 | (0x9E) 602 | """ 603 | key = self.dst.key 604 | to_sign = "" 605 | signature = "" 606 | 607 | if key is None: 608 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 609 | 610 | structure = unpack(data) 611 | for tag, length, value in structure: 612 | if tag == 0x9E: 613 | signature = value 614 | elif tag == 0x9A: # FIXME: Correct treatment of all possible tags 615 | to_sign = value 616 | elif tag == 0xAC: 617 | pass 618 | elif tag == 0xBC: 619 | pass 620 | 621 | if to_sign == "" or signature == "": 622 | raise SwError(SW["ERR_SECMESSOBJECTSMISSING"]) 623 | 624 | my_signature = key.sign(value) 625 | if my_signature == signature: 626 | return "" 627 | else: 628 | raise SwError(["ERR_SECMESSOBJECTSINCORRECT"]) 629 | 630 | def verify_certificate(self, p1, p2, data): 631 | """ 632 | Verify a certificate send by the terminal using the internal trust 633 | anchors. 634 | This method is currently not implemented. 635 | """ 636 | if p1 != 0x00 or p2 not in (0x92, 0xAE, 0xBE): 637 | raise SwError(SW["ERR_INCORRECTP1P2"]) 638 | else: 639 | raise SwError(SW["ERR_NOTSUPPORTED"]) 640 | 641 | def encipher(self, p1, p2, data): 642 | """ 643 | Encipher data using key, algorithm, IV and Padding specified 644 | by the current Security environment. 645 | 646 | :returns: raw data (no TLV coding). 647 | """ 648 | algo = self.ct.algorithm 649 | key = self.ct.key 650 | if key is None or algo is None: 651 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 652 | else: 653 | blocklen = vsCrypto.get_cipher_blocklen(algo) 654 | padded = vsCrypto.append_padding(blocklen, data) 655 | crypted = vsCrypto.encrypt(algo, key, padded, self.ct.iv) 656 | return crypted 657 | 658 | def decipher(self, p1, p2, data): 659 | """ 660 | Decipher data using key, algorithm, IV and Padding specified 661 | by the current Security environment. 662 | 663 | :returns: raw data (no TLV coding). Padding is not removed!!! 664 | """ 665 | algo = self.ct.algorithm 666 | key = self.ct.key 667 | if key is None or algo is None: 668 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 669 | else: 670 | plain = vsCrypto.decrypt(algo, key, data, self.ct.iv) 671 | return plain 672 | 673 | def generate_public_key_pair(self, p1, p2, data): 674 | """ 675 | The GENERATE PUBLIC-KEY PAIR command either initiates the generation 676 | and storing of a key pair, i.e., a public key and a private key, in the 677 | card, or accesses a key pair previously generated in the card. 678 | 679 | :param p1: should be 0x00 (generate new key) 680 | :param p2: '00' (no information provided) or reference of the key to 681 | be generated 682 | :param data: One or more CRTs associated to the key generation if 683 | P1-P2 different from '0000' 684 | """ 685 | 686 | from Crypto.PublicKey import RSA, DSA 687 | 688 | cipher = self.ct.algorithm 689 | 690 | c_class = locals().get(cipher, None) 691 | if c_class is None: 692 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 693 | 694 | if p1 & 0x01 == 0x00: # Generate key 695 | PublicKey = c_class.generate(self.dst.keylength) 696 | self.dst.key = PublicKey 697 | else: 698 | pass # Read key 699 | 700 | # Encode keys 701 | if cipher == "RSA": 702 | # Public key 703 | n = inttostring(PublicKey.n) 704 | e = inttostring(PublicKey.e) 705 | pk = ((0x81, len(n), n), (0x82, len(e), e)) 706 | result = bertlv_pack(pk) 707 | # Private key 708 | d = PublicKey.d 709 | elif cipher == "DSA": 710 | # DSAParams 711 | p = inttostring(PublicKey.p) 712 | q = inttostring(PublicKey.q) 713 | g = inttostring(PublicKey.g) 714 | # Public key 715 | y = inttostring(PublicKey.y) 716 | pk = ((0x81, len(p), p), (0x82, len(q), q), (0x83, len(g), g), 717 | (0x84, len(y), y)) 718 | # Private key 719 | x = inttostring(PublicKey.x) 720 | # Add more algorithms here 721 | # elif cipher = "ECDSA": 722 | else: 723 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 724 | 725 | result = bertlv_pack([[0x7F49, len(pk), pk]]) 726 | # TODO: Internally store key pair 727 | 728 | if p1 & 0x02 == 0x02: 729 | # We do not support extended header lists yet 730 | raise SwError["ERR_NOTSUPPORTED"] 731 | else: 732 | return SW["NORMAL"], result 733 | -------------------------------------------------------------------------------- /virtualsmartcard/SWutils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Frank Morgner 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | # Meaning of the interindustry values of SW1-SW2 20 | SW = { 21 | "NORMAL": 0x9000, 22 | "NORMAL_REST": 0x6100, 23 | "WARN_NOINFO62": 0x6200, 24 | "WARN_DATACORRUPTED": 0x6281, 25 | "WARN_EOFBEFORENEREAD": 0x6282, 26 | "WARN_DEACTIVATED": 0x6283, 27 | "WARN_FCIFORMATTING": 0x6284, 28 | "WARN_TERMINATIONSTATE": 0x6285, 29 | "WARN_NOINPUTSENSOR": 0x6286, 30 | "WARN_NOINFO63": 0x6300, 31 | "WARN_FILEFILLED": 0x6381, 32 | "ERR_EXECUTION": 0x6400, 33 | "ERR_RESPONSEREQUIRED": 0x6401, 34 | "ERR_NOINFO65": 0x6500, 35 | "ERR_MEMFAILURE": 0x6581, 36 | "ERR_WRONGLENGTH": 0x6700, 37 | "ERR_NOINFO68": 0x6800, 38 | "ERR_CHANNELNOTSUPPORTED": 0x6881, 39 | "ERR_SECMESSNOTSUPPORTED": 0x6882, 40 | "ERR_LASTCMDEXPECTED": 0x6883, 41 | "ERR_CHAININGNOTSUPPORTED": 0x6884, 42 | "ERR_NOINFO69": 0x6900, 43 | "ERR_INCOMPATIBLEWITHFILE": 0x6981, 44 | "ERR_SECSTATUS": 0x6982, 45 | "ERR_AUTHBLOCKED": 0x6983, 46 | "ERR_REFNOTUSABLE": 0x6984, 47 | "ERR_CONDITIONNOTSATISFIED": 0x6985, 48 | "ERR_NOCURRENTEF": 0x6986, 49 | "ERR_SECMESSOBJECTSMISSING": 0x6987, 50 | "ERR_SECMESSOBJECTSINCORRECT": 0x6988, 51 | "ERR_NOINFO6A": 0x6A00, 52 | "ERR_INCORRECTPARAMETERS": 0x6A80, 53 | "ERR_NOTSUPPORTED": 0x6A81, 54 | "ERR_FILENOTFOUND": 0x6A82, 55 | "ERR_RECORDNOTFOUND": 0x6A83, 56 | "ERR_NOTENOUGHMEMORY": 0x6A84, 57 | "ERR_NCINCONSISTENTWITHTLV": 0x6A85, 58 | "ERR_INCORRECTP1P2": 0x6A86, 59 | "ERR_NCINCONSISTENTP1P2": 0x6A87, 60 | "ERR_DATANOTFOUND": 0x6A88, 61 | "ERR_FILEEXISTS": 0x6A89, 62 | "ERR_DFNAMEEXISTS": 0x6A8A, 63 | "ERR_OFFSETOUTOFFILE": 0x6B00, 64 | "ERR_INSNOTSUPPORTED": 0x6D00, 65 | } 66 | 67 | SW_MESSAGES = { 68 | 0x9000: 'Normal processing (No further qualification)', 69 | 70 | 0x6200: 'Warning processing (State of non-volatile memory is unchanged): ' 71 | 'No information given', 72 | 0x6281: 'Warning processing (State of non-volatile memory is unchanged): ' 73 | 'Part of returned data may be corrupted', 74 | 0x6282: 'Warning processing (State of non-volatile memory is unchanged): ' 75 | 'End of file or record reached before reading Ne bytes', 76 | 0x6283: 'Warning processing (State of non-volatile memory is unchanged): ' 77 | 'Selected file deactivated', 78 | 0x6284: 'Warning processing (State of non-volatile memory is unchanged): ' 79 | 'File control information not formatted according to 5.3.3', 80 | 0x6285: 'Warning processing (State of non-volatile memory is unchanged): ' 81 | 'Selected file in termination state', 82 | 0x6286: 'Warning processing (State of non-volatile memory is unchanged): ' 83 | 'No input data available from a sensor on the card', 84 | 85 | 0x6300: 'Warning processing (State of non-volatile memory has changed): ' 86 | 'No information given', 87 | 0x6381: 'Warning processing (State of non-volatile memory has changed): ' 88 | 'File filled up by the last write', 89 | 90 | 0x6400: 'Execution error (State of non-volatile memory is unchanged): ' 91 | 'Execution error', 92 | 0x6401: 'Execution error (State of non-volatile memory is unchanged): ' 93 | 'Immediate response required by the card', 94 | 95 | 0x6500: 'Execution error (State of non-volatile memory has changed): ' 96 | 'No information given', 97 | 0x6581: 'Execution error (State of non-volatile memory has changed): ' 98 | 'Memory failure', 99 | 100 | 0x6700: 'Checking error: Wrong length; no further indication', 101 | 102 | 0x6800: 'Checking error (Functions in CLA not supported): ' 103 | 'No information given', 104 | 0x6881: 'Checking error (Functions in CLA not supported): ' 105 | 'Logical channel not supported', 106 | 0x6882: 'Checking error (Functions in CLA not supported): ' 107 | 'Secure messaging not supported', 108 | 0x6883: 'Checking error (Functions in CLA not supported): ' 109 | 'Last command of the chain expected', 110 | 0x6884: 'Checking error (Functions in CLA not supported): ' 111 | 'Command chaining not supported', 112 | 113 | 0x6900: 'Checking error (Command not allowed): ' 114 | 'No information given', 115 | 0x6981: 'Checking error (Command not allowed): ' 116 | 'Command incompatible with file structure', 117 | 0x6982: 'Checking error (Command not allowed): ' 118 | 'Security status not satisfied', 119 | 0x6983: 'Checking error (Command not allowed): ' 120 | 'Authentication method blocked', 121 | 0x6984: 'Checking error (Command not allowed): ' 122 | 'Reference data not usable', 123 | 0x6985: 'Checking error (Command not allowed): ' 124 | 'Conditions of use not satisfied', 125 | 0x6986: 'Checking error (Command not allowed): ' 126 | 'Command not allowed (no current EF)', 127 | 0x6987: 'Checking error (Command not allowed): ' 128 | 'Expected secure messaging data objects missing', 129 | 0x6988: 'Checking error (Command not allowed): ' 130 | 'Incorrect secure messaging data objects', 131 | 132 | 0x6A00: 'Checking error (Wrong parameters P1-P2): ' 133 | 'No information given', 134 | 0x6A80: 'Checking error (Wrong parameters P1-P2): ' 135 | 'Incorrect parameters in the command data field', 136 | 0x6A81: 'Checking error (Wrong parameters P1-P2): ' 137 | 'Function not supported', 138 | 0x6A82: 'Checking error (Wrong parameters P1-P2): ' 139 | 'File or application not found', 140 | 0x6A83: 'Checking error (Wrong parameters P1-P2): ' 141 | 'Record not found', 142 | 0x6A84: 'Checking error (Wrong parameters P1-P2): ' 143 | 'Not enough memory space in the file', 144 | 0x6A85: 'Checking error (Wrong parameters P1-P2): ' 145 | 'Nc inconsistent with TLV structure', 146 | 0x6A86: 'Checking error (Wrong parameters P1-P2): ' 147 | 'Incorrect parameters P1-P2', 148 | 0x6A87: 'Checking error (Wrong parameters P1-P2): ' 149 | 'Nc inconsistent with parameters P1-P2', 150 | 0x6A88: 'Checking error (Wrong parameters P1-P2): ' 151 | 'Referenced data or reference data not found (exact meaning ' 152 | 'depending on the command)', 153 | 0x6A89: 'Checking error (Wrong parameters P1-P2): ' 154 | 'File already exists', 155 | 0x6A8A: 'Checking error (Wrong parameters P1-P2): ' 156 | 'DF name already exists', 157 | 158 | 0x6B00: 'Checking error (Wrong parameters P1-P2): ' 159 | 'Wrong parameters (offset outside the EF)', 160 | 0x6D00: 'Checking error (Instruction code not supported or invalid)', 161 | 0x6E00: 'Checking error (Class not supported)', 162 | 0x6F00: 'Checking error (No precise diagnosis)', 163 | } 164 | for i in range(0, 0xff): 165 | SW_MESSAGES[0x6100 + i] = 'Normal Processing (' + str(i) + \ 166 | ' data bytes still available)' 167 | SW_MESSAGES[0x6600 + i] = 'Execution error (Security-related issues)' 168 | SW_MESSAGES[0x6C00 + i] = 'Checking error (Wrong Le field; ' + str(i) + \ 169 | ' available data bytes)' 170 | 171 | 172 | class SwError(Exception): 173 | def __init__(self, sw): 174 | self.sw = sw 175 | self.message = SW_MESSAGES[sw] 176 | -------------------------------------------------------------------------------- /virtualsmartcard/SmartcardSAM.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2014 Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | 20 | import logging 21 | from pickle import dumps, loads 22 | from os import urandom 23 | 24 | import virtualsmartcard.CryptoUtils as vsCrypto 25 | from virtualsmartcard.SWutils import SwError, SW 26 | from virtualsmartcard.utils import inttostring, stringtoint, hexdump 27 | from virtualsmartcard.SEutils import Security_Environment 28 | 29 | 30 | def get_referenced_cipher(p1): 31 | """ 32 | P1 defines the algorithm and mode to use. We dispatch it and return a 33 | string that is understood by CryptoUtils.py functions 34 | """ 35 | 36 | ciphertable = { 37 | 0x00: "DES3-ECB", 38 | 0x01: "DES3-ECB", 39 | 0x02: "DES3-CBC", 40 | 0x03: "DES-ECB", 41 | 0x04: "DES-CBC", 42 | 0x05: "AES-ECB", 43 | 0x06: "AES-CBC", 44 | 0x07: "RSA", 45 | 0x08: "DSA" 46 | } 47 | 48 | if (p1 in ciphertable): 49 | return ciphertable[p1] 50 | else: 51 | raise SwError(SW["ERR_INCORRECTP1P2"]) 52 | 53 | 54 | class SAM(object): 55 | """ 56 | This class is used to store the data needed by the SAM. 57 | It includes the PIN, the master key of the SAM and a hashmap containing all 58 | the keys used by the file encryption system. The keys in the hashmap are 59 | indexed via the path to the corresponding container. 60 | """ 61 | 62 | def __init__(self, PIN, cardNumber, mf=None, cardSecret=None, 63 | default_se=Security_Environment): 64 | 65 | self.PIN = PIN 66 | self.mf = mf 67 | self.cardNumber = cardNumber 68 | 69 | self.last_challenge = None # Will contain non-readable binary string 70 | self.counter = 3 # Number of tries for PIN validation 71 | 72 | self.cipher = 0x01 73 | self.asym_key = None 74 | 75 | keylen = vsCrypto.get_cipher_keylen(get_referenced_cipher(self.cipher)) 76 | if cardSecret is None: # Generate a random card secret 77 | self.cardSecret = urandom(keylen) 78 | else: 79 | if len(cardSecret) != keylen: 80 | raise ValueError("cardSecret has the wrong key length for: " + 81 | get_referenced_cipher(self.cipher)) 82 | else: 83 | self.cardSecret = cardSecret 84 | 85 | # Security Environments may be saved to/retrieved from this dictionary 86 | self.saved_SEs = {} 87 | self.default_se = default_se 88 | self.current_SE = default_se(self.mf, self) 89 | 90 | def set_MF(self, mf): 91 | """ 92 | Setter function for the internal reference to the Filesystem. The SAM 93 | needs a reference to the filesystem in order to store/retrieve keys. 94 | """ 95 | self.mf = mf 96 | 97 | def FSencrypt(self, data): 98 | """ 99 | Encrypt the given data, using the parameters stored in the SAM. 100 | Right now we do not encrypt the data. In memory encryption might or 101 | might not be added in a future version. 102 | """ 103 | return data 104 | 105 | def FSdecrypt(self, data): 106 | """ 107 | Decrypt the given data, using the parameters stored in the SAM. 108 | Right now we do not encrypt the data. In memory encryption might or 109 | might not be added in a future version. 110 | """ 111 | return data 112 | 113 | def store_SE(self, SEID): 114 | """ 115 | Stores the current Security environment in the secure access module. 116 | The SEID is used as a reference to identify the SE. 117 | """ 118 | SEstr = dumps(self.current_SE) 119 | self.saved_SEs[SEID] = SEstr 120 | return SW["NORMAL"], "" 121 | 122 | def restore_SE(self, SEID): 123 | """ 124 | Restores a Security Environment from the SAM and replaces the current 125 | SE with it. 126 | """ 127 | 128 | if (SEID not in self.saved_SEs): 129 | raise SwError(SW["ERR_REFNOTUSABLE"]) 130 | else: 131 | SEstr = self.saved_SEs[SEID] 132 | SE = loads(SEstr) 133 | if isinstance(SE, self.default_se): 134 | self.current_SE = SE 135 | else: 136 | raise SwError(SW["ERR_REFNOTUSABLE"]) 137 | 138 | return SW["NORMAL"], "" 139 | 140 | def erase_SE(self, SEID): 141 | """ 142 | Erases a Security Environment stored under SEID from the SAM 143 | """ 144 | if (SEID not in self.saved_SEs): 145 | raise SwError(SW["ERR_REFNOTUSABLE"]) 146 | else: 147 | del self.saved_SEs[SEID] 148 | 149 | return SW["NORMAL"], "" 150 | 151 | def set_asym_algorithm(self, cipher, keytype): 152 | """ 153 | :param cipher: Public/private key object from used for encryption 154 | :param keytype: Type of the public key (e.g. RSA, DSA) 155 | """ 156 | if keytype not in (0x07, 0x08): 157 | raise SwError(SW["ERR_INCORRECTP1P2"]) 158 | else: 159 | self.cipher = keytype 160 | self.asym_key = cipher 161 | 162 | def verify(self, p1, p2, PIN): 163 | """ 164 | Authenticate the card user. Check if he entered a valid PIN. 165 | If the PIN is invalid decrement retry counter. If retry counter 166 | equals zero, block the card until reset with correct PUK 167 | """ 168 | 169 | logging.debug("Received PIN: %s", PIN.strip()) 170 | PIN = PIN.replace(b"\0", b"") # Strip NULL characters 171 | 172 | if p1 != 0x00: 173 | raise SwError(SW["ERR_INCORRECTP1P2"]) 174 | 175 | if self.counter > 0: 176 | if self.PIN == PIN: 177 | self.counter = 3 178 | return SW["NORMAL"], "" 179 | else: 180 | self.counter -= 1 181 | raise SwError(SW["WARN_NOINFO63"]) 182 | else: 183 | raise SwError(SW["ERR_AUTHBLOCKED"]) 184 | 185 | def change_reference_data(self, p1, p2, data): 186 | """ 187 | Change the specified referenced data (e.g. CHV) of the card 188 | """ 189 | 190 | data = data.replace("\0", "") # Strip NULL characters 191 | self.PIN = data 192 | return SW["NORMAL"], "" 193 | 194 | def internal_authenticate(self, p1, p2, data): 195 | """ 196 | Authenticate card to terminal. Encrypt the challenge of the terminal 197 | to prove key posession 198 | """ 199 | 200 | if p1 == 0x00: # No information given 201 | cipher = get_referenced_cipher(self.cipher) 202 | else: 203 | cipher = get_referenced_cipher(p1) 204 | 205 | if cipher == "RSA" or cipher == "DSA": 206 | crypted_challenge = self.asym_key.sign(data, "") 207 | crypted_challenge = crypted_challenge[0] 208 | crypted_challenge = inttostring(crypted_challenge) 209 | else: 210 | key = self._get_referenced_key(p1, p2) 211 | crypted_challenge = vsCrypto.encrypt(cipher, key, data) 212 | 213 | return SW["NORMAL"], crypted_challenge 214 | 215 | def external_authenticate(self, p1, p2, data): 216 | """ 217 | Authenticate the terminal to the card. Check whether Terminal correctly 218 | encrypted the given challenge or not 219 | """ 220 | if self.last_challenge is None: 221 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 222 | 223 | key = self._get_referenced_key(p1, p2) 224 | if p1 == 0x00: # No information given 225 | cipher = get_referenced_cipher(self.cipher) 226 | else: 227 | cipher = get_referenced_cipher(p1) 228 | 229 | blocklen = vsCrypto.get_cipher_blocklen(cipher) 230 | reference = vsCrypto.append_padding(blocklen, self.last_challenge) 231 | reference = vsCrypto.encrypt(cipher, key, reference) 232 | if reference == data: 233 | # Invalidate last challenge 234 | self.last_challenge is None 235 | return SW["NORMAL"], "" 236 | else: 237 | raise SwError(SW["WARN_NOINFO63"]) 238 | # TODO: Counter for external authenticate? 239 | 240 | def mutual_authenticate(self, p1, p2, mutual_challenge): 241 | """ 242 | Takes an encrypted challenge in the form 243 | 'Terminal Challenge | Card Challenge | Card number' 244 | and checks it for validity. If the challenge is successful 245 | the card encrypts 'Card Challenge | Terminal challenge' and 246 | returns this value 247 | """ 248 | 249 | key = self._get_referenced_key(p1, p2) 250 | card_number = self.get_card_number() 251 | 252 | if key is None: 253 | raise SwError(SW["ERR_INCORRECTP1P2"]) 254 | if p1 == 0x00: # No information given 255 | cipher = get_referenced_cipher(self.cipher) 256 | else: 257 | cipher = get_referenced_cipher(p1) 258 | 259 | if cipher is None: 260 | raise SwError(SW["ERR_INCORRECTP1P2"]) 261 | 262 | plain = vsCrypto.decrypt(cipher, key, mutual_challenge) 263 | last_challenge_len = len(self.last_challenge) 264 | terminal_challenge = plain[:last_challenge_len - 1] 265 | card_challenge = plain[last_challenge_len:-len(card_number) - 1] 266 | serial_number = plain[-len(card_number):] 267 | 268 | if terminal_challenge != self.last_challenge: 269 | raise SwError(SW["WARN_NOINFO63"]) 270 | elif serial_number != card_number: 271 | raise SwError(SW["WARN_NOINFO63"]) 272 | 273 | result = card_challenge + terminal_challenge 274 | return SW["NORMAL"], vsCrypto.encrypt(cipher, key, result) 275 | 276 | def get_challenge(self, p1, p2, data): 277 | """ 278 | Generate a random number of maximum 8 Byte and return it. 279 | """ 280 | if p1 != 0x00 or p2 != 0x00: # RFU 281 | raise SwError(SW["ERR_INCORRECTP1P2"]) 282 | 283 | length = 8 # Length of the challenge in Byte 284 | self.last_challenge = urandom(length) 285 | logging.debug("Generated challenge: %s", self.last_challenge) 286 | 287 | return SW["NORMAL"], self.last_challenge 288 | 289 | def get_card_number(self): 290 | return SW["NORMAL"], inttostring(self.cardNumber) 291 | 292 | def _get_referenced_key(self, p1, p2): 293 | """ 294 | This method returns the key specified by the p2 parameter. The key may 295 | be stored on the cards filesystem. 296 | 297 | :param p1: Specifies the algorithm to use. 298 | :param p2: Specifies a reference to the key to be used for encryption. 299 | 300 | == == == == == == == == =========================================== 301 | b8 b7 b6 b5 b4 b3 b2 b1 Meaning 302 | == == == == == == == == =========================================== 303 | 0 0 0 0 0 0 0 0 No information is given 304 | 0 - - - - - - - Global reference data(e.g. MF specific key) 305 | 1 - - - - - - - Specific reference data(e.g. DF specific 306 | key) 307 | - - - x x x x x Number of the secret 308 | == == == == == == == == =========================================== 309 | 310 | Any other value RFU 311 | """ 312 | 313 | key = None 314 | qualifier = p2 & 0x1F 315 | algo = get_referenced_cipher(p1) 316 | keylength = vsCrypto.get_cipher_keylen(algo) 317 | 318 | if p2 == 0x00: # No information given, use the global card key 319 | key = self.cardSecret 320 | # We treat global and specific reference data alike 321 | else: 322 | # Interpret qualifier as an short fid (try to read the key from FS) 323 | if self.mf is None: 324 | raise SwError(SW["ERR_REFNOTUSABLE"]) 325 | df = self.mf.currentDF() 326 | fid = df.select("fid", stringtoint(qualifier)) 327 | key = fid.readbinary(keylength) 328 | 329 | if key is not None: 330 | return key 331 | else: 332 | raise SwError(SW["ERR_REFNOTUSABLE"]) 333 | 334 | # The following commands define the Secure Messaging interface 335 | def generate_public_key_pair(self, p1, p2, data): 336 | return self.current_SE.generate_public_key_pair(p1, p2, data) 337 | 338 | def parse_SM_CAPDU(self, CAPDU, header_authentication): 339 | """ 340 | Parse a command APDU protected by Secure Messaging and return the 341 | unprotected command APDU 342 | """ 343 | try: 344 | return self.current_SE.parse_SM_CAPDU(CAPDU, header_authentication) 345 | except: 346 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 347 | 348 | def protect_result(self, sw, unprotected_result): 349 | """ 350 | Protect a plain response APDU by Secure Messaging 351 | """ 352 | logging.info("Unprotected Response Data:\n" + 353 | hexdump(unprotected_result)) 354 | return self.current_SE.protect_response(sw, unprotected_result) 355 | 356 | def perform_security_operation(self, p1, p2, data): 357 | return self.current_SE.perform_security_operation(p1, p2, data) 358 | 359 | def manage_security_environment(self, p1, p2, data): 360 | return self.current_SE.manage_security_environment(p1, p2, data) 361 | -------------------------------------------------------------------------------- /virtualsmartcard/TLVutils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Frank Morgner 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | 20 | from virtualsmartcard.utils import stringtoint, inttostring 21 | 22 | TAG = { 23 | "FILECONTROLPARAMETERS": 0x62, 24 | "FILEMANAGEMENTDATA": 0x64, 25 | "FILECONTROLINFORMATION": 0x6F, 26 | "BYTES_EXCLUDINGSTRUCTURE": 0x80, 27 | "BYTES_INCLUDINGSTRUCTURE": 0x81, 28 | "FILEDISCRIPTORBYTE": 0x82, 29 | "FILEIDENTIFIER": 0x83, 30 | "DFNAME": 0x84, 31 | "PROPRIETARY_NOTBERTLV": 0x85, 32 | "PROPRIETARY_SECURITY": 0x86, 33 | "FIDEF_CONTAININGFCI": 0x87, 34 | "SHORTFID": 0x88, 35 | "LIFECYCLESTATUS": 0x8A, 36 | "SA_EXPANDEDFORMAT": 0x8B, 37 | "SA_COMPACTFORMAT": 0x8C, 38 | "FIDEF_CONTAININGSET": 0x8D, 39 | "CHANNELSECURITY": 0x8E, 40 | "SA_DATAOBJECTS": 0xA0, 41 | "PROPRIETARY_SECURITYTEMP": 0xA1, 42 | "PROPRIETARY_BERTLV": 0xA5, 43 | "SA_EXPANDEDFORMAT_TEMP": 0xAB, 44 | "CRYPTIDENTIFIER_TEMP": 0xAC, 45 | "DISCRETIONARY_DATA": 0x53, 46 | "DISCRETIONARY_TEMPLATE": 0x73, 47 | "OFFSET_DATA": 0x54, 48 | "TAG_LIST": 0x5C, 49 | "HEADER_LIST": 0x5D, 50 | "EXTENDED_HEADER_LIST": 0x4D 51 | } 52 | 53 | 54 | def tlv_unpack(data): 55 | ber_class = (data[0] & 0xC0) >> 6 56 | # 0 = primitive, 0x20 = constructed 57 | constructed = (data[0] & 0x20) != 0 58 | tag = data[0] 59 | data = data[1:] 60 | if (tag & 0x1F) == 0x1F: 61 | tag = (tag << 8) | data[0] 62 | while data[0] & 0x80 == 0x80: 63 | data = data[1:] 64 | tag = (tag << 8) | data[0] 65 | data = data[1:] 66 | 67 | length = data[0] 68 | if length < 0x80: 69 | data = data[1:] 70 | elif length & 0x80 == 0x80: 71 | length_ = 0 72 | data = data[1:] 73 | for i in range(0, length & 0x7F): 74 | length_ = length_ * 256 + data[0] 75 | data = data[1:] 76 | length = length_ 77 | 78 | value = b"".join(inttostring(i) for i in data[:length]) 79 | rest = data[length:] 80 | 81 | return ber_class, constructed, tag, length, value, rest 82 | 83 | 84 | def tlv_find_tags(tlv_data, tags, num_results=None): 85 | """Find (and return) all instances of tags in the given tlv structure (as 86 | returned by unpack). If num_results is specified then at most that many 87 | results will be returned.""" 88 | 89 | results = [] 90 | 91 | def find_recursive(tlv_data): 92 | for d in tlv_data: 93 | t, l, v = d[:3] 94 | if t in tags: 95 | results.append(d) 96 | else: 97 | if isinstance(v, bytes) and not isinstance(v[0], int): 98 | find_recursive(v) 99 | 100 | if num_results is not None and len(results) >= num_results: 101 | return 102 | 103 | find_recursive(tlv_data) 104 | 105 | return results 106 | 107 | 108 | def tlv_find_tag(tlv_data, tag, num_results=None): 109 | """Find (and return) all instances of tag in the given tlv structure (as 110 | returned by unpack). 111 | If num_results is specified then at most that many results will be 112 | returned.""" 113 | 114 | return tlv_find_tags(tlv_data, [tag], num_results) 115 | 116 | 117 | def pack(tlv_data, recalculate_length=False): 118 | result = [] 119 | 120 | for data in tlv_data: 121 | tag, length, value = data[:3] 122 | if tag in (0xff, 0x00): 123 | result = result + inttostring(tag) 124 | continue 125 | 126 | if not isinstance(value, bytes): 127 | value = pack(value, recalculate_length) 128 | 129 | if recalculate_length: 130 | length = len(value) 131 | 132 | t = b"" 133 | while tag > 0: 134 | t = inttostring(tag & 0xff) + t 135 | tag = tag >> 8 136 | 137 | if length < 0x7F: 138 | l = inttostring(length) 139 | else: 140 | l = b"" 141 | while length > 0: 142 | l = inttostring(length & 0xff) + l 143 | length = length >> 8 144 | assert len(l) < 0x7f 145 | l = inttostring(0x80 | len(l)) + l 146 | 147 | result.append(t) 148 | result.append(l) 149 | result.append(value) 150 | 151 | return b"".join(result) 152 | 153 | 154 | def bertlv_pack(data): 155 | """Packs a bertlv list of 3-tuples (tag, length, newvalue) into a string""" 156 | return pack(data) 157 | 158 | 159 | def unpack(data, with_marks=None, offset=0, include_filler=False): 160 | result = [] 161 | if isinstance(data, str): 162 | data = list(map(ord, data)) 163 | while len(data) > 0: 164 | if data[0] in (0x00, 0xFF): 165 | if include_filler: 166 | if with_marks is None: 167 | result.append((data[0], None, None)) 168 | else: 169 | result.append((data[0], None, None, ())) 170 | data = data[1:] 171 | offset = offset + 1 172 | continue 173 | 174 | l = len(data) 175 | ber_class, constructed, tag, length, value, data = tlv_unpack(data) 176 | stop = offset + (l - len(data)) 177 | start = stop - length 178 | 179 | if with_marks is not None: 180 | marks = [] 181 | for type, mark_start, mark_stop in with_marks: 182 | if (mark_start, mark_stop) == (start, stop): 183 | marks.append(type) 184 | marks = (marks,) 185 | else: 186 | marks = () 187 | 188 | if not constructed: 189 | result.append((tag, length, value) + marks) 190 | else: 191 | result.append((tag, length, 192 | unpack(value, with_marks, offset=start)) + marks) 193 | 194 | offset = stop 195 | 196 | return result 197 | 198 | 199 | def bertlv_unpack(data): 200 | """Unpacks a bertlv coded string into a list of 3-tuples (tag, length, 201 | newvalue).""" 202 | return unpack(data) 203 | 204 | 205 | def simpletlv_pack(tlv_data, recalculate_length=False): 206 | result = b"" 207 | 208 | for tag, length, value in tlv_data: 209 | if tag >= 0xff or tag <= 0x00: 210 | # invalid 211 | continue 212 | 213 | if recalculate_length: 214 | length = len(value) 215 | if length > 0xffff or length < 0: 216 | # invalid 217 | continue 218 | 219 | if length < 0xff: 220 | result += inttostring(tag) + inttostring(length) + value 221 | else: 222 | result += inttostring(tag) + inttostring(0xff) + inttostring(length >> 8) + \ 223 | inttostring(length & 0xff) + value 224 | 225 | return result 226 | 227 | 228 | def simpletlv_unpack(data): 229 | """Unpacks a simpletlv coded string into a list of 3-tuples (tag, length, 230 | newvalue).""" 231 | result = [] 232 | if isinstance(data, str): 233 | data = list(map(ord, data)) 234 | rest = data 235 | while rest: 236 | tag = rest[0] 237 | if tag == 0 or tag == 0xff: 238 | raise ValueError 239 | 240 | length = rest[1] 241 | if length == 0xff: 242 | length = (rest[2] << 8) + rest[3] 243 | newvalue = rest[4:4 + length] 244 | rest = rest[4 + length:] 245 | else: 246 | newvalue = rest[2:2 + length] 247 | rest = rest[2 + length:] 248 | result.append((tag, length, newvalue)) 249 | 250 | return result 251 | 252 | 253 | def decodeDiscretionaryDataObjects(tlv_data): 254 | datalist = [] 255 | tlv_tags = (tlv_find_tags(tlv_data, [TAG["DISCRETIONARY_DATA"], 256 | TAG["DISCRETIONARY_TEMPLATE"]])) 257 | for (tag, length, newvalue) in tlv_tags: 258 | datalist.append(newvalue) 259 | return datalist 260 | 261 | 262 | def decodeOffsetDataObjects(tlv_data): 263 | offsets = [] 264 | for (tag, length, newvalue) in tlv_find_tag(tlv_data, 265 | TAG["OFFSET_DATA"]): 266 | offsets.append(stringtoint(newvalue)) 267 | return offsets 268 | 269 | 270 | def decodeTagList(tlv_data): 271 | taglist = [] 272 | for (t, l, data) in tlv_find_tag(tlv_data, TAG["TAG_LIST"]): 273 | while data: 274 | tag = data[0] 275 | data = data[1:] 276 | if (tag & 0x1F) == 0x1F: 277 | tag = (tag << 8) | data[0] 278 | while data[0] & 0x80 == 0x80: 279 | data = data[1:] 280 | tag = (tag << 8) | data[0] 281 | data = data[1:] 282 | taglist.append((tag, 0)) 283 | return taglist 284 | 285 | 286 | def decodeHeaderList(tlv_data): 287 | headerlist = [] 288 | for (t, l, data) in tlv_find_tag(tlv_data, TAG["HEADER_LIST"]): 289 | while data: 290 | tag = data[0] 291 | data = data[1:] 292 | if (tag & 0x1F) == 0x1F: 293 | tag = (tag << 8) | data[0] 294 | while data[0] & 0x80 == 0x80: 295 | data = data[1:] 296 | tag = (tag << 8) | data[0] 297 | data = data[1:] 298 | 299 | length = data[0] 300 | if length < 0x80: 301 | data = data[1:] 302 | elif length & 0x80 == 0x80: 303 | length_ = 0 304 | data = data[1:] 305 | for i in range(0, length & 0x7F): 306 | length_ = length_ * 256 + data[0] 307 | data = data[1:] 308 | length = length_ 309 | 310 | headerlist.append((tag, length)) 311 | return headerlist 312 | 313 | 314 | def decodeExtendedHeaderList(tlv_data): 315 | # TODO 316 | return [] 317 | 318 | 319 | def encodebertlvDatalist(tag, datalist): 320 | tlvlist = [] 321 | for data in datalist: 322 | tlvlist.append((tag, len(data), data)) 323 | return bertlv_pack(tlvlist) 324 | 325 | 326 | def encodeDiscretionaryDataObjects(datalist): 327 | return encodebertlvDatalist(TAG["DISCRETIONARY_DATA"], datalist) 328 | 329 | 330 | def encodeDataOffsetObjects(datalist): 331 | return encodebertlvDatalist(TAG["OFFSET_DATA"], datalist) 332 | -------------------------------------------------------------------------------- /virtualsmartcard/VirtualSmartcard.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Frank Morgner, Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | 20 | import atexit 21 | import errno 22 | import logging 23 | import socket 24 | import struct 25 | import sys 26 | import traceback 27 | from virtualsmartcard.ConstantDefinitions import MAX_EXTENDED_LE, MAX_SHORT_LE 28 | from virtualsmartcard.SWutils import SwError, SW 29 | from virtualsmartcard.SmartcardFilesystem import make_property 30 | from virtualsmartcard.utils import C_APDU, R_APDU, hexdump, inttostring 31 | from virtualsmartcard.CardGenerator import CardGenerator 32 | 33 | 34 | class SmartcardOS(object): 35 | """Base class for a smart card OS""" 36 | def getATR(self): 37 | """Returns the ATR of the card as string of characters""" 38 | return b"" 39 | 40 | def powerUp(self): 41 | """Powers up the card""" 42 | pass 43 | 44 | def powerDown(self): 45 | """Powers down the card""" 46 | pass 47 | 48 | def reset(self): 49 | """Performs a warm reset of the card (no power down)""" 50 | pass 51 | 52 | def execute(self, msg): 53 | """Returns response to the given APDU as string of characters 54 | 55 | :param msg: the APDU as string of characters 56 | """ 57 | return b"" 58 | 59 | 60 | class Iso7816OS(SmartcardOS): 61 | 62 | mf = make_property("mf", "master file") 63 | SAM = make_property("SAM", "secure access module") 64 | 65 | def __init__(self, mf, sam, ins2handler=None, extended_length=False): 66 | self.mf = mf 67 | self.SAM = sam 68 | 69 | if not ins2handler: 70 | self.ins2handler = { 71 | 0x0c: self.mf.eraseRecord, 72 | 0x0e: self.mf.eraseBinaryPlain, 73 | 0x0f: self.mf.eraseBinaryEncapsulated, 74 | 0x2a: self.SAM.perform_security_operation, 75 | 0x20: self.SAM.verify, 76 | 0x22: self.SAM.manage_security_environment, 77 | 0x24: self.SAM.change_reference_data, 78 | 0x46: self.SAM.generate_public_key_pair, 79 | 0x82: self.SAM.external_authenticate, 80 | 0x84: self.SAM.get_challenge, 81 | 0x88: self.SAM.internal_authenticate, 82 | 0xa0: self.mf.searchBinaryPlain, 83 | 0xa1: self.mf.searchBinaryEncapsulated, 84 | 0xa4: self.mf.selectFile, 85 | 0xb0: self.mf.readBinaryPlain, 86 | 0xb1: self.mf.readBinaryEncapsulated, 87 | 0xb2: self.mf.readRecordPlain, 88 | 0xb3: self.mf.readRecordEncapsulated, 89 | 0xc0: self.getResponse, 90 | 0xca: self.mf.getDataPlain, 91 | 0xcb: self.mf.getDataEncapsulated, 92 | 0xd0: self.mf.writeBinaryPlain, 93 | 0xd1: self.mf.writeBinaryEncapsulated, 94 | 0xd2: self.mf.writeRecord, 95 | 0xd6: self.mf.updateBinaryPlain, 96 | 0xd7: self.mf.updateBinaryEncapsulated, 97 | 0xda: self.mf.putDataPlain, 98 | 0xdb: self.mf.putDataEncapsulated, 99 | 0xdc: self.mf.updateRecordPlain, 100 | 0xdd: self.mf.updateRecordEncapsulated, 101 | 0xe0: self.mf.createFile, 102 | 0xe2: self.mf.appendRecord, 103 | 0xe4: self.mf.deleteFile, 104 | } 105 | else: 106 | self.ins2handler = ins2handler 107 | 108 | if extended_length: 109 | self.maxle = MAX_EXTENDED_LE 110 | else: 111 | self.maxle = MAX_SHORT_LE 112 | 113 | self.lastCommandOffcut = b"" 114 | self.lastCommandSW = SW["NORMAL"] 115 | el = extended_length # only needed to keep following line short 116 | tsft = Iso7816OS.makeThirdSoftwareFunctionTable(extendedLe=el) 117 | card_capabilities = self.mf.firstSFT + self.mf.secondSFT + tsft 118 | self.atr = Iso7816OS.makeATR(T=1, directConvention=True, TA1=0x13, 119 | histChars=inttostring(0x80) + 120 | inttostring(0x70 + len(card_capabilities)) + 121 | card_capabilities) 122 | 123 | def getATR(self): 124 | return self.atr 125 | 126 | @staticmethod 127 | def makeATR(**args): 128 | """Calculate Answer to Reset (ATR) and returns the bitstring. 129 | 130 | - directConvention (bool): Whether to use direct convention or 131 | inverse convention. 132 | - TAi, TBi, TCi (optional): Value between 0 and 0xff. Interface 133 | Characters (for meaning see ISO 7816-3). Note that 134 | if no transmission protocol is given, it is 135 | automatically selected with T=max{j-1|TAj in args 136 | OR TBj in args OR TCj in args}. 137 | - T (optional): Value between 0 and 15. Transmission Protocol. 138 | Note that if T is set, TAi/TBi/TCi for i>T are 139 | omitted. 140 | - histChars (optional): Bitstring with 0 <= len(histChars) <= 15. 141 | Historical Characters T1 to T15 (for 142 | meaning see ISO 7816-4). 143 | 144 | T0, TDi and TCK are automatically calculated. 145 | """ 146 | # first byte TS 147 | if args["directConvention"]: 148 | atr = b"\x3b" 149 | else: 150 | atr = b"\x3f" 151 | 152 | if "T" in args: 153 | T = args["T"] 154 | else: 155 | T = 0 156 | 157 | # find maximum i of TAi/TBi/TCi in args 158 | maxTD = 0 159 | i = 15 160 | while i > 0: 161 | if ("TA" + str(i) in args or "TB" + str(i) in args or 162 | "TC" + str(i) in args): 163 | maxTD = i-1 164 | break 165 | i -= 1 166 | 167 | if maxTD == 0 and T > 0: 168 | maxTD = 2 169 | 170 | # insert TDi into args (TD0 is actually T0) 171 | for i in range(0, maxTD+1): 172 | if i == 0 and "histChars" in args: 173 | args["TD0"] = len(args["histChars"]) 174 | else: 175 | args["TD"+str(i)] = T 176 | 177 | if i < maxTD: 178 | args["TD"+str(i)] |= 1 << 7 179 | 180 | if "TA" + str(i+1) in args: 181 | args["TD"+str(i)] |= 1 << 4 182 | if "TB" + str(i+1) in args: 183 | args["TD"+str(i)] |= 1 << 5 184 | if "TC" + str(i+1) in args: 185 | args["TD"+str(i)] |= 1 << 6 186 | 187 | # initialize checksum 188 | TCK = 0 189 | 190 | # add TDi, TAi, TBi and TCi to ATR (TD0 is actually T0) 191 | for i in range(0, maxTD+1): 192 | atr = atr + b"%c" % args["TD" + str(i)] 193 | TCK ^= args["TD" + str(i)] 194 | for j in ["A", "B", "C"]: 195 | if "T" + j + str(i+1) in args: 196 | atr += b"%c" % args["T" + j + str(i+1)] 197 | # calculate checksum for all bytes from T0 to the end 198 | TCK ^= args["T" + j + str(i+1)] 199 | 200 | # add historical characters 201 | if "histChars" in args: 202 | atr += args["histChars"] 203 | for i in range(0, len(args["histChars"])): 204 | byte = args["histChars"][i] 205 | if isinstance(byte, str): 206 | TCK ^= ord(byte) 207 | else: 208 | TCK ^= byte 209 | 210 | # checksum is omitted for T=0 211 | if T > 0: 212 | atr += b"%c" % TCK 213 | 214 | return atr 215 | 216 | @staticmethod 217 | def makeThirdSoftwareFunctionTable(commandChainging=False, 218 | extendedLe=False, 219 | assignLogicalChannel=0, 220 | maximumChannels=0): 221 | """ 222 | Returns a byte according to the third software function table from the 223 | historical bytes of the card capabilities. 224 | """ 225 | tsft = 0 226 | if commandChainging: 227 | tsft |= 1 << 7 228 | if extendedLe: 229 | tsft |= 1 << 6 230 | if assignLogicalChannel: 231 | if not (0 <= assignLogicalChannel and assignLogicalChannel <= 3): 232 | raise ValueError 233 | tsft |= assignLogicalChannel << 3 234 | if maximumChannels: 235 | if not (0 <= maximumChannels and maximumChannels <= 7): 236 | raise ValueError 237 | tsft |= maximumChannels 238 | return inttostring(tsft) 239 | 240 | def formatResult(self, seekable, le, data, sw, sm): 241 | if not seekable: 242 | self.lastCommandOffcut = data[le:] 243 | l = len(self.lastCommandOffcut) 244 | if l == 0: 245 | self.lastCommandSW = SW["NORMAL"] 246 | else: 247 | self.lastCommandSW = sw 248 | sw = SW["NORMAL_REST"] + min(0xff, l) 249 | else: 250 | if le > len(data): 251 | sw = SW["WARN_EOFBEFORENEREAD"] 252 | 253 | if le is not None: 254 | result = data[:le] 255 | else: 256 | result = data[:0] 257 | if sm: 258 | sw, result = self.SAM.protect_result(sw, result) 259 | 260 | return R_APDU(result, inttostring(sw)).render() 261 | 262 | @staticmethod 263 | def seekable(ins): 264 | if ins in [0xb0, 0xb1, 0xd0, 0xd1, 0xd6, 0xd7, 0xa0, 0xa1, 0xb2, 0xb3, 265 | 0xdc, 0xdd]: 266 | return True 267 | else: 268 | return False 269 | 270 | def getResponse(self, p1, p2, data): 271 | if not (p1 == 0 and p2 == 0): 272 | raise SwError(SW["ERR_INCORRECTP1P2"]) 273 | 274 | return self.lastCommandSW, self.lastCommandOffcut 275 | 276 | def execute(self, msg): 277 | def notImplemented(*argz, **args): 278 | """ 279 | If an application tries to use a function which is not implemented 280 | by the currently emulated smartcard we raise an exception which 281 | should result in an appropriate response APDU being passed to the 282 | application. 283 | """ 284 | raise SwError(SW["ERR_INSNOTSUPPORTED"]) 285 | 286 | logging.info("Command APDU (%d bytes):\n %s", len(msg), 287 | hexdump(msg, indent=2)) 288 | 289 | try: 290 | c = C_APDU(msg) 291 | logging.debug("%s", str(c)) 292 | except ValueError as e: 293 | logging.warning(str(e)) 294 | return self.formatResult(False, 0, b"", 295 | SW["ERR_INCORRECTPARAMETERS"], False) 296 | 297 | # Handle Class Byte 298 | # {{{ 299 | class_byte = c.cla 300 | SM_STATUS = None 301 | logical_channel = 0 302 | command_chaining = 0 303 | header_authentication = 0 304 | 305 | # Ugly Hack for OpenSC-explorer 306 | if(class_byte == 0xb0): 307 | logging.debug("Open SC APDU") 308 | SM_STATUS = "No SM" 309 | 310 | # If Bit 8,7,6 == 0 then first industry values are used 311 | if (class_byte & 0xE0 == 0x00): 312 | # Bit 1 and 2 specify the logical channel 313 | logical_channel = class_byte & 0x03 314 | # Bit 3 and 4 specify secure messaging 315 | secure_messaging = class_byte >> 2 316 | secure_messaging &= 0x03 317 | if (secure_messaging == 0x00): 318 | SM_STATUS = "No SM" 319 | elif (secure_messaging == 0x01): 320 | SM_STATUS = "Proprietary SM" # Not supported ? 321 | elif (secure_messaging == 0x02): 322 | SM_STATUS = "Standard SM" 323 | elif (secure_messaging == 0x03): 324 | SM_STATUS = "Standard SM" 325 | header_authentication = 1 326 | # If Bit 8,7 == 01 then further industry values are used 327 | elif (class_byte & 0x0C == 0x0C): 328 | # Bit 1 to 4 specify logical channel. 4 is added, value range is 329 | # from four to nineteen 330 | logical_channel = class_byte & 0x0f 331 | logical_channel += 4 332 | # Bit 6 indicates secure messaging 333 | secure_messaging = class_byte >> 6 334 | if (secure_messaging == 0x00): 335 | SM_STATUS = "No SM" 336 | elif (secure_messaging == 0x01): 337 | SM_STATUS = "Standard SM" 338 | else: 339 | # Bit 8 is set to 1, which is not specified by ISO 7816-4 340 | SM_STATUS = "Proprietary SM" 341 | # In both cases Bit 5 specifies command chaining 342 | command_chaining = class_byte >> 5 343 | command_chaining &= 0x01 344 | # }}} 345 | 346 | sm = False 347 | try: 348 | if SM_STATUS == "Standard SM" or SM_STATUS == "Proprietary SM": 349 | c = self.SAM.parse_SM_CAPDU(c, header_authentication) 350 | logging.info("Decrypted APDU:\n%s", str(c)) 351 | sm = True 352 | sw, result = self.ins2handler.get(c.ins, notImplemented)(c.p1, 353 | c.p2, 354 | c.data) 355 | answer = self.formatResult(Iso7816OS.seekable(c.ins), 356 | c.effective_Le, result, sw, sm) 357 | except SwError as e: 358 | logging.debug(traceback.format_exc().rstrip()) 359 | logging.info(e.message) 360 | sw = e.sw 361 | result = b"" 362 | answer = self.formatResult(False, 0, result, sw, sm) 363 | 364 | return answer 365 | 366 | def powerUp(self): 367 | self.mf.current = self.mf 368 | 369 | def reset(self): 370 | self.mf.current = self.mf 371 | 372 | 373 | def loadMitMFromPath(path): 374 | from importlib import import_module 375 | from pathlib import Path 376 | def onNth(tup,ind,func): 377 | start = tup[0:ind] + (func(tup[ind]),) 378 | return start if ind + 1 == 0 else start + tup[ind+1:] 379 | 380 | def pathToModuleName(path): 381 | return ".".join(onNth(Path(path).parts,-1,lambda fName : str(Path(fName).stem))) 382 | 383 | mitmModule = import_module(pathToModuleName(path)) 384 | 385 | # Sanity Checks 386 | if not getattr(mitmModule,"get_MitM",None): 387 | raise ValueError("The provided module does not contain a 'get_MitM' method.") 388 | mitm = mitmModule.get_MitM() 389 | if not getattr(mitm,"handleInPDU",None): 390 | raise ValueError("The produced MitM has no 'handleInPDU' method.") 391 | if not getattr(mitm,"handleOutPDU",None): 392 | raise ValueError("The produced MitM has no 'handleOutPDU' method.") 393 | 394 | return mitm 395 | 396 | 397 | # sizeof(int) taken from asizof-package {{{ 398 | _Csizeof_short = len(struct.pack('h', 0)) 399 | # }}} 400 | 401 | 402 | VPCD_CTRL_LEN = 1 403 | VPCD_CTRL_OFF = 0 404 | VPCD_CTRL_ON = 1 405 | VPCD_CTRL_RESET = 2 406 | VPCD_CTRL_ATR = 4 407 | 408 | 409 | class VirtualICC(object): 410 | """ 411 | This class is responsible for maintaining the communication of the virtual 412 | PCD and the emulated smartcard. vpicc and vpcd communicate via a socket. 413 | The vpcd sends command APDUs (which it receives from an application) to the 414 | vicc. The vicc passes these CAPDUs on to an emulated smartcard, which 415 | produces a response APDU. This RAPDU is then passed back by the vicc to 416 | the vpcd, which forwards it to the application. 417 | """ 418 | 419 | def __init__(self, datasetfile, card_type, host, port, 420 | readernum=None, mitmPath=None, ef_cardsecurity=None, ef_cardaccess=None, 421 | ca_key=None, cvca=None, disable_checks=False, esign_key=None, 422 | esign_ca_cert=None, esign_cert=None, 423 | logginglevel=logging.INFO): 424 | from os.path import exists 425 | 426 | logging.basicConfig(level=logginglevel, 427 | format="%(asctime)s [%(levelname)s] %(message)s", 428 | datefmt="%d.%m.%Y %H:%M:%S") 429 | 430 | self.cardGenerator = CardGenerator(card_type) 431 | 432 | # If a dataset file is specified, read the card's data groups from disk 433 | if datasetfile is not None: 434 | if exists(datasetfile): 435 | logging.info("Reading Data Groups from file %s.", 436 | datasetfile) 437 | self.cardGenerator.readDatagroups(datasetfile) 438 | 439 | MF, SAM = self.cardGenerator.getCard() 440 | 441 | # Generate an OS object of the correct card_type 442 | if card_type == "iso7816" or card_type == "ePass": 443 | self.os = Iso7816OS(MF, SAM) 444 | elif card_type == "nPA": 445 | from virtualsmartcard.cards.nPA import NPAOS 446 | self.os = NPAOS(MF, SAM, ef_cardsecurity=ef_cardsecurity, 447 | ef_cardaccess=ef_cardaccess, ca_key=ca_key, 448 | cvca=cvca, disable_checks=disable_checks, 449 | esign_key=esign_key, esign_ca_cert=esign_ca_cert, 450 | esign_cert=esign_cert) 451 | elif card_type == "cryptoflex": 452 | from virtualsmartcard.cards.cryptoflex import CryptoflexOS 453 | self.os = CryptoflexOS(MF, SAM) 454 | elif card_type == "relay": 455 | from virtualsmartcard.cards.Relay import RelayOS 456 | from virtualsmartcard.cards.RelayMiddleman import RelayMiddleman 457 | mitm = loadMitMFromPath(mitmPath) if mitmPath else RelayMiddleman() 458 | self.os = RelayOS(readernum,mitm=mitm) 459 | elif card_type == "handler_test": 460 | from virtualsmartcard.cards.HandlerTest import HandlerTestOS 461 | self.os = HandlerTestOS() 462 | else: 463 | logging.warning("Unknown cardtype %s. Will use standard card_type \ 464 | (ISO 7816)", card_type) 465 | card_type = "iso7816" 466 | self.os = Iso7816OS(MF, SAM) 467 | self.type = card_type 468 | 469 | # Connect to the VPCD 470 | self.host = host 471 | self.port = port 472 | if host: 473 | # use normal connection mode 474 | try: 475 | self.sock = self.connectToPort(host, port) 476 | self.sock.settimeout(None) 477 | self.server_sock = None 478 | except socket.error as e: 479 | logging.error("Failed to open socket: %s", str(e)) 480 | logging.error("Is pcscd running at %s? Is vpcd loaded? Is a \ 481 | firewall blocking port %u?", host, port) 482 | sys.exit() 483 | else: 484 | # use reversed connection mode 485 | try: 486 | local_ip = [(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1] 487 | custom_url = 'vicc://%s:%d' % (local_ip, port) 488 | print('VICC hostname: %s' % local_ip) 489 | print('VICC port: %d' % port) 490 | print('On your NFC phone with the Android Smart Card Emulator app scan this code:') 491 | try: 492 | import qrcode 493 | qr = qrcode.QRCode() 494 | qr.add_data(custom_url) 495 | qr.print_ascii() 496 | except ImportError: 497 | print('https://api.qrserver.com/v1/create-qr-code/?data=%s' % custom_url) 498 | (self.sock, self.server_sock, host) = self.openPort(port) 499 | self.sock.settimeout(None) 500 | except socket.error as e: 501 | logging.error("Failed to open socket: %s", str(e)) 502 | logging.error("Is pcscd running? Is vpcd loaded and in \ 503 | reversed connection mode? Is a firewall \ 504 | blocking port %u?", port) 505 | sys.exit() 506 | 507 | logging.info("Connected to virtual PCD at %s:%u", host, port) 508 | 509 | atexit.register(self.stop) 510 | 511 | @staticmethod 512 | def connectToPort(host, port): 513 | """ 514 | Open a connection to a given host on a given port. 515 | """ 516 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 517 | sock.connect((host, port)) 518 | return sock 519 | 520 | @staticmethod 521 | def openPort(port): 522 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 523 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 524 | server_socket.bind(('', port)) 525 | server_socket.listen(0) 526 | logging.info("Waiting for vpcd on port " + str(port)) 527 | (client_socket, address) = server_socket.accept() 528 | return (client_socket, server_socket, address[0]) 529 | 530 | def __sendToVPICC(self, msg): 531 | """ Send a message to the vpcd """ 532 | if isinstance(msg, str): 533 | self.sock.sendall(struct.pack('!H', len(msg)) + bytes(map(ord,msg))) 534 | else: 535 | self.sock.sendall(struct.pack('!H', len(msg)) + msg) 536 | 537 | def __recvFromVPICC(self): 538 | """ Receive a message from the vpcd """ 539 | # receive message size 540 | while True: 541 | try: 542 | sizestr = self.sock.recv(_Csizeof_short) 543 | except socket.error as e: 544 | if e.errno == errno.EINTR: 545 | continue 546 | break 547 | if len(sizestr) == 0: 548 | logging.info("Virtual PCD shut down") 549 | raise socket.error 550 | size = struct.unpack('!H', sizestr)[0] 551 | 552 | # receive and return message 553 | if size: 554 | while True: 555 | try: 556 | msg = self.sock.recv(size) 557 | except socket.error as e: 558 | if e.errno == errno.EINTR: 559 | continue 560 | break 561 | if len(msg) == 0: 562 | logging.info("Virtual PCD shut down") 563 | raise socket.error 564 | else: 565 | msg = None 566 | 567 | return size, msg 568 | 569 | def run(self): 570 | """ 571 | Main loop of the vpicc. Receives command APDUs via a socket from the 572 | vpcd, dispatches them to the emulated smartcard and sends the resulting 573 | respsonse APDU back to the vpcd. 574 | """ 575 | while True: 576 | try: 577 | (size, msg) = self.__recvFromVPICC() 578 | except socket.error as e: 579 | if not self.host: 580 | logging.info("Waiting for vpcd on port " + str(self.port)) 581 | (self.sock, address) = self.server_sock.accept() 582 | continue 583 | else: 584 | sys.exit() 585 | 586 | if not size: 587 | logging.warning("Error in communication protocol (missing \ 588 | size parameter)") 589 | elif size == VPCD_CTRL_LEN: 590 | if msg == inttostring(VPCD_CTRL_OFF): 591 | logging.info("Power Down") 592 | self.os.powerDown() 593 | elif msg == inttostring(VPCD_CTRL_ON): 594 | logging.info("Power Up") 595 | self.os.powerUp() 596 | elif msg == inttostring(VPCD_CTRL_RESET): 597 | logging.info("Reset") 598 | self.os.reset() 599 | elif msg == inttostring(VPCD_CTRL_ATR): 600 | self.__sendToVPICC(self.os.getATR()) 601 | else: 602 | logging.warning("unknown control command") 603 | else: 604 | if size != len(msg): 605 | logging.warning("Expected %u bytes, but received only %u", 606 | size, len(msg)) 607 | 608 | answer = self.os.execute(msg) 609 | logging.info("Response APDU (%d bytes):\n %s\n", len(answer), 610 | hexdump(answer, indent=2)) 611 | self.__sendToVPICC(answer) 612 | 613 | def stop(self): 614 | self.sock.close() 615 | if self.server_sock: 616 | self.server_sock.close() 617 | -------------------------------------------------------------------------------- /virtualsmartcard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fluepke/konnektor-patch/1e6a5bec11a09875366d6bbaf9bef38c942b9807/virtualsmartcard/__init__.py -------------------------------------------------------------------------------- /virtualsmartcard/cards/Relay.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2015 Frank Morgner 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | 20 | import atexit 21 | import logging 22 | import smartcard 23 | import sys 24 | from virtualsmartcard.VirtualSmartcard import SmartcardOS 25 | from virtualsmartcard.cards.RelayMiddleman import RelayMiddleman 26 | 27 | 28 | class RelayOS(SmartcardOS): 29 | """ 30 | This class implements relaying of a (physical) smartcard. The RelayOS 31 | forwards the command APDUs received from the vpcd to the real smartcard via 32 | an actual smart card reader and sends the responses back to the vpcd. 33 | This class can be used to implement relay or MitM attacks. 34 | """ 35 | def __init__(self, readernum, mitm=RelayMiddleman()): 36 | """ 37 | Initialize the connection to the (physical) smart card via a given 38 | reader 39 | """ 40 | 41 | # See which readers are available 42 | readers = smartcard.System.listReaders() 43 | if len(readers) <= readernum: 44 | logging.error("Invalid number of reader '%u' (only %u available)", 45 | readernum, len(readers)) 46 | sys.exit() 47 | 48 | # Connect to the reader and its card 49 | # XXX this is a workaround, see on sourceforge bug #3083254 50 | # should better use 51 | # self.reader = smartcard.System.readers()[readernum] 52 | self.reader = readers[readernum] 53 | try: 54 | self.session = smartcard.Session(self.reader) 55 | except smartcard.Exceptions.CardConnectionException as e: 56 | logging.error("Error connecting to card: %s", e.message) 57 | sys.exit() 58 | 59 | logging.info("Connected to card in '%s'", self.reader) 60 | 61 | self.mitm = mitm 62 | 63 | atexit.register(self.cleanup) 64 | 65 | def cleanup(self): 66 | """ 67 | Close the connection to the physical card 68 | """ 69 | try: 70 | self.session.close() 71 | except smartcard.Exceptions.CardConnectionException as e: 72 | logging.warning("Error disconnecting from card: %s", e.message) 73 | 74 | def getATR(self): 75 | # when powerDown has been called, fetching the ATR will throw an error. 76 | # In this case we must try to reconnect (and then get the ATR). 77 | try: 78 | atr = self.session.getATR() 79 | except smartcard.Exceptions.CardConnectionException as e: 80 | try: 81 | # Try to reconnect to the card 82 | self.session.close() 83 | self.session = smartcard.Session(self.reader) 84 | atr = self.session.getATR() 85 | except smartcard.Exceptions.CardConnectionException as e: 86 | logging.error("Error getting ATR: %s", e.message) 87 | sys.exit() 88 | 89 | return "".join([chr(b) for b in atr]) 90 | 91 | def powerUp(self): 92 | # When powerUp is called multiple times the session is valid (and the 93 | # card is implicitly powered) we can check for an ATR. But when 94 | # powerDown has been called, the session gets lost. In this case we 95 | # must try to reconnect (and power the card). 96 | try: 97 | self.session.getATR() 98 | except smartcard.Exceptions.CardConnectionException as e: 99 | try: 100 | self.session = smartcard.Session(self.reader) 101 | except smartcard.Exceptions.CardConnectionException as e: 102 | logging.error("Error connecting to card: %s", e.message) 103 | sys.exit() 104 | 105 | def powerDown(self): 106 | # There is no power down in the session context so we simply 107 | # disconnect, which should implicitly power down the card. 108 | try: 109 | self.session.close() 110 | except smartcard.Exceptions.CardConnectionException as e: 111 | logging.error("Error disconnecting from card: %s", str(e)) 112 | sys.exit() 113 | 114 | def reset(self): 115 | self.powerDown() 116 | self.powerUp() 117 | 118 | def execute(self, msg): 119 | # sendCommandAPDU() expects a list of APDU bytes 120 | if isinstance(msg,str): 121 | apdu = map(ord, msg) 122 | else: 123 | apdu = list(msg) 124 | 125 | apdu = self.mitm.handleInPDU(apdu) 126 | 127 | try: 128 | rapdu, sw1, sw2 = self.session.sendCommandAPDU(apdu) 129 | except smartcard.Exceptions.CardConnectionException as e: 130 | logging.error("Error transmitting APDU: %s", str(e)) 131 | sys.exit() 132 | 133 | # XXX this is a workaround, see on sourceforge bug #3083586 134 | # should better use 135 | # rapdu = rapdu + [sw1, sw2] 136 | if rapdu[-2:] == [sw1, sw2]: 137 | pass 138 | else: 139 | rapdu = rapdu + [sw1, sw2] 140 | 141 | rapdu = self.mitm.handleOutPDU(rapdu) 142 | 143 | # return the response APDU as string 144 | return "".join(map(chr, rapdu)) 145 | -------------------------------------------------------------------------------- /virtualsmartcard/cards/RelayMiddleman.py: -------------------------------------------------------------------------------- 1 | class RelayMiddleman(object): 2 | """ 3 | The RelayMiddleman class serves as a base from which a user might derive 4 | their own relay middle man class. This base class implements the simplest 5 | Man-in-the-Middle: the NoOp. 6 | """ 7 | 8 | def handleInPDU(self, inPDU: bytes): 9 | """ 10 | This method is called on each PDU that is fed into the realy (vdpu -> vicc). 11 | It may be overwritten to modify the packages send from the terminal to the 12 | real smart card. 13 | """ 14 | return inPDU 15 | 16 | def handleOutPDU(self, outPDU: bytes): 17 | """ 18 | This method is called on each PDU that is produced by the relay (vicc -> vdpu). 19 | It may be overwritten to modify the packages send from the real smart card to the 20 | terminal. 21 | """ 22 | return outPDU 23 | -------------------------------------------------------------------------------- /virtualsmartcard/cards/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fluepke/konnektor-patch/1e6a5bec11a09875366d6bbaf9bef38c942b9807/virtualsmartcard/cards/__init__.py -------------------------------------------------------------------------------- /virtualsmartcard/cards/cryptoflex.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | 19 | from virtualsmartcard.SmartcardSAM import SAM 20 | from virtualsmartcard.SEutils import Security_Environment 21 | from virtualsmartcard.SWutils import SwError, SW 22 | from virtualsmartcard.utils import inttostring, stringtoint, C_APDU, R_APDU 23 | from virtualsmartcard import TLVutils 24 | from virtualsmartcard.VirtualSmartcard import Iso7816OS 25 | 26 | from virtualsmartcard.SmartcardFilesystem import MF, TransparentStructureEF, \ 27 | RecordStructureEF, DF, EF 28 | from virtualsmartcard.ConstantDefinitions import FDB, MAX_SHORT_LE 29 | 30 | import struct 31 | import logging 32 | 33 | 34 | class CryptoflexOS(Iso7816OS): 35 | def __init__(self, mf, sam, ins2handler=None, maxle=MAX_SHORT_LE): 36 | Iso7816OS.__init__(self, mf, sam, ins2handler, maxle) 37 | self.atr = '\x3B\xE2\x00\x00\x40\x20\x49\x06' 38 | 39 | def execute(self, msg): 40 | def notImplemented(*argz, **args): 41 | raise SwError(SW["ERR_INSNOTSUPPORTED"]) 42 | 43 | try: 44 | c = C_APDU(msg) 45 | except ValueError as e: 46 | logging.debug("Failed to parse APDU %s", msg) 47 | return self.formatResult(False, 0, 0, "", 48 | SW["ERR_INCORRECTPARAMETERS"]) 49 | 50 | try: 51 | sw, result = self.ins2handler.get(c.ins, notImplemented)(c.p1, 52 | c.p2, 53 | c.data) 54 | # print type(result) 55 | except SwError as e: 56 | logging.info(e.message) 57 | # traceback.print_exception(*sys.exc_info()) 58 | sw = e.sw 59 | result = "" 60 | 61 | r = self.formatResult(c.ins, c.le, result, sw) 62 | return r 63 | 64 | def formatResult(self, ins, le, data, sw): 65 | if le == 0 and len(data): 66 | # cryptoflex does not inpterpret le==0 as maxle 67 | self.lastCommandSW = sw 68 | self.lastCommandOffcut = data 69 | r = R_APDU(inttostring(SW["ERR_WRONGLENGTH"] + 70 | min(0xff, len(data)))).render() 71 | else: 72 | if ins == 0xa4 and len(data): 73 | # get response should be followed by select file 74 | self.lastCommandSW = sw 75 | self.lastCommandOffcut = data 76 | r = R_APDU(inttostring(SW["NORMAL_REST"] + 77 | min(0xff, len(data)))).render() 78 | else: 79 | r = Iso7816OS.formatResult(self, Iso7816OS.seekable(ins), le, 80 | data, sw, False) 81 | 82 | return r 83 | 84 | 85 | class CryptoflexSE(Security_Environment): 86 | def __init__(self, MF, SAM): 87 | Security_Environment.__init__(self, MF, SAM) 88 | 89 | def generate_public_key_pair(self, p1, p2, data): 90 | """ 91 | In the Cryptoflex card this command only supports RSA keys. 92 | 93 | :param data: 94 | Contains the public exponent used for key generation 95 | :param p1: 96 | The key number. Can be used later to refer to the generated key 97 | :param p2: 98 | Used to specify the key length. The mapping is: 0x40 => 256 Bit, 99 | 0x60 => 512 Bit, 0x80 => 1024 100 | """ 101 | from Crypto.PublicKey import RSA 102 | from Crypto.Util.randpool import RandomPool 103 | 104 | keynumber = p1 # TODO: Check if key exists 105 | 106 | keylength_dict = {0x40: 256, 0x60: 512, 0x80: 1024} 107 | 108 | if p2 not in keylength_dict: 109 | raise SwError(SW["ERR_INCORRECTP1P2"]) 110 | else: 111 | keylength = keylength_dict[p2] 112 | 113 | rnd = RandomPool() 114 | PublicKey = RSA.generate(keylength, rnd.get_bytes) 115 | self.dst.key = PublicKey 116 | 117 | e_in = struct.unpack(" 16: 206 | args["maxrecordsize"] = stringtoint(data[16]) 207 | elif p2: 208 | # if given a number of records 209 | args["maxrecordsize"] = (stringtoint(data[2:4]) / p2) 210 | args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_FIXED_" 211 | "NOFURTHERINFO"] 212 | new_file = RecordStructureEF(**args) 213 | elif data[6] == "\x03": 214 | args["filedescriptor"] = FDB["EFSTRUCTURE_LINEAR_VARIABLE_" 215 | "NOFURTHERINFO"] 216 | new_file = RecordStructureEF(**args) 217 | elif data[6] == "\x04": 218 | args["filedescriptor"] = FDB["EFSTRUCTURE_CYCLIC_NOFURTHERINFO"] 219 | new_file = RecordStructureEF(**args) 220 | elif data[6] == "\x38": 221 | if data[12] != "\x03": 222 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 223 | new_file = DF(**args) 224 | else: 225 | logging.error("unknown type: 0x%x" % ord(data[6])) 226 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 227 | 228 | return [new_file] 229 | 230 | def recordHandlingDecode(self, p1, p2): 231 | if p2 < 4: 232 | p1 = 0 233 | 234 | return MF.recordHandlingDecode(self, p1, p2) 235 | 236 | def dataUnitsDecodePlain(self, p1, p2, data): 237 | ef = self.currentEF() 238 | # FIXME: Cyberflex Access TM Software Development Kit Release 3C 239 | # enforces the following: 240 | # offsets = [(p2 << 8) + p1] 241 | # but smartcard_login-0.1.1 requires: 242 | offsets = [(p1 << 8) + p2] 243 | 244 | datalist = [data] 245 | return ef, offsets, datalist 246 | 247 | def selectFile(self, p1, p2, data): 248 | """ 249 | Function for instruction 0xa4. Takes the parameter bytes 'p1', 'p2' as 250 | integers and 'data' as binary string. Returns the status bytes as two 251 | byte long integer and the response data as binary string. 252 | """ 253 | P2_FCI = 0 254 | P2_FCP = 1 << 2 255 | P2_FMD = 2 << 2 256 | P2_NONE = 3 << 2 257 | file = self._selectFile(p1, p2, data) 258 | 259 | if isinstance(file, EF): 260 | # File size (body only) 261 | size = inttostring(min(0xffff, len(file.getenc('data'))), 2) 262 | extra = bytes(0) # RFU 263 | if (isinstance(file, RecordStructureEF) and 264 | file.hasFixedRecordSize() and not file.isCyclic()): 265 | # Length of records 266 | extra += bytes(0) + bytes(min(file.records, 0xff)) 267 | elif isinstance(file, DF): 268 | # Number of unused EEPROM bytes available in the DF 269 | size = inttostring(0xffff, 2) 270 | efcount = 0 271 | dfcount = 0 272 | chv1 = None 273 | chv2 = None 274 | chv1lifecycle = 0 275 | chv2lifecycle = 0 276 | for f in self.content: 277 | if f.fid == 0x0000: 278 | chv1 = f 279 | chv1lifecycle = f.lifecycle & 1 280 | if f.fid == 0x0100: 281 | chv2 = f 282 | chv2lifecycle = f.lifecycle & 1 283 | if isinstance(f, EF): 284 | efcount += 1 285 | if isinstance(f, DF): 286 | dfcount += 1 287 | if chv1: 288 | extra = bytes(1) # TODO LSB correct? 289 | else: 290 | extra = bytes(0) # TODO LSB correct? 291 | extra += bytes(efcount) 292 | extra += bytes(dfcount) 293 | extra += bytes(0) # TODO Number of PINs and unblock CHV PINs 294 | extra += bytes(0) # RFU 295 | if chv1: 296 | extra += bytes(0) # TODO remaining CHV1 attempts 297 | extra += bytes(0) # TODO remaining unblock CHV1 attempts 298 | if chv2: 299 | extra += bytes(0) # TODO CHV2 key status 300 | extra += bytes(0) # TODO CHV2 unblocking key status 301 | 302 | data = inttostring(0, 2) # RFU 303 | data += size 304 | data += inttostring(file.fid, 2) 305 | data += inttostring(file.filedescriptor) # File type 306 | data += inttostring(0, 4) # ACs TODO 307 | data += bytes(file.lifecycle & 1) # File status 308 | data += bytes(len(extra)) 309 | data += extra 310 | 311 | self.current = file 312 | 313 | return SW["NORMAL"], data 314 | # }}} 315 | -------------------------------------------------------------------------------- /virtualsmartcard/cards/ePass.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Dominik Oepen 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | 19 | from virtualsmartcard.SmartcardSAM import SAM 20 | from virtualsmartcard.SEutils import Security_Environment 21 | import virtualsmartcard.CryptoUtils as vsCrypto 22 | from virtualsmartcard.SWutils import SwError, SW 23 | from virtualsmartcard.utils import stringtoint 24 | 25 | import hashlib 26 | import struct 27 | from os import urandom 28 | 29 | 30 | class ePass_SE(Security_Environment): 31 | """This class implements the Security Environment of the ICAO Passports. It 32 | is required in order to use the send sequence counter for secure messaging. 33 | """ 34 | def __init__(self, MF, SAM, ssc=None): 35 | Security_Environment.__init__(self, MF, SAM) 36 | self.ssc = ssc 37 | self.cct.algorithm = "CC" 38 | self.cct.blocklength = 8 39 | self.ct.algorithm = "DES3-CBC" 40 | 41 | def compute_cryptographic_checksum(self, p1, p2, data): 42 | """ 43 | Compute a cryptographic checksum (e.g. MAC) for the given data. 44 | The ePass uses a Send Sequence Counter for MAC calculation 45 | """ 46 | if p1 != 0x8E or p2 != 0x80: 47 | raise SwError(SW["ERR_INCORRECTP1P2"]) 48 | 49 | self.ssc += 1 50 | checksum = vsCrypto.crypto_checksum(self.cct.algorithm, self.cct.key, 51 | data, self.cct.iv, self.ssc) 52 | 53 | return checksum 54 | 55 | 56 | class PassportSAM(SAM): 57 | """ 58 | SAM for ICAO ePassport. Implements Basic access control and key derivation 59 | for Secure Messaging. 60 | """ 61 | def __init__(self, mf): 62 | import virtualsmartcard.SmartcardFilesystem as vsFS 63 | 64 | SAM.__init__(self, None, None, mf, default_se=ePass_SE) 65 | 66 | ef_dg1 = vsFS.walk(mf, "\x00\x04\x01\x01") 67 | dg1 = ef_dg1.readbinary(5) 68 | self.mrz1 = dg1[:43] 69 | self.mrz2 = dg1[44:] 70 | self.KSeed = None 71 | self.KEnc = None 72 | self.KMac = None 73 | self.__computeKeys() 74 | 75 | def __computeKeys(self): 76 | """ 77 | Computes the keys depending on the machine readable 78 | zone of the passport according to TR-PKI mrtds ICC read-only 79 | access v1.1 annex E.1. 80 | """ 81 | 82 | MRZ_information = self.mrz2[0:10] + self.mrz2[13:20] + self.mrz2[21:28] 83 | H = hashlib.sha1(MRZ_information).digest() # pylint: disable=E1101 84 | self.KSeed = H[:16] 85 | self.KEnc = self.derive_key(self.KSeed, 1) 86 | self.KMac = self.derive_key(self.KSeed, 2) 87 | 88 | @staticmethod 89 | def derive_key(seed, c): 90 | """ 91 | Derive a key according to TR-PKI mrtds ICC read-only access v1.1 92 | annex E.1. 93 | c is either 1 for encryption or 2 for MAC computation. 94 | Returns: Ka + Kb 95 | Note: Does not adjust parity. Nobody uses that anyway ...""" 96 | D = seed + struct.pack(">i", c) 97 | H = hashlib.sha1(D).digest() # pylint: disable=E1101 98 | Ka = H[0:8] 99 | Kb = H[8:16] 100 | return Ka + Kb 101 | 102 | def external_authenticate(self, p1, p2, resp_data): 103 | """Performs the basic access control protocol as defined in 104 | the ICAO MRTD standard""" 105 | rnd_icc = self.last_challenge 106 | 107 | # Receive Mutual Authenticate APDU from terminal 108 | # Decrypt data and check MAC 109 | Eifd = resp_data[:-8] 110 | padded_Eifd = vsCrypto.append_padding(self.current_SE.cct.blocklength, 111 | Eifd) 112 | Mifd = vsCrypto.crypto_checksum("CC", self.KMac, padded_Eifd) 113 | # Check the MAC 114 | if not Mifd == resp_data[-8:]: 115 | raise SwError(SW["ERR_SECMESSOBJECTSINCORRECT"]) 116 | # Decrypt the data 117 | plain = vsCrypto.decrypt("DES3-CBC", self.KEnc, resp_data[:-8]) 118 | if plain[8:16] != rnd_icc: 119 | raise SwError(SW["WARN_NOINFO63"]) 120 | # Extract keying material from IFD, generate ICC keying material 121 | Kifd = plain[16:] 122 | rnd_ifd = plain[:8] 123 | Kicc = urandom(16) 124 | # Generate Answer 125 | data = plain[8:16] + plain[:8] + Kicc 126 | Eicc = vsCrypto.encrypt("DES3-CBC", self.KEnc, data) 127 | padded_Eicc = vsCrypto.append_padding(self.current_SE.cct.blocklength, 128 | Eicc) 129 | Micc = vsCrypto.crypto_checksum("CC", self.KMac, padded_Eicc) 130 | # Derive the final keys and set the current SE 131 | KSseed = vsCrypto.operation_on_string(Kicc, Kifd, lambda a, b: a ^ b) 132 | self.current_SE.ct.key = self.derive_key(KSseed, 1) 133 | self.current_SE.cct.key = self.derive_key(KSseed, 2) 134 | self.current_SE.ssc = stringtoint(rnd_icc[-4:] + rnd_ifd[-4:]) 135 | return SW["NORMAL"], Eicc + Micc 136 | -------------------------------------------------------------------------------- /virtualsmartcard/cards/nPA.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2011 Dominik Oepen, Frank Morgner 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | 19 | from virtualsmartcard.SmartcardSAM import SAM 20 | from virtualsmartcard.VirtualSmartcard import Iso7816OS 21 | from virtualsmartcard.SEutils import ControlReferenceTemplate, \ 22 | Security_Environment 23 | from virtualsmartcard.SWutils import SwError, SW 24 | from virtualsmartcard.ConstantDefinitions import CRT_TEMPLATE, SM_Class, \ 25 | ALGO_MAPPING, MAX_EXTENDED_LE, MAX_SHORT_LE 26 | from virtualsmartcard.TLVutils import unpack, bertlv_pack, \ 27 | decodeDiscretionaryDataObjects, tlv_find_tag 28 | from virtualsmartcard.SmartcardFilesystem import make_property 29 | from virtualsmartcard.utils import inttostring, R_APDU 30 | import virtualsmartcard.CryptoUtils as vsCrypto 31 | from chat import CHAT, CVC, PACE_SEC, EAC_CTX 32 | import binascii 33 | import eac 34 | import logging 35 | import sys 36 | import traceback 37 | 38 | 39 | class NPAOS(Iso7816OS): 40 | def __init__(self, mf, sam, ins2handler=None, maxle=MAX_EXTENDED_LE, 41 | ef_cardsecurity=None, ef_cardaccess=None, ca_key=None, 42 | cvca=None, disable_checks=False, esign_key=None, 43 | esign_ca_cert=None, esign_cert=None): 44 | Iso7816OS.__init__(self, mf, sam, ins2handler, maxle) 45 | self.ins2handler[0x86] = self.SAM.general_authenticate 46 | self.ins2handler[0x2c] = self.SAM.reset_retry_counter 47 | 48 | # different ATR (Answer To Reset) values depending on used Chip version 49 | # It's just a playground, because in past one of all those eID clients 50 | # did not recognize the card correctly with newest ATR values 51 | self.atr = b'\x3B\x8A\x80\x01\x80\x31\xF8\x73\xF7\x41\xE0\x82\x90' + \ 52 | b'\x00\x75' 53 | # self.atr = b'\x3B\x8A\x80\x01\x80\x31\xB8\x73\x84\x01\xE0\x82\x90' + \ 54 | # b'\x00\x06' 55 | # self.atr = b'\x3B\x88\x80\x01\x00\x00\x00\x00\x00\x00\x00\x00\x09' 56 | # self.atr = b'\x3B\x87\x80\x01\x80\x31\xB8\x73\x84\x01\xE0\x19' 57 | 58 | self.SAM.current_SE.disable_checks = disable_checks 59 | if ef_cardsecurity: 60 | ef = self.mf.select('fid', 0x011d) 61 | ef.data = ef_cardsecurity 62 | if ef_cardaccess: 63 | ef = self.mf.select('fid', 0x011c) 64 | ef.data = ef_cardaccess 65 | if cvca: 66 | self.SAM.current_SE.cvca = cvca 67 | if ca_key: 68 | self.SAM.current_SE.ca_key = ca_key 69 | esign = self.mf.select('dfname', 70 | b'\xA0\x00\x00\x01\x67\x45\x53\x49\x47\x4E') 71 | if esign_ca_cert: 72 | ef = esign.select('fid', 0xC000) 73 | ef.data = esign_ca_cert 74 | if esign_cert: 75 | ef = esign.select('fid', 0xC001) 76 | ef.data = esign_cert 77 | 78 | def formatResult(self, seekable, le, data, sw, sm): 79 | if seekable: 80 | # when le = 0 then we want to have 0x9000. here we only have the 81 | # effective le, which is either MAX_EXTENDED_LE or MAX_SHORT_LE, 82 | # depending on the APDU. Note that the following distinguisher has 83 | # one false positive 84 | if le > len(data) and le != MAX_EXTENDED_LE and le != MAX_SHORT_LE: 85 | sw = SW["WARN_EOFBEFORENEREAD"] 86 | 87 | if le is not None: 88 | result = data[:le] 89 | else: 90 | result = data[:0] 91 | if sm: 92 | try: 93 | sw, result = self.SAM.protect_result(sw, result) 94 | except SwError as e: 95 | logging.debug(traceback.format_exc().rstrip()) 96 | logging.info(e.message) 97 | sw = e.sw 98 | result = b"" 99 | answer = self.formatResult(False, 0, result, sw, False) 100 | 101 | return R_APDU(result, inttostring(sw)).render() 102 | 103 | 104 | class nPA_AT_CRT(ControlReferenceTemplate): 105 | 106 | PACE_MRZ = 0x01 107 | PACE_CAN = 0x02 108 | PACE_PIN = 0x03 109 | PACE_PUK = 0x04 110 | 111 | def __init__(self): 112 | ControlReferenceTemplate.__init__(self, CRT_TEMPLATE["AT"]) 113 | self.chat = None 114 | DateOfBirth = None 115 | DateOfExpiry = None 116 | CommunityID = None 117 | 118 | def keyref_is_mrz(self): 119 | if self.keyref_secret_key == b'%c' % self.PACE_MRZ: 120 | return True 121 | return False 122 | 123 | def keyref_is_can(self): 124 | if self.keyref_secret_key == b'%c' % self.PACE_CAN: 125 | return True 126 | return False 127 | 128 | def keyref_is_pin(self): 129 | if self.keyref_secret_key == b'%c' % self.PACE_PIN: 130 | return True 131 | return False 132 | 133 | def keyref_is_puk(self): 134 | if self.keyref_secret_key == b'%c' % self.PACE_PUK: 135 | return True 136 | return False 137 | 138 | def parse_SE_config(self, config): 139 | r = 0x9000 140 | try: 141 | ControlReferenceTemplate.parse_SE_config(self, config) 142 | except SwError as e: 143 | structure = unpack(config) 144 | for tlv in structure: 145 | tag, length, value = tlv 146 | if tag == 0x7f4c: 147 | self.chat = CHAT(bertlv_pack([[tag, length, value]])) 148 | elif tag == 0x67: 149 | self.auxiliary_data = bertlv_pack([[tag, length, value]]) 150 | # extract reference values for verifying 151 | # DateOfBirth, DateOfExpiry and CommunityID 152 | for ddo in decodeDiscretionaryDataObjects(value): 153 | try: 154 | oidvalue = ddo[0][2] 155 | reference = ddo[1][2] 156 | mapped_algo = ALGO_MAPPING[oidvalue] 157 | if mapped_algo == "DateOfBirth": 158 | self.DateOfBirth = int(reference) 159 | logging.info("Found reference DateOfBirth: " + 160 | str(self.DateOfBirth)) 161 | elif mapped_algo == "DateOfExpiry": 162 | self.DateOfExpiry = int(reference) 163 | logging.info("Found reference DateOfExpiry: " + 164 | str(self.DateOfExpiry)) 165 | elif mapped_algo == "CommunityID": 166 | self.CommunityID = binascii.hexlify(reference) 167 | logging.info("Found reference CommunityID: " + 168 | str(self.CommunityID)) 169 | except: 170 | pass 171 | elif tag == 0x80 or tag == 0x84 or tag == 0x83 or tag == 0x91: 172 | # handled by ControlReferenceTemplate.parse_SE_config 173 | pass 174 | else: 175 | raise SwError(SW["ERR_REFNOTUSABLE"]) 176 | 177 | return r, b"" 178 | 179 | 180 | class nPA_SE(Security_Environment): 181 | 182 | eac_step = make_property("eac_step", "next step to performed for EAC") 183 | 184 | def __init__(self, MF, SAM): 185 | Security_Environment.__init__(self, MF, SAM) 186 | self.at = nPA_AT_CRT() 187 | # This breaks support for 3DES 188 | self.cct.blocklength = 16 189 | self.cct.algorithm = "CC" 190 | self.eac_step = 0 191 | self.sec = None 192 | self.eac_ctx = None 193 | self.cvca = None 194 | self.car = None 195 | self.ca_key = None 196 | self.disable_checks = False 197 | 198 | def _set_SE(self, p2, data): 199 | sw, resp = Security_Environment._set_SE(self, p2, data) 200 | 201 | if self.at.algorithm == "PACE": 202 | if self.at.keyref_is_pin(): 203 | if self.sam.counter <= 0: 204 | print("Must use PUK to unblock") 205 | return 0x63c0, "" 206 | if self.sam.counter == 1 and not self.sam.active: 207 | print("Must use CAN to activate") 208 | return 0x63c1, "" 209 | self.eac_step = 0 210 | elif self.at.algorithm == "TA": 211 | if self.eac_step != 4: 212 | raise SwError(SW["ERR_AUTHBLOCKED"]) 213 | elif self.at.algorithm == "CA": 214 | if self.eac_step != 5: 215 | raise SwError(SW["ERR_AUTHBLOCKED"]) 216 | 217 | return sw, resp 218 | 219 | def general_authenticate(self, p1, p2, data): 220 | if (p1, p2) != (0x00, 0x00): 221 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 222 | 223 | if self.eac_step == 0 and self.at.algorithm == "PACE": 224 | return self.__eac_pace_step1(data) 225 | elif self.eac_step == 1 and self.at.algorithm == "PACE": 226 | return self.__eac_pace_step2(data) 227 | elif self.eac_step == 2 and self.at.algorithm == "PACE": 228 | return self.__eac_pace_step3(data) 229 | elif self.eac_step == 3 and self.at.algorithm == "PACE": 230 | return self.__eac_pace_step4(data) 231 | elif self.eac_step == 5 and self.at.algorithm == "CA": 232 | return self.__eac_ca(data) 233 | elif self.eac_step == 6: 234 | # TODO implement RI 235 | # "\x7c\x22\x81\x20\" is some prefix and the rest is our RI 236 | return SW["NORMAL"], b"\x7c\x22\x81\x20\x48\x1e\x58\xd1\x7c\x12" + \ 237 | b"\x9a\x0a\xb4\x63\x7d\x43\xc7\xf7\xeb\x2b" + \ 238 | b"\x06\x10\x6f\x26\x90\xe3\x00\xc4\xe7\x03" + \ 239 | b"\x54\xa0\x41\xf0\xd3\x90" 240 | 241 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 242 | 243 | @staticmethod 244 | def __unpack_general_authenticate(data): 245 | data_structure = [] 246 | structure = unpack(data) 247 | for tlv in structure: 248 | tag, length, value = tlv 249 | if tag == 0x7c: 250 | data_structure = value 251 | else: 252 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 253 | return data_structure 254 | 255 | @staticmethod 256 | def __pack_general_authenticate(data): 257 | tlv_data = bertlv_pack(data) 258 | return bertlv_pack([[0x7c, len(tlv_data), tlv_data]]) 259 | 260 | def __eac_pace_step1(self, data): 261 | tlv_data = nPA_SE.__unpack_general_authenticate(data) 262 | if tlv_data != []: 263 | raise SwError(SW["WARN_NOINFO63"]) 264 | 265 | if self.at.keyref_is_mrz(): 266 | self.PACE_SEC = PACE_SEC(self.sam.mrz, eac.PACE_MRZ) 267 | elif self.at.keyref_is_can(): 268 | self.PACE_SEC = PACE_SEC(self.sam.can, eac.PACE_CAN) 269 | elif self.at.keyref_is_pin(): 270 | if self.sam.counter <= 0: 271 | print("Must use PUK to unblock") 272 | return 0x63c0, b"" 273 | if self.sam.counter == 1 and not self.sam.active: 274 | print("Must use CAN to activate") 275 | return 0x63c1, b"" 276 | self.PACE_SEC = PACE_SEC(self.sam.eid_pin, eac.PACE_PIN) 277 | self.sam.counter -= 1 278 | if self.sam.counter <= 1: 279 | self.sam.active = False 280 | elif self.at.keyref_is_puk(): 281 | if self.sam.counter_puk <= 0: 282 | raise SwError(SW["WARN_NOINFO63"]) 283 | self.PACE_SEC = PACE_SEC(self.sam.puk, eac.PACE_PUK) 284 | self.sam.counter_puk -= 1 285 | else: 286 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 287 | self.sec = self.PACE_SEC.sec 288 | 289 | if not self.eac_ctx: 290 | eac.EAC_init() 291 | 292 | self.EAC_CTX = EAC_CTX() 293 | self.eac_ctx = self.EAC_CTX.ctx 294 | eac.CA_disable_passive_authentication(self.eac_ctx) 295 | 296 | ef_card_security = self.mf.select('fid', 0x011d) 297 | ef_card_security_data = ef_card_security.data 298 | eac.EAC_CTX_init_ef_cardsecurity(ef_card_security_data, 299 | self.eac_ctx) 300 | 301 | if self.ca_key: 302 | ca_pubkey = eac.CA_get_pubkey(self.eac_ctx, 303 | ef_card_security_data) 304 | if 1 != eac.CA_set_key(self.eac_ctx, self.ca_key, ca_pubkey): 305 | eac.print_ossl_err() 306 | raise SwError(SW["WARN_NOINFO63"]) 307 | else: 308 | # we don't have a good CA key, so we simply generate an 309 | # ephemeral one 310 | comp_pubkey = eac.TA_STEP3_generate_ephemeral_key(self.eac_ctx) 311 | pubkey = eac.CA_STEP2_get_eph_pubkey(self.eac_ctx) 312 | if not comp_pubkey or not pubkey: 313 | eac.print_ossl_err() 314 | raise SwError(SW["WARN_NOINFO63"]) 315 | 316 | # save public key in EF.CardSecurity (and invalidate the 317 | # signature) 318 | # FIXME this only works for the default EF.CardSecurity. 319 | # Better use an ASN.1 parser to do this manipulation 320 | ef_card_security = self.mf.select('fid', 0x011d) 321 | ef_card_security_data = ef_card_security.data 322 | ef_card_security_data = \ 323 | ef_card_security_data[:61+4+239+2+1] + pubkey + \ 324 | ef_card_security_data[61+4+239+2+1+len(pubkey):] 325 | ef_card_security.data = ef_card_security_data 326 | 327 | nonce = eac.PACE_STEP1_enc_nonce(self.eac_ctx, self.sec) 328 | if not nonce: 329 | eac.print_ossl_err() 330 | raise SwError(SW["WARN_NOINFO63"]) 331 | 332 | resp = nPA_SE.__pack_general_authenticate([[0x80, len(nonce), nonce]]) 333 | 334 | self.eac_step += 1 335 | 336 | return 0x9000, resp 337 | 338 | def __eac_pace_step2(self, data): 339 | tlv_data = nPA_SE.__unpack_general_authenticate(data) 340 | 341 | pubkey = eac.PACE_STEP3A_generate_mapping_data(self.eac_ctx) 342 | if not pubkey: 343 | eac.print_ossl_err() 344 | raise SwError(SW["WARN_NOINFO63"]) 345 | 346 | for tag, length, value in tlv_data: 347 | if tag == 0x81: 348 | eac.PACE_STEP3A_map_generator(self.eac_ctx, value) 349 | else: 350 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 351 | 352 | self.eac_step += 1 353 | 354 | return 0x9000, \ 355 | nPA_SE.__pack_general_authenticate([[0x82, len(pubkey), pubkey]]) 356 | 357 | def __eac_pace_step3(self, data): 358 | tlv_data = nPA_SE.__unpack_general_authenticate(data) 359 | 360 | self.my_pace_eph_pubkey = \ 361 | eac.PACE_STEP3B_generate_ephemeral_key(self.eac_ctx) 362 | if not self.my_pace_eph_pubkey: 363 | eac.print_ossl_err() 364 | raise SwError(SW["WARN_NOINFO63"]) 365 | eph_pubkey = self.my_pace_eph_pubkey 366 | 367 | for tag, length, value in tlv_data: 368 | if tag == 0x83: 369 | self.pace_opp_pub_key = value 370 | eac.PACE_STEP3B_compute_shared_secret(self.eac_ctx, 371 | self.pace_opp_pub_key) 372 | else: 373 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 374 | 375 | self.eac_step += 1 376 | 377 | return 0x9000, \ 378 | nPA_SE.__pack_general_authenticate([[0x84, len(eph_pubkey), 379 | eph_pubkey]]) 380 | 381 | def __eac_pace_step4(self, data): 382 | tlv_data = nPA_SE.__unpack_general_authenticate(data) 383 | eac.PACE_STEP3C_derive_keys(self.eac_ctx) 384 | my_token = \ 385 | eac.PACE_STEP3D_compute_authentication_token(self.eac_ctx, 386 | self.pace_opp_pub_key) 387 | token = b"" 388 | for tag, length, value in tlv_data: 389 | if tag == 0x85: 390 | token = value 391 | else: 392 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 393 | 394 | ver = eac.PACE_STEP3D_verify_authentication_token(self.eac_ctx, token) 395 | if not my_token or ver != 1: 396 | eac.print_ossl_err() 397 | raise SwError(SW["WARN_NOINFO63"]) 398 | 399 | print("Established PACE channel") 400 | 401 | if self.at.keyref_is_can(): 402 | if (self.sam.counter == 1): 403 | self.sam.active = True 404 | print("PIN resumed") 405 | elif self.at.keyref_is_pin(): 406 | self.sam.active = True 407 | self.sam.counter = 3 408 | elif self.at.keyref_is_puk(): 409 | self.sam.active = True 410 | self.sam.counter = 3 411 | print("PIN unblocked") 412 | 413 | self.eac_step += 1 414 | self.at.algorithm = "TA" 415 | 416 | self.new_encryption_ctx = eac.EAC_ID_PACE 417 | 418 | result = [[0x86, len(my_token), my_token]] 419 | if self.at.chat: 420 | if self.cvca: 421 | self.car = CVC(self.cvca).get_chr() 422 | result.append([0x87, len(self.car), self.car]) 423 | if (self.disable_checks): 424 | eac.TA_disable_checks(self.eac_ctx) 425 | if not eac.EAC_CTX_init_ta(self.eac_ctx, None, self.cvca): 426 | eac.print_ossl_err() 427 | raise SwError(SW["WARN_NOINFO63"]) 428 | 429 | return 0x9000, nPA_SE.__pack_general_authenticate(result) 430 | 431 | def __eac_ca(self, data): 432 | tlv_data = nPA_SE.__unpack_general_authenticate(data) 433 | 434 | pubkey = "" 435 | for tag, length, value in tlv_data: 436 | if tag == 0x80: 437 | pubkey = value 438 | else: 439 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 440 | 441 | if eac.CA_STEP4_compute_shared_secret(self.eac_ctx, pubkey) != 1: 442 | eac.print_ossl_err() 443 | raise SwError(SW["ERR_NOINFO69"]) 444 | 445 | nonce, token = eac.CA_STEP5_derive_keys(self.eac_ctx, pubkey) 446 | if not nonce or not token: 447 | eac.print_ossl_err() 448 | raise SwError(SW["WARN_NOINFO63"]) 449 | 450 | self.eac_step += 1 451 | 452 | print("Generated Nonce and Authentication Token for CA") 453 | 454 | # TODO activate SM 455 | self.new_encryption_ctx = eac.EAC_ID_CA 456 | 457 | return 0x9000, \ 458 | nPA_SE.__pack_general_authenticate([[0x81, len(nonce), nonce], 459 | [0x82, len(token), token]]) 460 | 461 | def verify_certificate(self, p1, p2, data): 462 | if (p1, p2) != (0x00, 0xbe): 463 | raise SwError(SW["ERR_INCORRECTPARAMETERS"]) 464 | 465 | cert = bertlv_pack([[0x7f21, len(data), data]]) 466 | if 1 != eac.TA_STEP2_import_certificate(self.eac_ctx, cert): 467 | eac.print_ossl_err() 468 | raise SwError(SW["ERR_NOINFO69"]) 469 | 470 | print("Imported Certificate") 471 | 472 | return b"" 473 | 474 | def external_authenticate(self, p1, p2, data): 475 | """ 476 | Authenticate the terminal to the card. Check whether Terminal correctly 477 | encrypted the given challenge or not 478 | """ 479 | if self.dst.keyref_public_key: # TODO check if this is the correct CAR 480 | id_picc = eac.EAC_Comp(self.eac_ctx, eac.EAC_ID_PACE, 481 | self.my_pace_eph_pubkey) 482 | 483 | # FIXME auxiliary_data might be from an older run of PACE 484 | if hasattr(self.at, "auxiliary_data"): 485 | auxiliary_data = self.at.auxiliary_data 486 | else: 487 | auxiliary_data = None 488 | 489 | if 1 != eac.TA_STEP6_verify(self.eac_ctx, self.at.iv, id_picc, 490 | auxiliary_data, data): 491 | eac.print_ossl_err() 492 | print("Could not verify Terminal's signature") 493 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 494 | 495 | print("Terminal's signature verified") 496 | 497 | self.eac_step += 1 498 | 499 | return 0x9000, b"" 500 | 501 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 502 | 503 | def compute_cryptographic_checksum(self, p1, p2, data): 504 | checksum = eac.EAC_authenticate(self.eac_ctx, data) 505 | if not checksum: 506 | eac.print_ossl_err() 507 | raise SwError(SW["ERR_NOINFO69"]) 508 | 509 | return checksum 510 | 511 | def encipher(self, p1, p2, data): 512 | padded = vsCrypto.append_padding(self.cct.blocklength, data) 513 | cipher = eac.EAC_encrypt(self.eac_ctx, padded) 514 | if not cipher: 515 | eac.print_ossl_err() 516 | raise SwError(SW["ERR_NOINFO69"]) 517 | 518 | return cipher 519 | 520 | def decipher(self, p1, p2, data): 521 | plain = eac.EAC_decrypt(self.eac_ctx, data) 522 | if not plain: 523 | eac.print_ossl_err() 524 | raise SwError(SW["ERR_NOINFO69"]) 525 | 526 | return plain 527 | 528 | def protect_response(self, sw, result): 529 | """ 530 | This method protects a response APDU using secure messaging mechanisms 531 | 532 | :returns: the protected data and the SW bytes 533 | """ 534 | 535 | return_data = b"" 536 | 537 | if result: 538 | # Encrypt the data included in the RAPDU 539 | encrypted = self.encipher(0x82, 0x80, result) 540 | encrypted = b"\x01" + encrypted 541 | encrypted_tlv = bertlv_pack([( 542 | SM_Class["CRYPTOGRAM_PADDING_INDICATOR_ODD"], 543 | len(encrypted), 544 | encrypted)]) 545 | return_data += encrypted_tlv 546 | 547 | sw_str = inttostring(sw) 548 | length = len(sw_str) 549 | tag = SM_Class["PLAIN_PROCESSING_STATUS"] 550 | tlv_sw = bertlv_pack([(tag, length, sw_str)]) 551 | return_data += tlv_sw 552 | 553 | if self.cct.algorithm is None: 554 | raise SwError(SW["CONDITIONSNOTSATISFIED"]) 555 | elif self.cct.algorithm == "CC": 556 | tag = SM_Class["CHECKSUM"] 557 | padded = vsCrypto.append_padding(self.cct.blocklength, return_data) 558 | auth = self.compute_cryptographic_checksum(0x8E, 0x80, padded) 559 | length = len(auth) 560 | return_data += bertlv_pack([(tag, length, auth)]) 561 | elif self.cct.algorithm == "SIGNATURE": 562 | tag = SM_Class["DIGITAL_SIGNATURE"] 563 | hash = self.hash(0x90, 0x80, return_data) 564 | auth = self.compute_digital_signature(0x9E, 0x9A, hash) 565 | length = len(auth) 566 | return_data += bertlv_pack([(tag, length, auth)]) 567 | 568 | return sw, return_data 569 | 570 | def compute_digital_signature(self, p1, p2, data): 571 | # TODO Signing with brainpoolP256r1 or any other key needs some more 572 | # effort ;-) 573 | return b'\x0D\xB2\x9B\xB9\x5E\x97\x7D\x42\x73\xCF\xA5\x45\xB7\xED' + \ 574 | b'\x5C\x39\x3F\xCE\xCD\x4A\xDE\xDC\x2B\x85\x23\x9F\x66\x52' + \ 575 | b'\x10\xC2\x67\xDC\xA6\x35\x94\x2D\x24\xED\xEB\xC8\x34\x6C' + \ 576 | b'\x4B\xD1\xA1\x15\xB4\x48\x3A\xA4\x4A\xCE\xFF\xED\x97\x0E' + \ 577 | b'\x07\xF3\x72\xF0\xFB\xA3\x62\x8C' 578 | 579 | 580 | class nPA_SAM(SAM): 581 | 582 | def __init__(self, eid_pin, can, mrz, puk, qes_pin, mf, default_se=nPA_SE): 583 | SAM.__init__(self, qes_pin, None, mf) 584 | self.active = True 585 | self.current_SE = default_se(self.mf, self) 586 | self.eid_pin = eid_pin 587 | self.can = can 588 | self.mrz = mrz 589 | self.puk = puk 590 | self.counter_puk = 10 591 | 592 | def general_authenticate(self, p1, p2, data): 593 | return self.current_SE.general_authenticate(p1, p2, data) 594 | 595 | def reset_retry_counter(self, p1, p2, data): 596 | # check if PACE was successful 597 | if self.current_SE.eac_step < 4: 598 | raise SwError(SW["ERR_SECSTATUS"]) 599 | 600 | # TODO: check CAN and PIN for the correct character set 601 | if p1 == 0x02: 602 | # change secret 603 | if p2 == self.current_SE.at.PACE_CAN: 604 | self.can = data 605 | print("Changed CAN to %r" % self.can) 606 | elif p2 == self.current_SE.at.PACE_PIN: 607 | # TODO: allow terminals to change the PIN with permission 608 | # "CAN allowed" 609 | if not self.current_SE.at.keyref_is_pin(): 610 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 611 | self.eid_pin = data 612 | print("Changed PIN to %r" % self.eid_pin) 613 | else: 614 | raise SwError(SW["ERR_DATANOTFOUND"]) 615 | elif p1 == 0x03: 616 | # resume/unblock secret 617 | if p2 == self.current_SE.at.PACE_CAN: 618 | # CAN has no counter 619 | pass 620 | elif p2 == self.current_SE.at.PACE_PIN: 621 | if self.current_SE.at.keyref_is_can(): 622 | self.active = True 623 | print("Resumed PIN") 624 | elif self.current_SE.at.keyref_is_pin(): 625 | # PACE was successful with PIN, nothing to do 626 | # resume/unblock 627 | pass 628 | elif self.current_SE.at.keyref_is_puk(): 629 | # TODO unblock PIN for signature 630 | print("Unblocked PIN") 631 | self.active = True 632 | self.counter = 3 633 | else: 634 | raise SwError(SW["ERR_CONDITIONNOTSATISFIED"]) 635 | else: 636 | raise SwError(SW["ERR_DATANOTFOUND"]) 637 | else: 638 | raise SwError(SW["ERR_INCORRECTP1P2"]) 639 | 640 | return 0x9000, b"" 641 | 642 | def external_authenticate(self, p1, p2, data): 643 | return self.current_SE.external_authenticate(p1, p2, data) 644 | 645 | def get_challenge(self, p1, p2, data): 646 | if self.current_SE.eac_step == 4: 647 | # TA 648 | if (p1 != 0x00 or p2 != 0x00): 649 | raise SwError(SW["ERR_INCORRECTP1P2"]) 650 | 651 | self.last_challenge = \ 652 | eac.TA_STEP4_get_nonce(self.current_SE.eac_ctx) 653 | if not self.last_challenge: 654 | eac.print_ossl_err() 655 | raise SwError(SW["ERR_NOINFO69"]) 656 | else: 657 | SAM.get_challenge(self, p1, p2, data) 658 | 659 | return SW["NORMAL"], self.last_challenge 660 | 661 | def verify(self, p1, p2, data): 662 | if p1 == 0x80 and p2 == 0x00: 663 | if self.current_SE.eac_step == 6: 664 | # data should only contain exactly OID 665 | [(tag, _, value)] = structure = unpack(data) 666 | if tag == 6: 667 | mapped_algo = ALGO_MAPPING[value] 668 | eid = self.mf.select('dfname', b'\xe8\x07\x04\x00\x7f\x00' 669 | b'\x07\x03\x02') 670 | if mapped_algo == "DateOfExpiry": 671 | [(_, _, [(_, _, mine)])] = \ 672 | unpack(eid.select('fid', 0x0103).data) 673 | logging.info("DateOfExpiry: " + str(mine) + 674 | "; reference: " + 675 | str(self.current_SE.at.DateOfExpiry)) 676 | if self.current_SE.at.DateOfExpiry < mine: 677 | print("Date of expiry verified") 678 | return SW["NORMAL"], "" 679 | else: 680 | print("Date of expiry not verified (expired)") 681 | return SW["WARN_NOINFO63"], "" 682 | elif mapped_algo == "DateOfBirth": 683 | [(_, _, [(_, _, mine)])] = \ 684 | unpack(eid.select('fid', 0x0108).data) 685 | # case1: YYYYMMDD -> good 686 | # case2: YYYYMM -> mapped to last day of given month, 687 | # i.e. YYYYMM31 ;-) 688 | # case3: YYYY -> mapped to YYYY-12-31 689 | if len(str(mine)) == 6: 690 | mine = int(str(mine) + "31") 691 | elif len(str(mine)) == 4: 692 | mine = int(str(mine) + "1231") 693 | logging.info("DateOfBirth: " + str(mine) + 694 | "; reference: " + 695 | str(self.current_SE.at.DateOfExpiry)) 696 | if self.current_SE.at.DateOfBirth < mine: 697 | print("Date of birth verified (old enough)") 698 | return SW["NORMAL"], "" 699 | else: 700 | print("Date of birth not verified (too young)") 701 | return SW["WARN_NOINFO63"], "" 702 | elif mapped_algo == "CommunityID": 703 | [(_, _, [(_, _, mine)])] = \ 704 | unpack(eid.select('fid', 0x0112).data) 705 | mine = binascii.hexlify(mine) 706 | logging.info("CommunityID: " + str(mine) + 707 | "; reference: " + 708 | str(self.current_SE.at.CommunityID)) 709 | if mine.startswith(self.current_SE.at.CommunityID): 710 | print("Community ID verified (living there)") 711 | return SW["NORMAL"], b"" 712 | else: 713 | print("Community ID not verified (not living" 714 | "there)") 715 | return SW["WARN_NOINFO63"], b"" 716 | else: 717 | return SwError(SW["ERR_DATANOTFOUND"]) 718 | else: 719 | return SwError(SW["ERR_DATANOTFOUND"]) 720 | 721 | else: 722 | return SAM.verify(self, p1, p2, data) 723 | 724 | def parse_SM_CAPDU(self, CAPDU, header_authentication): 725 | if hasattr(self.current_SE, "new_encryption_ctx"): 726 | if self.current_SE.new_encryption_ctx == eac.EAC_ID_PACE: 727 | protocol = "PACE" 728 | else: 729 | protocol = "CA" 730 | logging.info("switching to new encryption context established in " 731 | "%s:" % protocol) 732 | logging.info(eac.EAC_CTX_print_private(self.current_SE.eac_ctx, 4)) 733 | 734 | eac.EAC_CTX_set_encryption_ctx(self.current_SE.eac_ctx, 735 | self.current_SE.new_encryption_ctx) 736 | 737 | delattr(self.current_SE, "new_encryption_ctx") 738 | 739 | eac.EAC_increment_ssc(self.current_SE.eac_ctx) 740 | return SAM.parse_SM_CAPDU(self, CAPDU, 1) 741 | 742 | def protect_result(self, sw, unprotected_result): 743 | eac.EAC_increment_ssc(self.current_SE.eac_ctx) 744 | return SAM.protect_result(self, sw, unprotected_result) 745 | -------------------------------------------------------------------------------- /virtualsmartcard/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2006 Henryk Ploetz 3 | # 4 | # This file is part of virtualsmartcard. 5 | # 6 | # virtualsmartcard is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) any 9 | # later version. 10 | # 11 | # virtualsmartcard is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # virtualsmartcard. If not, see . 18 | # 19 | import binascii 20 | import string 21 | import struct 22 | from virtualsmartcard.ConstantDefinitions import MAX_SHORT_LE, MAX_EXTENDED_LE 23 | 24 | 25 | def stringtoint(data): 26 | i = 0 27 | if data: 28 | if isinstance(data, str): 29 | data = list(map(ord, data)) 30 | for byte in data: 31 | i = (i << 8) + byte 32 | return i 33 | 34 | 35 | def inttostring(i, length=None, len_extendable=False): 36 | str = b"" 37 | while True: 38 | str = struct.pack('B', i & 0xFF) + str 39 | i = i >> 8 40 | if i <= 0: 41 | break 42 | 43 | if length: 44 | l = len(str) 45 | if l > length and not len_extendable: 46 | raise ValueError("i too big for the specified stringlength") 47 | else: 48 | str = b'\x00'*(length-l) + str 49 | 50 | return str 51 | 52 | 53 | _myprintable = list(" " + string.ascii_letters + string.digits + string.punctuation) 54 | 55 | 56 | def hexdump(data, indent=0, short=False, linelen=16, offset=0): 57 | """Generates a nice hexdump of data and returns it. Consecutive lines will 58 | be indented with indent spaces. When short is true, will instead generate 59 | hexdump without adresses and on one line. 60 | 61 | Examples: 62 | hexdump(b'\x00\x41') -> \ 63 | '0000: 00 41 .A ' 64 | hexdump(b'\x00\x41', short=True) -> '00 41 (.A)'""" 65 | 66 | def hexable(data): 67 | if isinstance(data, str): 68 | data = list(map(ord, data)) 69 | return " ".join(map("{0:0>2X}".format, data)) 70 | 71 | def printable(data): 72 | return "".join([e in _myprintable and e or "." for e in data]) 73 | 74 | if short: 75 | return "%s (%s)" % (hexable(data), printable(data)) 76 | 77 | if isinstance(data, str): 78 | data = list(map(ord, data)) 79 | FORMATSTRING = "%04x: %-" + str(linelen*3) + "s %-" + str(linelen) + "s" 80 | result = "" 81 | (head, tail) = (data[:linelen], data[linelen:]) 82 | pos = 0 83 | while len(head) > 0: 84 | if pos > 0: 85 | result = result + "\n%s" % (' ' * indent) 86 | result = result + FORMATSTRING % (pos+offset, hexable(head), 87 | printable(head)) 88 | pos = pos + len(head) 89 | (head, tail) = (tail[:linelen], tail[linelen:]) 90 | return result 91 | 92 | LIFE_CYCLES = {0x01: "Load file = loaded", 93 | 0x03: "Applet instance / security domain = Installed", 94 | 0x07: "Card manager = Initialized; Applet instance / security " 95 | "domain = Selectable", 96 | 0x0F: "Card manager = Secured; Applet instance / security " 97 | "domain = Personalized", 98 | 0x7F: "Card manager = Locked; Applet instance / security " 99 | "domain = Blocked", 100 | 0xFF: "Applet instance = Locked"} 101 | 102 | 103 | def _make_byte_property(prop): 104 | "Make a byte property(). This is meta code." 105 | return property(lambda self: getattr(self, "_"+prop, None), 106 | lambda self, value: self._setbyte(prop, value), 107 | lambda self: delattr(self, "_"+prop), 108 | "The %s attribute of the APDU" % prop) 109 | 110 | 111 | class APDU(object): 112 | "Base class for an APDU" 113 | 114 | def __init__(self, *args, **kwargs): 115 | """Creates a new APDU instance. Can be given positional parameters which 116 | must be sequences of either strings (or strings themselves) or integers 117 | specifying byte values that will be concatenated in order. 118 | Alternatively you may give exactly one positional argument that is an 119 | APDU instance. 120 | After all the positional arguments have been concatenated they must 121 | form a valid APDU! 122 | 123 | The keyword arguments can then be used to override those values. 124 | Keywords recognized are: 125 | 126 | - C_APDU: cla, ins, p1, p2, lc, le, data 127 | - R_APDU: sw, sw1, sw2, data 128 | """ 129 | 130 | initbuff = list() 131 | 132 | if len(args) == 1 and isinstance(args[0], self.__class__): 133 | self.parse(args[0].render()) 134 | else: 135 | for arg in args: 136 | if type(arg) == str: 137 | initbuff.extend(arg) 138 | elif hasattr(arg, "__iter__"): 139 | for elem in arg: 140 | if hasattr(elem, "__iter__"): 141 | initbuff.extend(elem) 142 | else: 143 | initbuff.append(elem) 144 | else: 145 | initbuff.append(arg) 146 | 147 | for (index, value) in enumerate(initbuff): 148 | t = type(value) 149 | if t == str: 150 | initbuff[index] = ord(value) 151 | elif t != int: 152 | raise TypeError("APDU must consist of ints or one-byte " 153 | "strings, not %s (index %s)" % (t, index)) 154 | 155 | self.parse(initbuff) 156 | 157 | for (name, value) in kwargs.items(): 158 | if value is not None: 159 | setattr(self, name, value) 160 | 161 | def _getdata(self): 162 | return self._data 163 | 164 | def _setdata(self, value): 165 | if isinstance(value, str): 166 | self._data = b"".join([e for e in value]) 167 | elif isinstance(value, list): 168 | self._data = b"".join([inttostring(int(e)) for e in value]) 169 | elif isinstance(value, bytes): 170 | self._data = value 171 | else: 172 | raise ValueError("'data' attribute can only be a str or a list of " 173 | "int, not %s" % type(value)) 174 | self.Lc = len(value) 175 | 176 | def _deldata(self): 177 | del self._data 178 | self.data = "" 179 | 180 | data = property(_getdata, _setdata, None, 181 | "The data contents of this APDU") 182 | 183 | def _setbyte(self, name, value): 184 | if isinstance(value, int): 185 | setattr(self, "_"+name, value) 186 | elif isinstance(value, str): 187 | setattr(self, "_"+name, ord(value)) 188 | else: 189 | raise ValueError("'%s' attribute can only be a byte, that is: int " 190 | "or str, not %s" % (name, type(value))) 191 | 192 | def _format_parts(self, fields): 193 | "utility function to be used in __str__ and __repr__" 194 | 195 | parts = [] 196 | for i in fields: 197 | parts.append("%s=0x%02X" % (i, getattr(self, i))) 198 | 199 | return parts 200 | 201 | def __str__(self): 202 | result = "%s(%s)" % (self.__class__.__name__, 203 | ", ".join(self._format_fields())) 204 | 205 | if len(self.data) > 0: 206 | return result + ":\n " + hexdump(self.data, indent=2) 207 | else: 208 | return result 209 | 210 | def __repr__(self): 211 | parts = self._format_fields() 212 | 213 | if len(self.data) > 0: 214 | parts.append("data=%r" % self.data) 215 | 216 | return "%s(%s)" % (self.__class__.__name__, ", ".join(parts)) 217 | 218 | 219 | class C_APDU(APDU): 220 | "Class for a command APDU" 221 | 222 | def parse(self, apdu): 223 | """Parse a full command APDU and assign the values to our object, 224 | overwriting whatever there was.""" 225 | apdu = list(map(lambda a: (isinstance(a, str) and (ord(a),) or 226 | (a,))[0], apdu)) 227 | apdu = apdu + [0] * max(4-len(apdu), 0) 228 | 229 | self.CLA, self.INS, self.P1, self.P2 = apdu[:4] # case 1, 2, 3, 4 230 | self.__extended_length = False 231 | if len(apdu) == 4: # case 1 232 | self.data = "" 233 | elif (len(apdu) >= 7) and (apdu[4] == 0): # extended length apdu 234 | self.__extended_length = True 235 | if len(apdu) == 7: # case 2 extended length 236 | self.Le = (apdu[-2] << 8) + apdu[-1] 237 | self.data = "" 238 | else: # case 3, 4 extended length 239 | self.Lc = (apdu[5] << 8) + apdu[6] 240 | if len(apdu) == 7 + self.Lc: # case 3 extended length 241 | self.data = apdu[7:] 242 | elif len(apdu) == 7 + self.Lc + 3: # case 4 extended length 243 | self.Le = (apdu[-2] << 8) + apdu[-1] 244 | self.data = apdu[7:-3] 245 | 246 | # case 4 extended length with max le 247 | elif len(apdu) == 7 + self.Lc + 2 and apdu[-2:] == [0, 0]: 248 | self.Le = 0 249 | self.data = apdu[7:-2] 250 | else: 251 | raise ValueError("Invalid Lc value. Is %s, should be %s " 252 | "or %s" % (self.Lc, 7 + self.Lc, 253 | 7 + self.Lc + 3)) 254 | else: # short apdu 255 | if len(apdu) == 5: # case 2 short apdu 256 | self.Le = apdu[-1] 257 | self.data = "" 258 | elif len(apdu) > 5: # case 3, 4 short apdu 259 | self.Lc = apdu[4] 260 | if len(apdu) == 5 + self.Lc: # case 3 261 | self.data = apdu[5:] 262 | elif len(apdu) == 5 + self.Lc + 1: # case 4 263 | self.data = apdu[5:-1] 264 | self.Le = apdu[-1] 265 | else: 266 | raise ValueError("Invalid Lc value. Is %s, should be %s " 267 | "or %s" % (self.Lc, 5 + self.Lc, 268 | 5 + self.Lc + 1)) 269 | 270 | CLA = _make_byte_property("CLA") 271 | cla = CLA 272 | INS = _make_byte_property("INS") 273 | ins = INS 274 | P1 = _make_byte_property("P1") 275 | p1 = P1 276 | P2 = _make_byte_property("P2") 277 | p2 = P2 278 | Lc = _make_byte_property("Lc") 279 | lc = Lc 280 | Le = _make_byte_property("Le") 281 | le = Le 282 | 283 | @property 284 | def effective_Le(self): 285 | if hasattr(self, "_Le") and (self.Le == 0): 286 | if self.__extended_length: 287 | return MAX_EXTENDED_LE 288 | else: 289 | return MAX_SHORT_LE 290 | else: 291 | return self.Le 292 | 293 | def _format_fields(self): 294 | fields = ["CLA", "INS", "P1", "P2"] 295 | if self.Lc > 0: 296 | fields.append("Lc") 297 | 298 | # There's a difference between "Le = 0" and "no Le" 299 | if hasattr(self, "_Le"): 300 | fields.append("Le") 301 | 302 | return self._format_parts(fields) 303 | 304 | def render(self): 305 | "Return this APDU as a binary string" 306 | buffer = [] 307 | 308 | for i in self.CLA, self.INS, self.P1, self.P2: 309 | buffer.append(inttostring(i)) 310 | 311 | if len(self.data) > 0: 312 | buffer.append(inttostring(self.Lc)) 313 | buffer.append(self.data) 314 | 315 | if hasattr(self, "_Le"): 316 | if self.__extended_length: 317 | buffer.append(inttostring(0x00)) 318 | buffer.append(inttostring(self.Le >> 8)) 319 | buffer.append(inttostring(self.Le - self.Le >> 8)) 320 | else: 321 | buffer.append(inttostring(self.Le)) 322 | 323 | return b"".join(buffer) 324 | 325 | def case(self): 326 | "Return 1, 2, 3 or 4, depending on which ISO case we represent." 327 | if self.Lc == 0: 328 | if not hasattr(self, "_Le"): 329 | return 1 330 | else: 331 | return 2 332 | else: 333 | if not hasattr(self, "_Le"): 334 | return 3 335 | else: 336 | return 4 337 | 338 | 339 | class R_APDU(APDU): 340 | "Class for a response APDU" 341 | 342 | def _getsw(self): 343 | return inttostring(self.SW1) + inttostring(self.SW2) 344 | 345 | def _setsw(self, value): 346 | if len(value) != 2: 347 | raise ValueError("SW must be exactly two bytes") 348 | self.SW1 = value[0] 349 | self.SW2 = value[1] 350 | 351 | SW = property(_getsw, _setsw, None, 352 | "The Status Word of this response APDU") 353 | sw = SW 354 | 355 | SW1 = _make_byte_property("SW1") 356 | sw1 = SW1 357 | SW2 = _make_byte_property("SW2") 358 | sw2 = SW2 359 | 360 | def parse(self, apdu): 361 | """Parse a full response APDU and assign the values to our object, 362 | overwriting whatever there was.""" 363 | self.SW = apdu[-2:] 364 | self.data = apdu[:-2] 365 | 366 | def _format_fields(self): 367 | fields = ["SW1", "SW2"] 368 | return self._format_parts(fields) 369 | 370 | def render(self): 371 | "Return this APDU as a binary string" 372 | return self.data + self.sw 373 | --------------------------------------------------------------------------------