<p>fixeria has uploaded this change for <strong>review</strong>.</p><p><a href="https://gerrit.osmocom.org/c/pysim/+/18159">View Change</a></p><pre style="font-family: monospace,monospace; white-space: pre-wrap;">Implement Generic SIM Access interface as per 3GPP TS 27.007<br><br>According to 3GPP TS 27.007, sections 8.17 and 8.18, the modem<br>may *optionally* provide Generic and/or Restricted SIM Access<br>to the TE (Terminal Equipment) by means of the AT commands.<br>This basically means that a modem can act as a card reader.<br><br>Generic SIM Access allows the TE to send raw PDUs in the format<br>as described in 3GPP TS 51.011 directly to the SIM card, while<br>Restricted SIM Access is more limited, and thus is not really<br>interesting to us.<br><br>This change implements a new transport called ModemATCommandLink,<br>so using it a SIM card can be read and/or programmed without the<br>need to remove it from the modem's socket. A downside of this<br>approach is relatively slow I/O speed compared to PC/SC readers.<br><br>Tested with Quectel EC20:<br><br>  $ ./pySim-read.py --modem-dev /dev/ttyUSB2<br><br>Change-Id: I20bc00315e2c7c298f46283852865c1416047bc6<br>Signed-off-by: Vadim Yanitskiy <axilirator@gmail.com><br>---<br>M pySim-prog.py<br>M pySim-read.py<br>A pySim/transport/modem_atcmd.py<br>3 files changed, 147 insertions(+), 0 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;">git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/59/18159/1</pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/pySim-prog.py b/pySim-prog.py</span><br><span>index e3045a6..892a565 100755</span><br><span>--- a/pySim-prog.py</span><br><span>+++ b/pySim-prog.py</span><br><span>@@ -61,6 +61,14 @@</span><br><span>                  help="Which PC/SC reader number for SIM access",</span><br><span>                   default=None,</span><br><span>                )</span><br><span style="color: hsl(120, 100%, 40%);">+     parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",</span><br><span style="color: hsl(120, 100%, 40%);">+                    help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",</span><br><span style="color: hsl(120, 100%, 40%);">+                        default=None,</span><br><span style="color: hsl(120, 100%, 40%);">+         )</span><br><span style="color: hsl(120, 100%, 40%);">+     parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",</span><br><span style="color: hsl(120, 100%, 40%);">+                      help="Baudrate used for modem's port [default: %default]",</span><br><span style="color: hsl(120, 100%, 40%);">+                      default=115200,</span><br><span style="color: hsl(120, 100%, 40%);">+               )</span><br><span>    parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",</span><br><span>                    help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",</span><br><span>                  default=None,</span><br><span>@@ -693,6 +701,11 @@</span><br><span>                         % opts.osmocon_sock)</span><br><span>                 from pySim.transport.calypso import CalypsoSimLink</span><br><span>           sl = CalypsoSimLink(sock_path=opts.osmocon_sock)</span><br><span style="color: hsl(120, 100%, 40%);">+      elif opts.modem_dev is not None:</span><br><span style="color: hsl(120, 100%, 40%);">+              print("Using modem (dev=%s, speed=%d) for Generic SIM Access (3GPP TS 27.007)"</span><br><span style="color: hsl(120, 100%, 40%);">+                      % (opts.modem_dev, opts.modem_baud))</span><br><span style="color: hsl(120, 100%, 40%);">+          from pySim.transport.modem_atcmd import ModemATCommandLink</span><br><span style="color: hsl(120, 100%, 40%);">+            sl = ModemATCommandLink(device=opts.modem_dev, baudrate=opts.modem_baud)</span><br><span>     else: # Serial reader is default</span><br><span>             print("Using serial reader (port=%s, baudrate=%d) interface"</span><br><span>                       % (opts.device, opts.baudrate))</span><br><span>diff --git a/pySim-read.py b/pySim-read.py</span><br><span>index 33e93a7..9713fb9 100755</span><br><span>--- a/pySim-read.py</span><br><span>+++ b/pySim-read.py</span><br><span>@@ -53,6 +53,14 @@</span><br><span>                        help="Which PC/SC reader number for SIM access",</span><br><span>                   default=None,</span><br><span>                )</span><br><span style="color: hsl(120, 100%, 40%);">+     parser.add_option("--modem-device", dest="modem_dev", metavar="DEV",</span><br><span style="color: hsl(120, 100%, 40%);">+                    help="Serial port of modem for Generic SIM Access (3GPP TS 27.007)",</span><br><span style="color: hsl(120, 100%, 40%);">+                        default=None,</span><br><span style="color: hsl(120, 100%, 40%);">+         )</span><br><span style="color: hsl(120, 100%, 40%);">+     parser.add_option("--modem-baud", dest="modem_baud", type="int", metavar="BAUD",</span><br><span style="color: hsl(120, 100%, 40%);">+                      help="Baudrate used for modem's port [default: %default]",</span><br><span style="color: hsl(120, 100%, 40%);">+                      default=115200,</span><br><span style="color: hsl(120, 100%, 40%);">+               )</span><br><span>    parser.add_option("--osmocon", dest="osmocon_sock", metavar="PATH",</span><br><span>                    help="Socket path for Calypso (e.g. Motorola C1XX) based reader (via OsmocomBB)",</span><br><span>                  default=None,</span><br><span>@@ -82,6 +90,11 @@</span><br><span>                   % opts.osmocon_sock)</span><br><span>                 from pySim.transport.calypso import CalypsoSimLink</span><br><span>           sl = CalypsoSimLink(sock_path=opts.osmocon_sock)</span><br><span style="color: hsl(120, 100%, 40%);">+      elif opts.modem_dev is not None:</span><br><span style="color: hsl(120, 100%, 40%);">+              print("Using modem (dev=%s, speed=%d) for Generic SIM Access (3GPP TS 27.007)"</span><br><span style="color: hsl(120, 100%, 40%);">+                      % (opts.modem_dev, opts.modem_baud))</span><br><span style="color: hsl(120, 100%, 40%);">+          from pySim.transport.modem_atcmd import ModemATCommandLink</span><br><span style="color: hsl(120, 100%, 40%);">+            sl = ModemATCommandLink(device=opts.modem_dev, baudrate=opts.modem_baud)</span><br><span>     else: # Serial reader is default</span><br><span>             print("Using serial reader (port=%s, baudrate=%d) interface"</span><br><span>                       % (opts.device, opts.baudrate))</span><br><span>diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py</span><br><span>new file mode 100644</span><br><span>index 0000000..4594325</span><br><span>--- /dev/null</span><br><span>+++ b/pySim/transport/modem_atcmd.py</span><br><span>@@ -0,0 +1,121 @@</span><br><span style="color: hsl(120, 100%, 40%);">+#!/usr/bin/env python</span><br><span style="color: hsl(120, 100%, 40%);">+# -*- coding: utf-8 -*-</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+""" pySim: Transport Link for 3GPP TS 27.007 compliant modems</span><br><span style="color: hsl(120, 100%, 40%);">+"""</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+# Copyright (C) 2020 Vadim Yanitskiy <axilirator@gmail.com></span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is free software: you can redistribute it and/or modify</span><br><span style="color: hsl(120, 100%, 40%);">+# it under the terms of the GNU General Public License as published by</span><br><span style="color: hsl(120, 100%, 40%);">+# the Free Software Foundation, either version 2 of the License, or</span><br><span style="color: hsl(120, 100%, 40%);">+# (at your option) any later version.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is distributed in the hope that it will be useful,</span><br><span style="color: hsl(120, 100%, 40%);">+# but WITHOUT ANY WARRANTY; without even the implied warranty of</span><br><span style="color: hsl(120, 100%, 40%);">+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</span><br><span style="color: hsl(120, 100%, 40%);">+# GNU General Public License for more details.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# You should have received a copy of the GNU General Public License</span><br><span style="color: hsl(120, 100%, 40%);">+# along with this program.  If not, see <http://www.gnu.org/licenses/>.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from __future__ import absolute_import</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+import logging as log</span><br><span style="color: hsl(120, 100%, 40%);">+import serial</span><br><span style="color: hsl(120, 100%, 40%);">+import time</span><br><span style="color: hsl(120, 100%, 40%);">+import re</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.transport import LinkBase</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.exceptions import *</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+class ModemATCommandLink(LinkBase):</span><br><span style="color: hsl(120, 100%, 40%);">+ def __init__(self, device='/dev/ttyUSB0', baudrate=115200):</span><br><span style="color: hsl(120, 100%, 40%);">+           self._sl = serial.Serial(device, baudrate, timeout=5)</span><br><span style="color: hsl(120, 100%, 40%);">+         self._device = device</span><br><span style="color: hsl(120, 100%, 40%);">+         self._atr = None</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+            # HACK: if somebody needs to debug this thing</span><br><span style="color: hsl(120, 100%, 40%);">+         # log.root.setLevel(log.DEBUG)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+      def __del__(self):</span><br><span style="color: hsl(120, 100%, 40%);">+            self._sl.close()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    def send_at_cmd(self, cmd):</span><br><span style="color: hsl(120, 100%, 40%);">+           # Convert from string to bytes, if needed</span><br><span style="color: hsl(120, 100%, 40%);">+             if type(cmd) is str:</span><br><span style="color: hsl(120, 100%, 40%);">+                  bcmd = str.encode(cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+                bcmd += b'\r'</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+               # Send command to the modem</span><br><span style="color: hsl(120, 100%, 40%);">+           log.debug('Sending AT command: %s' % cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+             try:</span><br><span style="color: hsl(120, 100%, 40%);">+                  wlen = self._sl.write(bcmd)</span><br><span style="color: hsl(120, 100%, 40%);">+                   assert(wlen == len(bcmd))</span><br><span style="color: hsl(120, 100%, 40%);">+             except:</span><br><span style="color: hsl(120, 100%, 40%);">+                       raise ReaderError('Failed to sent AT command: %s' % cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+            # Give the modem some time...</span><br><span style="color: hsl(120, 100%, 40%);">+         time.sleep(0.3)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+             # Read the response</span><br><span style="color: hsl(120, 100%, 40%);">+           try:</span><br><span style="color: hsl(120, 100%, 40%);">+                  # Skip characters sent back</span><br><span style="color: hsl(120, 100%, 40%);">+                   self._sl.read(wlen)</span><br><span style="color: hsl(120, 100%, 40%);">+                   # Read the rest</span><br><span style="color: hsl(120, 100%, 40%);">+                       rsp = self._sl.read_all()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+                   # Strip '\r\n'</span><br><span style="color: hsl(120, 100%, 40%);">+                        rsp = rsp.strip()</span><br><span style="color: hsl(120, 100%, 40%);">+                     # Split into a list</span><br><span style="color: hsl(120, 100%, 40%);">+                   rsp = rsp.split(b'\r\n\r\n')</span><br><span style="color: hsl(120, 100%, 40%);">+          except:</span><br><span style="color: hsl(120, 100%, 40%);">+                       raise ReaderError('Failed parse response to AT command: %s' % cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+          log.debug('Got response from modem: %s' % rsp)</span><br><span style="color: hsl(120, 100%, 40%);">+                return rsp</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+  def connect(self):</span><br><span style="color: hsl(120, 100%, 40%);">+            # Make sure that we can talk to the modem</span><br><span style="color: hsl(120, 100%, 40%);">+             if self.send_at_cmd('AT') != [b'OK']:</span><br><span style="color: hsl(120, 100%, 40%);">+                 raise ReaderError('Failed to connect to modem')</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+             # Reset the modem, just to be sure</span><br><span style="color: hsl(120, 100%, 40%);">+            if self.send_at_cmd('ATZ') != [b'OK']:</span><br><span style="color: hsl(120, 100%, 40%);">+                        raise ReaderError('Failed to reset the modem')</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+              # Make sure that generic SIM access is supported</span><br><span style="color: hsl(120, 100%, 40%);">+              if self.send_at_cmd('AT+CSIM=?') != [b'OK']:</span><br><span style="color: hsl(120, 100%, 40%);">+                  raise ReaderError('The modem does not seem to support SIM access')</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+          log.info('Modem at \'%s\' is ready!' % device)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+      def disconnect(self):</span><br><span style="color: hsl(120, 100%, 40%);">+         pass # Nothing to do really ...</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+     def wait_for_card(self, timeout=None, newcardonly=False):</span><br><span style="color: hsl(120, 100%, 40%);">+             pass # Nothing to do really ...</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+     def send_apdu_raw(self, pdu):</span><br><span style="color: hsl(120, 100%, 40%);">+         # Prepare the command as described in 8.17</span><br><span style="color: hsl(120, 100%, 40%);">+            cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+               # Send AT+CSIM command to the modem</span><br><span style="color: hsl(120, 100%, 40%);">+           # TODO: also handle +CME ERROR: <err></span><br><span style="color: hsl(120, 100%, 40%);">+           rsp = self.send_at_cmd(cmd)</span><br><span style="color: hsl(120, 100%, 40%);">+           if len(rsp) != 2 or rsp[-1] != b'OK':</span><br><span style="color: hsl(120, 100%, 40%);">+                 raise ReaderError('APDU transfer failed: %s' % str(rsp))</span><br><span style="color: hsl(120, 100%, 40%);">+              rsp = rsp[0] # Get rid of b'OK'</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+             # Make sure that the response has format: b'+CSIM: %d,\"%s\"'</span><br><span style="color: hsl(120, 100%, 40%);">+               try:</span><br><span style="color: hsl(120, 100%, 40%);">+                  result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)</span><br><span style="color: hsl(120, 100%, 40%);">+                    (rsp_pdu_len, rsp_pdu) = result.groups()</span><br><span style="color: hsl(120, 100%, 40%);">+              except:</span><br><span style="color: hsl(120, 100%, 40%);">+                       raise ReaderError('Failed to parse response from modem: %s' % rsp)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+          # TODO: make sure we have at least SW</span><br><span style="color: hsl(120, 100%, 40%);">+         data = rsp_pdu[:-4].decode()</span><br><span style="color: hsl(120, 100%, 40%);">+          sw   = rsp_pdu[-4:].decode()</span><br><span style="color: hsl(120, 100%, 40%);">+          return data, sw</span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.osmocom.org/c/pysim/+/18159">change 18159</a>. To unsubscribe, or for help writing mail filters, visit <a href="https://gerrit.osmocom.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.osmocom.org/c/pysim/+/18159"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: pysim </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-Change-Id: I20bc00315e2c7c298f46283852865c1416047bc6 </div>
<div style="display:none"> Gerrit-Change-Number: 18159 </div>
<div style="display:none"> Gerrit-PatchSet: 1 </div>
<div style="display:none"> Gerrit-Owner: fixeria <axilirator@gmail.com> </div>
<div style="display:none"> Gerrit-MessageType: newchange </div>