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
diff --git a/contrib/sim-rest-client.py b/contrib/sim-rest-client.py
new file mode 100755
index 0000000..8f74adc
--- /dev/null
+++ b/contrib/sim-rest-client.py
@@ -0,0 +1,155 @@
+#!/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@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/>.
+
+from typing import Optional, Dict
+
+import sys
+import argparse
+import secrets
+import requests
+
+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."""
+ return int.from_bytes(x, byteorder='big')
+
+def pack48(x:int) -> bytes:
+ """Encode a big-endian 48bit number from integer to binary."""
+ return x.to_bytes(48 // 8, byteorder='big')
+
+def milenage_generate(opc:bytes, amf:bytes, k:bytes, sqn:bytes, rand:bytes) -> Dict[str, bytes]:
+ """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) -> Optional[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:Optional[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 is not 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)