tncc-emulate.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. # Juniper/Pulse TNCC emulator
  4. #
  5. # Copyright © 2015-2018 Russ Dill
  6. #
  7. # Author: Russ Dill <russdill@gmail.com>
  8. #
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU Lesser General Public License
  11. # version 2.1, as published by the Free Software Foundation.
  12. #
  13. # This program is distributed in the hope that it will be useful, but
  14. # WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. # Lesser General Public License for more details.
  17. ########################################
  18. #
  19. # Required modules:
  20. # - Mechanize (https://pypi.org/project/mechanize). Tested with v0.4.5
  21. # - For client certificate support and server certificate validation, asn1crypto
  22. # is required (https://github.com/wbond/asn1crypto). Tested with v0.24.0 and v1.3.0
  23. # - For autodetection of network interfaces' hardware/MAC addresses,
  24. # netifaces is required (https://pypi.org/project/netifaces). Tested with v0.10.9
  25. #
  26. # OpenConnect will automatically set the TNCC_HOSTNAME variable when calling this
  27. # script, and will set TNCC_SHA256 to the pin-sha256 hash of the server certificates
  28. # public key (currently not verified).
  29. #
  30. # Environment variables that may need customization (excerpted from
  31. # https://github.com/russdill/juniper-vpn-py/blame/master/README.host_checker):
  32. #
  33. # TNCC_DEVICE_ID: May need to be overridden to match a known value from a computer
  34. # running the official client software (on Windows, obtained from the registry key
  35. # \HKEY_CURRENT_USER\Software\Juniper Networks\Device Id)
  36. #
  37. # TNCC_USER_AGENT: May need to be overridden to match a known value from a computer
  38. # running the official Windows client software. For historical reasons, the default
  39. # value is 'Neoteris NC Http'; the value 'DSClient; PulseLinux' is known to be sent
  40. # by the official Pulse Linux client.
  41. #
  42. # TNCC_FUNK: Set TNCC_FUNK=1 to force the use of client machine identification
  43. # (known as "funk" to Juniper). This identification will include host platform,
  44. # a list of network hardware/MAC addresses, and client certificates requested
  45. # by the server.
  46. #
  47. # TNCC_PLATFORM: override system value (e.g. "Windows 7").
  48. # TNCC_HOSTNAME: override system value (e.g. "laptop1234.bigcorp.com").
  49. # TNCC_HWADDR: override with a comma-separated list of network hardware/MAC
  50. # addresses to report to the server (e.g. "aa:bb:cc:dd:00:21,ee:ff:12:34:45:78").
  51. # The default behavior is to include the all the MAC addresses returned by the
  52. # netifaces module, or to leave blank if this module is not available.
  53. # TNCC_CERTS: a comma-separated list of absolute paths to PEM-formatted client
  54. # certificates to offer to the server
  55. #
  56. ########################################
  57. import sys
  58. import os
  59. import logging
  60. from http.cookiejar import Cookie, CookieJar
  61. import struct
  62. import ssl
  63. import base64
  64. import collections
  65. import zlib
  66. import html.parser as HTMLParser
  67. import socket
  68. import platform
  69. import datetime
  70. import hashlib
  71. import xml.etree.ElementTree
  72. import mechanize
  73. try:
  74. import asn1crypto.pem
  75. import asn1crypto.x509
  76. except ImportError:
  77. asn1crypto = None
  78. try:
  79. import netifaces
  80. except ImportError:
  81. netifaces = None
  82. ssl._create_default_https_context = ssl._create_unverified_context
  83. debug = False
  84. logging.basicConfig(stream=sys.stderr, level=logging.DEBUG if debug else logging.INFO)
  85. MSG_POLICY = 0x58316
  86. MSG_FUNK_PLATFORM = 0x58301
  87. MSG_FUNK = 0xa4c01
  88. def _decode_helper(buf, indent):
  89. ret = collections.defaultdict(list)
  90. while (len(buf) >= 12):
  91. length, cmd, out = decode_packet(buf, indent + " ")
  92. buf = buf[length:]
  93. ret[cmd].append(out)
  94. return ret
  95. # 0013 - Message
  96. def decode_0013(buf, indent):
  97. logging.debug('%scmd 0013 (Message) %d bytes', indent, len(buf))
  98. return _decode_helper(buf, indent)
  99. # 0012 - u32
  100. def decode_0012(buf, indent):
  101. logging.debug('%scmd 0012 (u32) %d bytes', indent, len(buf))
  102. return struct.unpack(">I", buf)
  103. # 0016 - zlib compressed message
  104. def decode_0016(buf, indent):
  105. logging.debug('%scmd 0016 (compressed message) %d bytes', indent, len(buf))
  106. _, compressed = struct.unpack(">I" + str(len(buf) - 4) + "s", buf)
  107. buf = zlib.decompress(compressed)
  108. return _decode_helper(buf, indent)
  109. # 0ce4 - encapsulation
  110. def decode_0ce4(buf, indent):
  111. logging.debug('%scmd 0ce4 (encapsulation) %d bytes', indent, len(buf))
  112. return _decode_helper(buf, indent)
  113. # 0ce5 - string without hex prefixer
  114. def decode_0ce5(buf, indent):
  115. s = struct.unpack(str(len(buf)) + "s", buf)[0]
  116. logging.debug('%scmd 0ce5 (string) %d bytes', indent, len(buf))
  117. s = s.rstrip(b'\0')
  118. logging.debug('%s', s)
  119. return s
  120. # 0ce7 - string with hex prefixer
  121. def decode_0ce7(buf, indent):
  122. id, s = struct.unpack(">I" + str(len(buf) - 4) + "s", buf)
  123. logging.debug('%scmd 0ce7 (id %08x string) %d bytes', indent, id, len(buf))
  124. if s.startswith(b'COMPRESSED:'):
  125. typ, length, data = s.split(b':', 2)
  126. s = zlib.decompress(data)
  127. s = s.rstrip(b'\0')
  128. logging.debug('%s', s)
  129. return (id, s)
  130. # 0cf0 - encapsulation
  131. def decode_0cf0(buf, indent):
  132. logging.debug('%scmd 0cf0 (encapsulation) %d bytes', indent, len(buf))
  133. ret = {}
  134. cmd, _, out = decode_packet(buf, indent + " ")
  135. ret[cmd] = out
  136. return ret
  137. # 0cf1 - string without hex prefixer
  138. def decode_0cf1(buf, indent):
  139. s = struct.unpack(str(len(buf)) + "s", buf)[0]
  140. logging.debug('%scmd 0cf1 (string) %d bytes', indent, len(buf))
  141. s = s.rstrip(b'\0')
  142. logging.debug('%s', s)
  143. return s
  144. # 0cf3 - u32
  145. def decode_0cf3(buf, indent):
  146. ret = struct.unpack(">I", buf)
  147. logging.debug('%scmd 0cf3 (u32) %d bytes - %d', indent, len(buf), ret[0])
  148. return ret
  149. def decode_packet(buf, indent=""):
  150. cmd, _1, _2, length, _3 = struct.unpack(">IBBHI", buf[:12])
  151. if length < 12:
  152. raise Exception("Invalid packet, cmd %04x, _1 %02x, _2 %02x, length %d" % (cmd, _1, _2, length))
  153. data = buf[12:length]
  154. if length % 4:
  155. length += 4 - (length % 4)
  156. decode_function = {
  157. 0x0012: decode_0012,
  158. 0x0013: decode_0013,
  159. 0x0016: decode_0016,
  160. 0x0ce4: decode_0ce4,
  161. 0x0ce5: decode_0ce5,
  162. 0x0ce7: decode_0ce7,
  163. 0x0cf0: decode_0cf0,
  164. 0x0cf1: decode_0cf1,
  165. 0x0cf3: decode_0cf3,
  166. }
  167. if cmd in decode_function:
  168. data = decode_function[cmd](data, indent)
  169. else:
  170. logging.debug('%scmd %04x(%02x:%02x) is unknown, length %d', indent, cmd, _1, _2, length)
  171. data = None
  172. return length, cmd, data
  173. def encode_packet(cmd, align, buf):
  174. align = 4
  175. orig_len = len(buf)
  176. if align > 1 and (len(buf) + 12) % align:
  177. buf += struct.pack(str(align - len(buf) % align) + "x")
  178. return struct.pack(">IBBHI", cmd, 0xc0, 0x00, orig_len + 12, 0x0000583) + buf
  179. # 0013 - Message
  180. def encode_0013(buf):
  181. return encode_packet(0x0013, 4, buf)
  182. # 0012 - u32
  183. def encode_0012(i):
  184. return encode_packet(0x0012, 1, struct.pack("<I", i))
  185. # 0ce4 - encapsulation
  186. def encode_0ce4(buf):
  187. return encode_packet(0x0ce4, 4, buf)
  188. # 0ce5 - string without hex prefixer
  189. def encode_0ce5(s):
  190. return encode_packet(0x0ce5, 1, struct.pack(str(len(s)) + "s", s))
  191. # 0ce7 - string with hex prefixer
  192. def encode_0ce7(s, prefix):
  193. s += b'\0'
  194. return encode_packet(0x0ce7, 1, struct.pack(">I" + str(len(s)) + "sx",
  195. prefix, s))
  196. # 0cf0 - encapsulation
  197. def encode_0cf0(buf):
  198. return encode_packet(0x0cf0, 4, buf)
  199. # 0cf1 - string without hex prefixer
  200. def encode_0cf1(s):
  201. s += b'\0'
  202. return encode_packet(0x0ce5, 1, struct.pack(str(len(s)) + "s", s))
  203. # 0cf3 - u32
  204. def encode_0cf3(i):
  205. return encode_packet(0x0013, 1, struct.pack("<I", i))
  206. class x509cert:
  207. @staticmethod
  208. def decode_names(names):
  209. ret = {}
  210. for name in names.chosen:
  211. for attr in name:
  212. type_dotted = attr['type'].dotted # dotted-quad value (e.g. '2.5.4.10' = organization)
  213. value_native = attr['value'].native # literal string value (e.g. 'Bigcorp Inc.')
  214. ret.setdefault(type_dotted, []).append(value_native)
  215. return ret
  216. def __init__(self, cert_file):
  217. with open(cert_file, 'r') as f:
  218. self.data = f.read()
  219. type_name, headers, der_bytes = asn1crypto.pem.unarmor(self.data.encode())
  220. cert = asn1crypto.x509.Certificate.load(der_bytes)
  221. tbs = cert['tbs_certificate']
  222. self.issuer = self.decode_names(tbs['issuer'])
  223. self.not_before = tbs['validity']['not_before'].native.astimezone(datetime.timezone.utc).replace(tzinfo=None)
  224. self.not_after = tbs['validity']['not_after'].native.astimezone(datetime.timezone.utc).replace(tzinfo=None)
  225. self.subject = self.decode_names(tbs['subject'])
  226. class tncc:
  227. def __init__(self, vpn_host, device_id=None, funk=None, platform=None, hostname=None, mac_addrs=None, certs=None, interval=None, user_agent=None):
  228. self.vpn_host = vpn_host
  229. self.path = '/dana-na/'
  230. self.funk = funk
  231. self.platform = platform
  232. self.hostname = hostname
  233. if mac_addrs is None:
  234. self.mac_addrs = []
  235. else:
  236. self.mac_addrs = mac_addrs
  237. if certs is None:
  238. self.avail_certs = []
  239. else:
  240. self.avail_certs = certs
  241. self.interval = interval
  242. self.deviceid = device_id
  243. self.br = mechanize.Browser()
  244. self.cj = CookieJar()
  245. self.br.set_cookiejar(self.cj)
  246. # Browser options
  247. self.br.set_handle_equiv(True)
  248. self.br.set_handle_redirect(True)
  249. self.br.set_handle_referer(True)
  250. self.br.set_handle_robots(False)
  251. # Follows refresh 0 but not hangs on refresh > 0
  252. self.br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(),
  253. max_time=1)
  254. # Want debugging messages?
  255. if debug:
  256. self.br.set_debug_http(True)
  257. self.br.set_debug_redirects(True)
  258. self.br.set_debug_responses(True)
  259. self.user_agent = user_agent
  260. self.br.addheaders = [('User-agent', self.user_agent)]
  261. def find_cookie(self, name):
  262. for cookie in self.cj:
  263. if cookie.name == name:
  264. return cookie
  265. return None
  266. def set_cookie(self, name, value):
  267. cookie = Cookie(version=0, name=name, value=value,
  268. port=None, port_specified=False, domain=self.vpn_host,
  269. domain_specified=True, domain_initial_dot=False, path=self.path,
  270. path_specified=True, secure=True, expires=None, discard=True,
  271. comment=None, comment_url=None, rest=None, rfc2109=False)
  272. self.cj.set_cookie(cookie)
  273. def parse_response(self):
  274. # Read in key/token fields in HTTP responsedict
  275. response = {}
  276. last_key = ''
  277. for line in self.r.readlines():
  278. line = line.strip().decode()
  279. # Note that msg is too long and gets wrapped, handle it special
  280. if last_key == 'msg' and line:
  281. response['msg'] += line
  282. else:
  283. key = ''
  284. try:
  285. key, val = line.split('=', 1)
  286. response[key] = val
  287. except ValueError:
  288. pass
  289. last_key = key
  290. logging.debug('Parsed response:\n\t%s', '\n\t'.join('%r: %r,' % pair for pair in response.items()))
  291. return response
  292. @staticmethod
  293. def parse_policy_response(msg_data):
  294. # The decompressed data is HTMLish, decode it. The value="" of each
  295. # tag is the data we want.
  296. objs = []
  297. class ParamHTMLParser(HTMLParser.HTMLParser):
  298. @staticmethod
  299. def handle_starttag(tag, attrs):
  300. if tag.lower() == 'param':
  301. for key, value in attrs:
  302. if key.lower() == 'value':
  303. # It's made up of a bunch of key=value pairs separated
  304. # by semicolons
  305. d = {}
  306. for field in value.split(';'):
  307. field = field.strip()
  308. try:
  309. key, value = field.split('=', 1)
  310. d[key] = value
  311. except ValueError:
  312. pass
  313. objs.append(d)
  314. p = ParamHTMLParser()
  315. p.feed(msg_data)
  316. p.close()
  317. return objs
  318. @staticmethod
  319. def parse_funk_response(msg_data):
  320. e = xml.etree.ElementTree.fromstring(msg_data)
  321. req_certs = {}
  322. for cert in e.find('AttributeRequest').findall('CertData'):
  323. dns = {}
  324. cert_id = cert.attrib['Id']
  325. for attr in cert.findall('Attribute'):
  326. name = attr.attrib['Name']
  327. value = attr.attrib['Value']
  328. attr_type = attr.attrib['Type']
  329. if attr_type == 'DN':
  330. dns[name] = dict(n.strip().split('=') for n in value.split(','))
  331. else:
  332. # Unknown attribute type
  333. pass
  334. req_certs[cert_id] = dns
  335. return req_certs
  336. def gen_funk_platform(self):
  337. # We don't know if the xml parser on the other end is fully complaint,
  338. # just format a string like it expects.
  339. msg = "<FunkMessage VendorID='2636' ProductID='1' Version='1' Platform='%s' ClientType='Agentless'> " % self.platform
  340. msg += "<ClientAttributes SequenceID='-1'> "
  341. def add_attr(key, val):
  342. return "<Attribute Name='%s' Value='%s' />" % (key, val)
  343. msg += add_attr('Platform', self.platform)
  344. if self.hostname:
  345. msg += add_attr(self.hostname, 'NETBIOSName') # Reversed
  346. for mac in self.mac_addrs:
  347. msg += add_attr(mac, 'MACAddress') # Reversed
  348. msg += "</ClientAttributes> </FunkMessage>"
  349. return encode_0ce7(msg.encode(), MSG_FUNK_PLATFORM)
  350. def gen_funk_present(self):
  351. msg = "<FunkMessage VendorID='2636' ProductID='1' Version='1' Platform='%s' ClientType='Agentless'> " % self.platform
  352. msg += "<Present SequenceID='0'></Present> </FunkMessage>"
  353. return encode_0ce7(msg.encode(), MSG_FUNK)
  354. def gen_funk_response(self, certs):
  355. msg = "<FunkMessage VendorID='2636' ProductID='1' Version='1' Platform='%s' ClientType='Agentless'> " % self.platform
  356. msg += "<ClientAttributes SequenceID='0'> "
  357. msg += "<Attribute Name='Platform' Value='%s' />" % self.platform
  358. for name, value in certs.items():
  359. msg += "<Attribute Name='%s' Value='%s' />" % (name, value.data.strip())
  360. msg += "<Attribute Name='%s' Value='%s' />" % (name, value.data.strip())
  361. msg += "</ClientAttributes> </FunkMessage>"
  362. return encode_0ce7(msg.encode(), MSG_FUNK)
  363. @staticmethod
  364. def gen_policy_request():
  365. policy_blocks = collections.OrderedDict({
  366. 'policy_request': {
  367. 'message_version': '3'
  368. },
  369. 'esap': {
  370. 'esap_version': 'NOT_AVAILABLE',
  371. 'fileinfo': 'NOT_AVAILABLE',
  372. 'has_file_versions': 'YES',
  373. 'needs_exact_sdk': 'YES',
  374. 'opswat_sdk_version': '3'
  375. },
  376. 'system_info': {
  377. 'os_version': '2.6.2',
  378. 'sp_version': '0',
  379. 'hc_mode': 'userMode'
  380. }
  381. })
  382. msg = ''
  383. for policy_key, policy_val in policy_blocks.items():
  384. v = ''.join(['%s=%s;' % (k, v) for k, v in policy_val.items()])
  385. msg += '<parameter name="%s" value="%s">' % (policy_key, v)
  386. return encode_0ce7(msg.encode(), 0xa4c18)
  387. @staticmethod
  388. def gen_policy_response(policy_objs):
  389. # Make a set of policies
  390. policies = set()
  391. for entry in policy_objs:
  392. if 'policy' in entry:
  393. policies.add(entry['policy'])
  394. # Try to determine on policy name whether the response should be OK
  395. # or NOTOK. Default to OK if we don't know, this may need updating.
  396. msg = ''
  397. for policy in policies:
  398. msg += '\npolicy:%s\nstatus:' % policy
  399. if 'Unsupported' in policy or 'Deny' in policy:
  400. msg += 'NOTOK\nerror:Unknown error'
  401. else:
  402. # Default action, including 'Required'
  403. msg += 'OK\n'
  404. return encode_0ce7(msg.encode(), MSG_POLICY)
  405. def get_cookie(self, dspreauth=None, dssignin=None):
  406. if dspreauth is None or dssignin is None:
  407. self.r = self.br.open('https://' + self.vpn_host)
  408. else:
  409. try:
  410. self.cj.set_cookie(dspreauth)
  411. except Exception:
  412. self.set_cookie('DSPREAUTH', dspreauth)
  413. try:
  414. self.cj.set_cookie(dssignin)
  415. except Exception:
  416. self.set_cookie('DSSIGNIN', dssignin)
  417. inner = self.gen_policy_request()
  418. inner += encode_0ce7(b'policy request\x00v4', MSG_POLICY)
  419. if self.funk:
  420. inner += self.gen_funk_platform()
  421. inner += self.gen_funk_present()
  422. msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5(b'Accept-Language: en') + encode_0cf3(1))
  423. logging.debug('Sending packet -')
  424. decode_packet(msg_raw)
  425. post_attrs = {
  426. 'connID': '0',
  427. 'timestamp': '0',
  428. 'msg': base64.b64encode(msg_raw).decode(),
  429. 'firsttime': '1'
  430. }
  431. if self.deviceid:
  432. post_attrs['deviceid'] = self.deviceid
  433. post_data = ''.join(['%s=%s;' % (k, v) for k, v in post_attrs.items()])
  434. self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data)
  435. # Parse the data returned into a key/value dict
  436. response = self.parse_response()
  437. if 'interval' in response:
  438. m = int(response['interval'])
  439. logging.debug('Got interval of %d minutes', m)
  440. if self.interval is None or self.interval > m * 60:
  441. self.interval = m * 60
  442. # msg has the stuff we want, it's base64 encoded
  443. logging.debug('Receiving packet -')
  444. msg_raw = base64.b64decode(response['msg'])
  445. _1, _2, msg_decoded = decode_packet(msg_raw)
  446. # Within msg, there is a field of data
  447. sub_strings = msg_decoded[0x0ce4][0][0x0ce7]
  448. # Pull the data out of the 'value' key in the htmlish stuff returned
  449. policy_objs = []
  450. req_certs = {}
  451. for str_id, sub_str in sub_strings:
  452. if str_id == MSG_POLICY:
  453. policy_objs += self.parse_policy_response(sub_str.decode())
  454. elif str_id == MSG_FUNK:
  455. req_certs = self.parse_funk_response(sub_str.decode())
  456. if debug:
  457. for obj in policy_objs:
  458. if 'policy' in obj:
  459. logging.debug('policy %s', obj['policy'])
  460. for key, val in obj.items():
  461. if key != 'policy':
  462. logging.debug('\t%s %s', key, val)
  463. # Try to locate the required certificates
  464. certs = {}
  465. for cert_id, req_dns in req_certs.items():
  466. for cert in self.avail_certs:
  467. fail = False
  468. for dn_name, dn_vals in req_dns.items():
  469. if dn_name == 'IssuerDN':
  470. for name, val in dn_vals.items():
  471. if (
  472. name not in cert.issuer
  473. or val not in cert.issuer[name]
  474. ):
  475. fail = True
  476. break
  477. else:
  478. logging.warning('Unknown DN type %s', str(dn_name))
  479. fail = True
  480. if fail:
  481. break
  482. if not fail:
  483. certs[cert_id] = cert
  484. break
  485. if cert_id not in certs:
  486. logging.warning('Could not find certificate for %s', str(req_dns))
  487. inner = b''
  488. if certs:
  489. inner += self.gen_funk_response(certs)
  490. inner += self.gen_policy_response(policy_objs)
  491. msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5(b'Accept-Language: en'))
  492. logging.debug('Sending packet -')
  493. decode_packet(msg_raw)
  494. post_attrs = {
  495. 'connID': '1',
  496. 'msg': base64.b64encode(msg_raw).decode(),
  497. 'firsttime': '1'
  498. }
  499. post_data = ''.join(['%s=%s;' % (k, v) for k, v in post_attrs.items()])
  500. self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data)
  501. # We have a new DSPREAUTH cookie
  502. return self.find_cookie('DSPREAUTH')
  503. class tncc_server:
  504. def __init__(self, s, t):
  505. self.sock = s
  506. self.tncc = t
  507. def process_cmd(self):
  508. buf = self.sock.recv(1024).decode('ascii')
  509. if not buf:
  510. sys.exit(0)
  511. cmd, buf = buf.split('\n', 1)
  512. cmd = cmd.strip()
  513. args = {}
  514. for n in buf.split('\n'):
  515. n = n.strip()
  516. if n:
  517. key, val = n.strip().split('=', 1)
  518. args[key] = val
  519. if cmd == 'start':
  520. cookie = self.tncc.get_cookie(args['Cookie'], args['DSSIGNIN'])
  521. resp = ['200', '3', cookie.value]
  522. if self.tncc.interval is not None:
  523. resp.append(str(self.tncc.interval))
  524. self.sock.send(('\n'.join(resp) + '\n\n').encode('ascii'))
  525. elif cmd == 'setcookie':
  526. self.tncc.get_cookie(args['Cookie'],
  527. self.tncc.find_cookie('DSSIGNIN'))
  528. else:
  529. logging.warning('Unknown command %r', cmd)
  530. def fingerprint_checking_SSLSocket(_fingerprint):
  531. class SSLSocket(ssl.SSLSocket):
  532. fingerprint = _fingerprint
  533. def do_handshake(self):
  534. res = super().do_handshake()
  535. der_bytes = self.getpeercert(True)
  536. cert = asn1crypto.x509.Certificate.load(der_bytes)
  537. pubkey = cert.public_key.dump()
  538. pin_sha256 = base64.b64encode(hashlib.sha256(pubkey).digest()).decode()
  539. if pin_sha256 != self.fingerprint:
  540. raise Exception("Server fingerprint %s does not match expected pin-sha256:%s" % (pin_sha256, self.fingerprint))
  541. return res
  542. return SSLSocket
  543. if __name__ == "__main__":
  544. vpn_host = sys.argv[1]
  545. funk = 'TNCC_FUNK' in os.environ and os.environ['TNCC_FUNK'] != '0'
  546. interval = int(os.environ.get('TNCC_INTERVAL', 0)) or None
  547. platform = os.environ.get('TNCC_PLATFORM', platform.system() + ' ' + platform.release())
  548. user_agent = os.environ.get('TNCC_USER_AGENT', 'Neoteris HC Http')
  549. if 'TNCC_HWADDR' in os.environ:
  550. mac_addrs = [n.strip() for n in os.environ['TNCC_HWADDR'].split(',')]
  551. else:
  552. mac_addrs = []
  553. if netifaces is None:
  554. logging.warning("No netifaces module; mac_addrs will be empty.")
  555. else:
  556. for iface in netifaces.interfaces():
  557. try:
  558. mac = netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr']
  559. except (IndexError, KeyError):
  560. pass
  561. else:
  562. if mac != '00:00:00:00:00:00':
  563. mac_addrs.append(mac)
  564. hostname = os.environ.get('TNCC_HOSTNAME', socket.gethostname())
  565. fingerprint = os.environ.get('TNCC_SHA256')
  566. if not fingerprint:
  567. logging.warning("TNCC_SHA256 not set, will not validate server certificate")
  568. elif not asn1crypto:
  569. logging.warning("asn1crypto module not available, will not validate server certificate")
  570. else:
  571. # For Python <3.7, we monkey-patch ssl.SSLSocket directly, because ssl.SSLContext.sslsocket_class
  572. # isn't available until Python 3.7. For Python 3.7+, we set ssl.SSLContext.sslsocket_class
  573. # to our modified version (which is sort of monkey-patching too).
  574. # (see https://gist.github.com/dlenski/fc42156c00a615f4aa18a6d19d67e208)
  575. if sys.version_info >= (3, 7):
  576. ssl.SSLContext.sslsocket_class = fingerprint_checking_SSLSocket(fingerprint)
  577. else:
  578. ssl.SSLSocket = fingerprint_checking_SSLSocket(fingerprint)
  579. certs = []
  580. if 'TNCC_CERTS' in os.environ:
  581. if asn1crypto:
  582. now = datetime.datetime.utcnow()
  583. for f in os.environ['TNCC_CERTS'].split(','):
  584. cert = x509cert(f.strip())
  585. if now < cert.not_before:
  586. logging.warning('WARNING: %s is not yet valid', f)
  587. if now > cert.not_after:
  588. logging.warning('WARNING: %s is expired', f)
  589. certs.append(cert)
  590. else:
  591. raise Exception('TNCC_CERTS environment variable set, but asn1crypto module is not available')
  592. # \HKEY_CURRENT_USER\Software\Juniper Networks\Device Id
  593. device_id = os.environ.get('TNCC_DEVICE_ID')
  594. t = tncc(vpn_host, device_id, funk, platform, hostname, mac_addrs, certs, interval, user_agent)
  595. sock = socket.fromfd(0, socket.AF_UNIX, socket.SOCK_SEQPACKET)
  596. server = tncc_server(sock, t)
  597. while True:
  598. server.process_cmd()