Change in pysim[master]: contrib: Add sim-rest-{server,client}.py

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

laforge gerrit-no-reply at lists.osmocom.org
Sun May 23 10:04:51 UTC 2021


laforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/24342 )


Change subject: contrib: Add sim-rest-{server,client}.py
......................................................................

contrib: Add sim-rest-{server,client}.py

sim-rest-server.py can be used to provide a RESTful API to allow remote
clients to perform the authentication command against a SIM card in a
PC/SC reader.

sim-rest-client.py is an example client against sim-rest-server.py
which can be used to test the functionality of sim-rest-server.py.

Change-Id: I738ca3109ab038d4f5595cc1dab6a49087df5886
---
A contrib/sim-rest-client.py
A contrib/sim-rest-server.py
2 files changed, 246 insertions(+), 0 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/42/24342/1

diff --git a/contrib/sim-rest-client.py b/contrib/sim-rest-client.py
new file mode 100755
index 0000000..68f87b0
--- /dev/null
+++ b/contrib/sim-rest-client.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+#
+# sim-rest-client.py: client program to test the sim-rest-server.py
+#
+# this will generate authentication tuples just like a HLR / HSS
+# and will then send the related challenge to the REST interface
+# of sim-rest-server.py
+#
+# sim-rest-server.py will then contact the SIM card to perform the
+# authentication (just like a 3GPP RAN), and return the results via
+# the REST to sim-rest-client.py.
+#
+# (C) 2021 by Harald Welte <laforge at osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import argparse
+import requests
+import struct
+import secrets
+
+from CryptoMobile.Milenage import Milenage
+from CryptoMobile.utils import xor_buf
+
+def unpack48(x:bytes) -> int:
+    """Decode a big-endian 48bit number from binary to integer."""
+    x2, x3 = struct.unpack('>IH', x)
+    return (x2 << 16) | x3
+
+def pack48(x:int) -> bytes:
+    """Encode a big-endian 48bit number from integer to binary."""
+    x3 = x & 0xffff
+    x2 = x >> 16
+    return struct.pack('>IH', x2, x3)
+
+def milenage_generate(opc:bytes, amf:bytes, k:bytes, sqn:bytes, rand:bytes) -> dict():
+    """Generate an MILENAGE Authentication Tuple."""
+    m = Milenage(None)
+    m.set_opc(opc)
+    mac_a = m.f1(k, rand, sqn, amf)
+    res, ck, ik, ak = m.f2345(k, rand)
+
+    # AUTN = (SQN ^ AK) || AMF || MAC
+    sqn_ak = xor_buf(sqn, ak)
+    autn = b''.join([sqn_ak, amf, mac_a])
+
+    return {'res': res, 'ck': ck, 'ik': ik, 'autn': autn}
+
+def milenage_auts(opc:bytes, k:bytes, rand:bytes, auts:bytes):
+    """Validate AUTS. If successful, returns SQN_MS"""
+    amf = b'\x00\x00' # TS 33.102 Section 6.3.3
+    m = Milenage(None)
+    m.set_opc(opc)
+    ak = m.f5star(k, rand)
+
+    sqn_ak = auts[:6]
+    sqn = xor_buf(sqn_ak, ak[:6])
+
+    mac_s = m.f1star(k, rand, sqn, amf)
+    if mac_s == auts[6:14]:
+        return sqn
+    else:
+        return False
+
+
+def build_url(suffix:str) -> str:
+    """Build an URL from global server_host, server_port, BASE_PATH and suffix."""
+    BASE_PATH= "/sim-auth-api/v1"
+    return "http://%s:%u%s%s" % (server_host, server_port, BASE_PATH, suffix)
+
+
+def rest_post(suffix:str, js:dict = None):
+    """Perform a RESTful POST."""
+    url = build_url(suffix)
+    if verbose:
+        print("POST %s (%s)" % (url, str(js)))
+    resp = requests.post(url, json=js)
+    if verbose:
+        print("-> %s" % (resp))
+    if not resp.ok:
+        print("POST failed")
+    return resp
+
+
+
+def main(argv):
+    global server_port, server_host, verbose
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-H", "--host", help="Host to connect to", default="localhost")
+    parser.add_argument("-p", "--port", help="TCP port to connect to", default=8000)
+    parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
+    parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)
+    parser.add_argument("-c", "--count", help="Auth count", type=int, default=10)
+
+    parser.add_argument("-k", "--key", help="Secret key K (hex)", type=str, required=True)
+    parser.add_argument("-o", "--opc", help="Secret OPc (hex)", type=str, required=True)
+    parser.add_argument("-a", "--amf", help="AMF Field (hex)", type=str, default="0000")
+    parser.add_argument("-s", "--sqn", help="SQN Field (hex)", type=str, default="000000000000")
+
+    args = parser.parse_args()
+
+    server_host = args.host
+    server_port = args.port
+    verbose = args.verbose
+
+    #opc = bytes.fromhex('767A662ACF4587EB0C450C6A95540A04')
+    #k = bytes.fromhex('876B2D8D403EE96755BEF3E0A1857EBE')
+    opc = bytes.fromhex(args.opc)
+    k = bytes.fromhex(args.key)
+    amf = bytes.fromhex(args.amf)
+    sqn = bytes.fromhex(args.sqn)
+
+    for i in range(args.count):
+        rand = secrets.token_bytes(16)
+        t = milenage_generate(opc=opc, amf=amf, k=k, sqn=sqn, rand=rand)
+
+        req_json = {'rand': rand.hex(), 'autn': t['autn'].hex()}
+        print("-> %s" % req_json)
+        resp = rest_post('/slot/%u' % args.slot_nr, req_json)
+        resp_json = resp.json()
+        print("<- %s" % resp_json)
+        if 'synchronisation_failure' in resp_json:
+            auts = bytes.fromhex(resp_json['synchronisation_failure']['auts'])
+            sqn_ms = milenage_auts(opc, k, rand, auts)
+            if sqn_ms != False:
+                print("SQN_MS = %s" % sqn_ms.hex())
+                sqn_ms_int = unpack48(sqn_ms)
+                # we assume an IND bit-length of 5 here
+                sqn = pack48(sqn_ms_int + (1 << 5))
+            else:
+                raise RuntimeError("AUTS auth failure during re-sync?!?")
+        elif 'successful_3g_authentication' in resp_json:
+            auth_res = resp_json['successful_3g_authentication']
+            assert bytes.fromhex(auth_res['res']) == t['res']
+            assert bytes.fromhex(auth_res['ck']) == t['ck']
+            assert bytes.fromhex(auth_res['ik']) == t['ik']
+            # we assume an IND bit-length of 5 here
+            sqn = pack48(unpack48(sqn) + (1 << 5))
+        else:
+            raise RuntimeError("Auth failure")
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/contrib/sim-rest-server.py b/contrib/sim-rest-server.py
new file mode 100755
index 0000000..a727d2f
--- /dev/null
+++ b/contrib/sim-rest-server.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+# RESTful HTTP service for performing authentication against USIM cards
+#
+# (C) 2021 by Harald Welte <laforge at osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import sys
+import argparse
+
+from klein import run, route
+
+from pySim.transport import ApduTracer
+from pySim.transport.pcsc import PcscSimLink
+from pySim.commands import SimCardCommands
+from pySim.cards import UsimCard
+from pySim.exceptions import *
+
+class ApduPrintTracer(ApduTracer):
+    def trace_response(self, cmd, sw, resp):
+        #print("CMD: %s -> RSP: %s %s" % (cmd, sw, resp))
+        pass
+
+ at route('/sim-auth-api/v1/slot/<int:slot>')
+def auth(request, slot):
+    """REST API endpoint for performing authentication against a USIM.
+       Expects a JSON body containing RAND and AUTN.
+       Returns a JSON body containing RES, CK, IK and Kc."""
+    try:
+        # there are two hex-string JSON parameters in the body: rand and autn
+        content = json.loads(request.content.read())
+        rand = content['rand']
+        autn = content['autn']
+    except:
+        request.setResponseCode(400)
+        return "Malformed Request"
+
+    try:
+        tp = PcscSimLink(slot, apdu_tracer=ApduPrintTracer())
+        tp.connect()
+    except ReaderError:
+        request.setResponseCode(404)
+        return "Specified SIM Slot doesn't exist"
+    except ProtocolError:
+        request.setResponseCode(500)
+        return "Error"
+    except NoCardError:
+        request.setResponseCode(410)
+        return "No SIM card inserted in slot"
+
+    scc = SimCardCommands(tp)
+    card = UsimCard(scc)
+    try:
+        card.read_aids()
+        card.select_adf_by_aid(adf='usim')
+        res, sw = scc.authenticate(rand, autn)
+    except SwMatchError as e:
+        request.setResponseCode(500)
+        return "Communication Error %s" % e
+
+    tp.disconnect()
+
+    return json.dumps(res, indent=4)
+
+def main(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")
+    parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
+    #parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)
+
+    args = parser.parse_args()
+
+    run(args.host, args.port)
+
+if __name__ == "__main__":
+    main(sys.argv)

-- 
To view, visit https://gerrit.osmocom.org/c/pysim/+/24342
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I738ca3109ab038d4f5595cc1dab6a49087df5886
Gerrit-Change-Number: 24342
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge at osmocom.org>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20210523/c10e3247/attachment.htm>


More information about the gerrit-log mailing list