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.orglaforge 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>