blob: 7da2363f73eecaaef9e18a224be40e857dfd70ac [file] [log] [blame]
Harald Weltec781ab82021-05-23 11:50:19 +02001#!/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
28from typing import Optional, Dict
29
30import sys
31import argparse
32import secrets
33import requests
34
35from CryptoMobile.Milenage import Milenage
36from CryptoMobile.utils import xor_buf
37
38def unpack48(x:bytes) -> int:
39 """Decode a big-endian 48bit number from binary to integer."""
40 return int.from_bytes(x, byteorder='big')
41
42def 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
46def 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
59def 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 Weltedf3d01b2021-11-03 12:33:42 +010076def build_url(suffix:str, base_path="/sim-auth-api/v1") -> str:
Harald Weltec781ab82021-05-23 11:50:19 +020077 """Build an URL from global server_host, server_port, BASE_PATH and suffix."""
Harald Weltedf3d01b2021-11-03 12:33:42 +010078 return "http://%s:%u%s%s" % (server_host, server_port, base_path, suffix)
Harald Weltec781ab82021-05-23 11:50:19 +020079
80
81def 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 Weltedf3d01b2021-11-03 12:33:42 +010093def 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 Weltec781ab82021-05-23 11:50:19 +0200104
105
Harald Weltedf3d01b2021-11-03 12:33:42 +0100106def 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 Weltec781ab82021-05-23 11:50:19 +0200113
Harald Weltec781ab82021-05-23 11:50:19 +0200114
Harald Weltedf3d01b2021-11-03 12:33:42 +0100115def main_auth(args):
Harald Weltec781ab82021-05-23 11:50:19 +0200116 #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 Welte7a401a22021-11-03 12:14:14 +0100130 if not resp.ok:
131 print("<- ERROR %u: %s" % (resp.status_code, resp.text))
132 break
Harald Weltec781ab82021-05-23 11:50:19 +0200133 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 Weltedf3d01b2021-11-03 12:33:42 +0100156def 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 Weltec781ab82021-05-23 11:50:19 +0200184if __name__ == "__main__":
185 main(sys.argv)