Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # sim-rest-client.py: client program to test the sim-rest-server.py |
| 4 | # |
| 5 | # this will generate authentication tuples just like a HLR / HSS |
| 6 | # and will then send the related challenge to the REST interface |
| 7 | # of sim-rest-server.py |
| 8 | # |
| 9 | # sim-rest-server.py will then contact the SIM card to perform the |
| 10 | # authentication (just like a 3GPP RAN), and return the results via |
| 11 | # the REST to sim-rest-client.py. |
| 12 | # |
| 13 | # (C) 2021 by Harald Welte <laforge@osmocom.org> |
| 14 | # |
| 15 | # This program is free software: you can redistribute it and/or modify |
| 16 | # it under the terms of the GNU General Public License as published by |
| 17 | # the Free Software Foundation, either version 2 of the License, or |
| 18 | # (at your option) any later version. |
| 19 | # |
| 20 | # This program is distributed in the hope that it will be useful, |
| 21 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 22 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 23 | # GNU General Public License for more details. |
| 24 | # |
| 25 | # You should have received a copy of the GNU General Public License |
| 26 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 27 | |
| 28 | from typing import Optional, Dict |
| 29 | |
| 30 | import sys |
| 31 | import argparse |
| 32 | import secrets |
| 33 | import requests |
| 34 | |
| 35 | from CryptoMobile.Milenage import Milenage |
| 36 | from CryptoMobile.utils import xor_buf |
| 37 | |
| 38 | def unpack48(x:bytes) -> int: |
| 39 | """Decode a big-endian 48bit number from binary to integer.""" |
| 40 | return int.from_bytes(x, byteorder='big') |
| 41 | |
| 42 | def pack48(x:int) -> bytes: |
| 43 | """Encode a big-endian 48bit number from integer to binary.""" |
| 44 | return x.to_bytes(48 // 8, byteorder='big') |
| 45 | |
| 46 | def milenage_generate(opc:bytes, amf:bytes, k:bytes, sqn:bytes, rand:bytes) -> Dict[str, bytes]: |
| 47 | """Generate an MILENAGE Authentication Tuple.""" |
| 48 | m = Milenage(None) |
| 49 | m.set_opc(opc) |
| 50 | mac_a = m.f1(k, rand, sqn, amf) |
| 51 | res, ck, ik, ak = m.f2345(k, rand) |
| 52 | |
| 53 | # AUTN = (SQN ^ AK) || AMF || MAC |
| 54 | sqn_ak = xor_buf(sqn, ak) |
| 55 | autn = b''.join([sqn_ak, amf, mac_a]) |
| 56 | |
| 57 | return {'res': res, 'ck': ck, 'ik': ik, 'autn': autn} |
| 58 | |
| 59 | def milenage_auts(opc:bytes, k:bytes, rand:bytes, auts:bytes) -> Optional[bytes]: |
| 60 | """Validate AUTS. If successful, returns SQN_MS""" |
| 61 | amf = b'\x00\x00' # TS 33.102 Section 6.3.3 |
| 62 | m = Milenage(None) |
| 63 | m.set_opc(opc) |
| 64 | ak = m.f5star(k, rand) |
| 65 | |
| 66 | sqn_ak = auts[:6] |
| 67 | sqn = xor_buf(sqn_ak, ak[:6]) |
| 68 | |
| 69 | mac_s = m.f1star(k, rand, sqn, amf) |
| 70 | if mac_s == auts[6:14]: |
| 71 | return sqn |
| 72 | else: |
| 73 | return False |
| 74 | |
| 75 | |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 76 | def build_url(suffix:str, base_path="/sim-auth-api/v1") -> str: |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 77 | """Build an URL from global server_host, server_port, BASE_PATH and suffix.""" |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 78 | return "http://%s:%u%s%s" % (server_host, server_port, base_path, suffix) |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 79 | |
| 80 | |
| 81 | def rest_post(suffix:str, js:Optional[dict] = None): |
| 82 | """Perform a RESTful POST.""" |
| 83 | url = build_url(suffix) |
| 84 | if verbose: |
| 85 | print("POST %s (%s)" % (url, str(js))) |
| 86 | resp = requests.post(url, json=js) |
| 87 | if verbose: |
| 88 | print("-> %s" % (resp)) |
| 89 | if not resp.ok: |
| 90 | print("POST failed") |
| 91 | return resp |
| 92 | |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 93 | def rest_get(suffix:str, base_path=None): |
| 94 | """Perform a RESTful GET.""" |
| 95 | url = build_url(suffix, base_path) |
| 96 | if verbose: |
| 97 | print("GET %s" % url) |
| 98 | resp = requests.get(url) |
| 99 | if verbose: |
| 100 | print("-> %s" % (resp)) |
| 101 | if not resp.ok: |
| 102 | print("GET failed") |
| 103 | return resp |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 104 | |
| 105 | |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 106 | def main_info(args): |
| 107 | resp = rest_get('/slot/%u' % args.slot_nr, base_path="/sim-info-api/v1") |
| 108 | if not resp.ok: |
| 109 | print("<- ERROR %u: %s" % (resp.status_code, resp.text)) |
| 110 | sys.exit(1) |
| 111 | resp_json = resp.json() |
| 112 | print("<- %s" % resp_json) |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 113 | |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 114 | |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 115 | def main_auth(args): |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 116 | #opc = bytes.fromhex('767A662ACF4587EB0C450C6A95540A04') |
| 117 | #k = bytes.fromhex('876B2D8D403EE96755BEF3E0A1857EBE') |
| 118 | opc = bytes.fromhex(args.opc) |
| 119 | k = bytes.fromhex(args.key) |
| 120 | amf = bytes.fromhex(args.amf) |
| 121 | sqn = bytes.fromhex(args.sqn) |
| 122 | |
| 123 | for i in range(args.count): |
| 124 | rand = secrets.token_bytes(16) |
| 125 | t = milenage_generate(opc=opc, amf=amf, k=k, sqn=sqn, rand=rand) |
| 126 | |
| 127 | req_json = {'rand': rand.hex(), 'autn': t['autn'].hex()} |
| 128 | print("-> %s" % req_json) |
| 129 | resp = rest_post('/slot/%u' % args.slot_nr, req_json) |
Harald Welte | 7a401a2 | 2021-11-03 12:14:14 +0100 | [diff] [blame] | 130 | if not resp.ok: |
| 131 | print("<- ERROR %u: %s" % (resp.status_code, resp.text)) |
| 132 | break |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 133 | resp_json = resp.json() |
| 134 | print("<- %s" % resp_json) |
| 135 | if 'synchronisation_failure' in resp_json: |
| 136 | auts = bytes.fromhex(resp_json['synchronisation_failure']['auts']) |
| 137 | sqn_ms = milenage_auts(opc, k, rand, auts) |
| 138 | if sqn_ms is not False: |
| 139 | print("SQN_MS = %s" % sqn_ms.hex()) |
| 140 | sqn_ms_int = unpack48(sqn_ms) |
| 141 | # we assume an IND bit-length of 5 here |
| 142 | sqn = pack48(sqn_ms_int + (1 << 5)) |
| 143 | else: |
| 144 | raise RuntimeError("AUTS auth failure during re-sync?!?") |
| 145 | elif 'successful_3g_authentication' in resp_json: |
| 146 | auth_res = resp_json['successful_3g_authentication'] |
| 147 | assert bytes.fromhex(auth_res['res']) == t['res'] |
| 148 | assert bytes.fromhex(auth_res['ck']) == t['ck'] |
| 149 | assert bytes.fromhex(auth_res['ik']) == t['ik'] |
| 150 | # we assume an IND bit-length of 5 here |
| 151 | sqn = pack48(unpack48(sqn) + (1 << 5)) |
| 152 | else: |
| 153 | raise RuntimeError("Auth failure") |
| 154 | |
| 155 | |
Harald Welte | df3d01b | 2021-11-03 12:33:42 +0100 | [diff] [blame] | 156 | def main(argv): |
| 157 | global server_port, server_host, verbose |
| 158 | |
| 159 | parser = argparse.ArgumentParser() |
| 160 | parser.add_argument("-H", "--host", help="Host to connect to", default="localhost") |
| 161 | parser.add_argument("-p", "--port", help="TCP port to connect to", default=8000) |
| 162 | parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0) |
| 163 | parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0) |
| 164 | subp = parser.add_subparsers() |
| 165 | |
| 166 | auth_p = subp.add_parser('auth', help='UMTS AKA Authentication') |
| 167 | auth_p.add_argument("-c", "--count", help="Auth count", type=int, default=10) |
| 168 | auth_p.add_argument("-k", "--key", help="Secret key K (hex)", type=str, required=True) |
| 169 | auth_p.add_argument("-o", "--opc", help="Secret OPc (hex)", type=str, required=True) |
| 170 | auth_p.add_argument("-a", "--amf", help="AMF Field (hex)", type=str, default="0000") |
| 171 | auth_p.add_argument("-s", "--sqn", help="SQN Field (hex)", type=str, default="000000000000") |
| 172 | auth_p.set_defaults(func=main_auth) |
| 173 | |
| 174 | info_p = subp.add_parser('info', help='Information about the Card') |
| 175 | info_p.set_defaults(func=main_info) |
| 176 | |
| 177 | args = parser.parse_args() |
| 178 | server_host = args.host |
| 179 | server_port = args.port |
| 180 | verbose = args.verbose |
| 181 | args.func(args) |
| 182 | |
| 183 | |
Harald Welte | c781ab8 | 2021-05-23 11:50:19 +0200 | [diff] [blame] | 184 | if __name__ == "__main__": |
| 185 | main(sys.argv) |