ls_ssltrust_fixer_p2.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. #!/usr/bin/env python
  2. '''
  3. Program: ls_ssltrust_fixer
  4. Attempt to automate https://kb.vmware.com/s/article/2121701
  5. with intentions to scan against certificate mismatch on service registrations and fixing the mismatch
  6. Scope: Scan for mismatch, fix the mismatch based on scan result as second step
  7. Authors: Jishnu Surendran Thankamani (jishnut@vmware.com), Ramprasad K.S. (ramprasad@vmware.com)
  8. Copyright: 2017 Vmware Inc
  9. '''
  10. import lstoolutil
  11. import ssl
  12. import socket
  13. import re
  14. import hashlib
  15. import base64
  16. import logging
  17. import os,errno
  18. import argparse
  19. import getpass
  20. import sys
  21. import subprocess
  22. ConnectFailure_ct=0
  23. ConnectFailure_nodes=[]
  24. certcache = {}
  25. logger = None
  26. logdir=os.environ["VMWARE_LOG_DIR"]+ os.path.sep +"ls_ssltrust_fixer" + os.path.sep
  27. #Move lstool_communicate function here to control logging
  28. def lstoolcommunicate(argv, stdout=subprocess.PIPE):
  29. """
  30. Lookup service client tool
  31. """
  32. log4jcfile = logdir + 'log4j.conf'
  33. with open(log4jcfile, "w") as log4jcfile_fh:
  34. log4jcfile_fh.write("log4j.rootLogger=OFF")
  35. java = lstoolutil._get_java()
  36. javacpath = lstoolutil._get_classpath()
  37. javasec = lstoolutil._get_java_security_properties()
  38. cmd = [java,
  39. "-Djava.security.properties=%s" % javasec,
  40. "-cp",
  41. javacpath,
  42. "-Dlog4j.configuration=file:%s" % log4jcfile]
  43. cmd.append("com.vmware.vim.lookup.client.tool.LsTool")
  44. cmd += argv
  45. process = subprocess.Popen(cmd, stdout=stdout)
  46. stdout, _ = process.communicate(None)
  47. return process.returncode, stdout
  48. def right_psc(id,psctosite,idtosite, psc_blacklist):
  49. thesite = None
  50. for site in psctosite:
  51. if ((psctosite[site] == idtosite.get(id)) and (site not in psc_blacklist)):
  52. thesite = site
  53. break
  54. return thesite
  55. def read_topology(lsout):
  56. psctosite={}
  57. idtosite={}
  58. id = site = ""
  59. for line in lsout.splitlines():
  60. if "Service ID" in line:
  61. (dummy, id) = line.split(': ', 1)
  62. elif "Site ID" in line:
  63. (dummy, site) = line.split(': ', 1)
  64. idtosite[id] = site
  65. elif (("URL" in line) and ("sso-adminserver" in line)):
  66. (dummy, url) = line.split(': ', 1)
  67. url = url.split('//', 2)[1].split('/')[0].split(':')[0]
  68. psctosite[url] = site
  69. return(psctosite,idtosite)
  70. def _findFirstMatch(lines, pat):
  71. idx = 0
  72. for line in lines:
  73. if re.match(pat, line):
  74. return (line, idx)
  75. break
  76. idx = idx + 1
  77. return (None, -1)
  78. def _modify_ep_certs(oldspec, newspec, newCert):
  79. update_ct = 0
  80. ssltrust_ct = 0
  81. oldlines = []
  82. newlines = []
  83. with open(oldspec,"r") as oldspec_fh:
  84. lines = oldspec_fh.read().splitlines()
  85. for line in lines:
  86. if (line.find('ssltrust') == -1):
  87. newlines.append(line)
  88. else:
  89. (key,oldcert) = line.split('=', 1)
  90. newlines.append('{0}={1}'.format(key, newCert.replace('\\', '\\\\')))
  91. update_ct = update_ct + 1
  92. with open(newspec,"w") as newspec_fh:
  93. newspec_fh.write("\n".join(newlines))
  94. return update_ct
  95. def parseopts(args):
  96. '''Parse the command line options'''
  97. parser = argparse.ArgumentParser()
  98. required_set = parser.add_argument_group('required')
  99. required_set.add_argument('-f', '--function', dest='function', help='scan or fix', default = '', required=True)
  100. return parser.parse_args(args)
  101. def get_cur_cert(spec):
  102. global ConnectFailure_ct
  103. global ConnectFailure_nodes
  104. newcert = None
  105. with open(spec, "r") as spec_fh:
  106. for line in spec_fh.read().splitlines():
  107. if "endpoint0.url" in line:
  108. url = line.split('=', 1)[1].split('//', 2)[1].split('/')[0].split(':')[0]
  109. if (url=="localhost"):
  110. endpointurl="endpoint1.url"
  111. else:
  112. endpointurl="endpoint0.url"
  113. with open(spec, "r") as spec_fh:
  114. for line in spec_fh.read().splitlines():
  115. if endpointurl in line:
  116. url = line.split('=', 1)[1].split('//', 2)[1].split('/')[0].split(':')[0]
  117. print("FQDN used to retrieve current certificate:"+url)
  118. if (url=="localhost"):
  119. print("First and second end points found to be using localhost as fqdn - manually new cert and update .NewCert file before fix")
  120. break
  121. port = 443
  122. endpoint = "{0}:{1}".format(url, port)
  123. if (endpoint in certcache):
  124. logger.debug("Using cached certificate for %s", endpoint)
  125. newcert = certcache[endpoint]
  126. else:
  127. logger.debug("Retreiving certificate for %s", endpoint)
  128. conn = None
  129. newcert = None
  130. try:
  131. conn = socket.create_connection((url, port), timeout=5)
  132. sock = ssl.wrap_socket(conn)
  133. current_cert = sock.getpeercert(True)
  134. newcert = (ssl.DER_cert_to_PEM_cert(current_cert))
  135. certcache[endpoint] = newcert
  136. except Exception:
  137. logger.error("**Failed to get in use certificate from node %s:%s**", url,port)
  138. ConnectFailure_ct = ConnectFailure_ct + 1
  139. if url not in ConnectFailure_nodes: ConnectFailure_nodes.append(url)
  140. finally:
  141. if conn is not None:
  142. conn.shutdown(socket.SHUT_RDWR)
  143. conn.close()
  144. break
  145. return newcert
  146. def read_pem_cert(cert):
  147. pat = "-----BEGIN CERTIFICATE-----([a-zA-Z0-9/+=\r\n]+)-----END CERTIFICATE-----"
  148. m = re.match(pat, cert)
  149. if not m:
  150. raise Exception("Failed to parse cert")
  151. return m.group(1).replace("\n", "").replace("\r", "")
  152. def _setupLogging():
  153. try:
  154. os.makedirs(logdir)
  155. except OSError as e:
  156. if e.errno != errno.EEXIST:
  157. raise
  158. loghandle = logging.getLogger('ls_ssltrus_fixer')
  159. fileformatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
  160. consoleformatter = logging.Formatter('%(message)s')
  161. loghandle.setLevel(logging.DEBUG)
  162. fh = logging.FileHandler(logdir+os.path.sep+'ls_ssltrust_fixer.log')
  163. fh.setLevel(logging.DEBUG)
  164. fh.setFormatter(fileformatter)
  165. ch = logging.StreamHandler()
  166. ch.setLevel(logging.INFO)
  167. ch.setFormatter(consoleformatter)
  168. loghandle.addHandler(fh)
  169. loghandle.addHandler(ch)
  170. return loghandle
  171. def get_filename_from_id(id):
  172. specfile = "{0}{1}".format(logdir, id.replace(":","%"))
  173. certfile = "{0}.newcert".format(specfile)
  174. return(specfile, certfile)
  175. def _doScan():
  176. mismatchedIDs=[]
  177. matchedIDs=[]
  178. lsUrl="https://localhost/lookupservice/sdk"
  179. mismatchlistinput = "{0}mismatchIDs".format(logdir)
  180. logger.info("Scan Phase1: Getting service IDs")
  181. rc, ids = lstoolcommunicate(["list","--no-check-cert","--url",lsUrl,"--id-only"])
  182. if (rc != 0):
  183. raise Exception("'lstool get' failed: %d" % rc)
  184. ids = ids.splitlines()
  185. logger.info("Found %d service IDs", len(ids) - 1)
  186. logger.info("Scan Phase2: Getting spec and verifying certicate/trust")
  187. for id in ids:
  188. if not id:
  189. continue
  190. if "_com" in id:
  191. print("skipping validation as external solution on:"+id)
  192. continue
  193. logger.info("Processing ID: %s", id)
  194. logger.debug("Calling get for ID: %s", id)
  195. rc, oldSpec = lstoolcommunicate(["get","--no-check-cert","--url",lsUrl,"--id",id,"--as-spec",])
  196. if (rc != 0):
  197. logger.error("'lstool get' failed for ID: %s", id)
  198. continue
  199. (specfile, certfile) = get_filename_from_id(id)
  200. logger.debug("Creating spec file %s", specfile)
  201. with open(specfile,"w") as specfile_fh:
  202. specfile_fh.write(oldSpec)
  203. logger.debug("Created spec file %s", specfile)
  204. logger.debug("Creating cert file %s", certfile)
  205. logger.debug("Getting certificate for ID: %s", id)
  206. cert=get_cur_cert(specfile)
  207. if(cert):
  208. with open(certfile, "w") as certfile_fh:
  209. certfile_fh.write(cert)
  210. logger.debug("Created cert file %s", certfile)
  211. newcert_parsed=read_pem_cert(cert)
  212. oldcert = ""
  213. for line in oldSpec.splitlines():
  214. if "ssltrust0" in line:
  215. (key, oldcert) = line.split("=", 1)
  216. break
  217. o = hashlib.sha1()
  218. o.update(base64.decodestring(oldcert))
  219. n = hashlib.sha1()
  220. n.update(base64.decodestring(newcert_parsed))
  221. othumb = o.hexdigest().lower()
  222. nthumb = n.hexdigest().lower()
  223. logger.debug("ID: %s Old thumbprint: %s new thumbprint %s", id, othumb, nthumb)
  224. if (othumb == nthumb):
  225. matchedIDs.append(id)
  226. logger.debug("Trust matches the current certificate. Added %s to matchedIDs", id)
  227. else:
  228. mismatchedIDs.append(id)
  229. logger.debug("***Trust DOES NOT match the current certificate***. Added %s to mismatchedIDs", id)
  230. logger.info("")
  231. if len(matchedIDs) !=0:
  232. for id in matchedIDs:
  233. (specfile, certfile) = get_filename_from_id(id)
  234. logger.debug("Matched: id: %s spec: %s cert in use: %s", id, specfile, certfile)
  235. if len(mismatchedIDs) !=0:
  236. logger.warn("***WARNING*** %d Mismatched ID(s) found", len(mismatchedIDs))
  237. with open(mismatchlistinput,"w") as mismatchIDstoFile:
  238. mismatchIDstoFile.write("\n".join(mismatchedIDs))
  239. logger.info("Written mismatched IDs to %s",mismatchlistinput )
  240. logger.info("List of registrations with cert mismatch")
  241. logger.info("****************************************")
  242. for id in mismatchedIDs:
  243. (specfile, certfile) = get_filename_from_id(id)
  244. logger.info("ID: %s\n spec: %s\n cert in use: %s\n", id, specfile, certfile)
  245. logger.warn("Please DOUBLE CHECK the detection before running 'fix'")
  246. logger.warn("NOTE: Partial upgrade state of 5.5 to 6.x is unsupported for this tool- 5.5 web client registration might change")
  247. logger.info("")
  248. else:
  249. mismatchIDstoFile = open(mismatchlistinput,"w")
  250. mismatchIDstoFile.close()
  251. if ConnectFailure_ct!=0:
  252. logger.info("")
  253. logger.info("***WARNING*** %s ID(s) skipped comparison due to connect failure, ignore if node is dead, use KB:2121701 for manual update procedure. Note: Port 443 is hardcoded", str(ConnectFailure_ct))
  254. logger.info("List of node(s) with connect failure")
  255. logger.info("************************************")
  256. for entry in ConnectFailure_nodes:
  257. logger.info(entry)
  258. def _doFix():
  259. update_idct=0
  260. updated_endpoint = 0
  261. psc_Blacklist = []
  262. lstooloutfile = logdir + 'lstooloutput'
  263. lsUrl="https://localhost/lookupservice/sdk"
  264. mismatchlistfile = "{0}mismatchIDs".format(logdir)
  265. logger.info("Fix phase 1: Reading IDs with incorrect certificate from scan results")
  266. logger.info("Using mismatch ID list from: %s", mismatchlistfile)
  267. try:
  268. mismatchlist_fh=open(mismatchlistfile,"r")
  269. mismatchlist=mismatchlist_fh.read().splitlines()
  270. mismatchlist_fh.close()
  271. except:
  272. logger.error("Mismatch ID list file does not exist, Please run tool with 'scan' function")
  273. return
  274. if not mismatchlist:
  275. logger.info("Mismatch ID list file is empty, no registrations to fix")
  276. return
  277. user=raw_input("SSO administrator user (Default:Administrator@vsphere.local):") or "Administrator@vsphere.local"
  278. passwd=getpass.getpass("Password for "+ user + ":")
  279. logger.info("Fix phase 2: Collecting site topology information")
  280. rc, lsoutput = lstoolcommunicate(["list","--no-check-cert","--url",lsUrl])
  281. if (rc != 0):
  282. raise Exception("'lstool get' failed: %d" % rc)
  283. with open(lstooloutfile, "w") as lstool_fh:
  284. lstool_fh.write(lsoutput)
  285. psctosite,idtosite = read_topology(lsoutput)
  286. logger.info("Fix Phase 3: creating new spec file with new ssltrust values and register")
  287. for id in mismatchlist:
  288. logger.info("\nFixing ID: %s",id)
  289. (specfile, certfile) = get_filename_from_id(id)
  290. newspecfile = specfile + ".newspec"
  291. newcert_parsed = cert = None
  292. #cert=get_cur_cert(specfile) #Use this for production
  293. with open(certfile, "r") as certfile_fh: #Debug only
  294. cert = certfile_fh.read()
  295. newcert_parsed=read_pem_cert(cert)
  296. if(cert):
  297. updated=_modify_ep_certs(specfile, newspecfile, newcert_parsed)
  298. logger.info("Updated %d End points with new cert for ID: %s", updated, id)
  299. if updated != 0:
  300. site = right_psc(id, psctosite, idtosite, psc_Blacklist)
  301. rc = -1
  302. while (site):
  303. lsUrl="https://"+site+"/lookupservice/sdk"
  304. logger.info("Re-registering ID: %s using lsURL: %s", id, lsUrl)
  305. try:
  306. rc, _ = lstoolcommunicate(["reregister", "--no-check-cert",
  307. "--url", lsUrl,
  308. "--id", id,
  309. "--spec", newspecfile,
  310. "--user", user,
  311. "--password", passwd,
  312. ])
  313. if rc==0:
  314. update_idct = update_idct + 1
  315. site = None
  316. except:
  317. pass
  318. if (rc != 0):
  319. psc_Blacklist.append(site)
  320. logger.info("Blacklisted PSC at %s as connecting failed", site)
  321. site = right_psc(id, psctosite, idtosite, psc_Blacklist)
  322. if (rc != 0):
  323. logger.error("'lstool reregister' failed for ID: %s with error %d", id, rc)
  324. else:
  325. updated_endpoint = updated_endpoint + updated
  326. logger.info("Fixing ID: %s completed\n",id)
  327. logger.info("*** %d endpoints for %d service IDs updated with current cetificates and trust ***", updated_endpoint, update_idct)
  328. def main():
  329. global ConnectFailure_ct
  330. global ConnectFailure_nodes
  331. opts = parseopts(sys.argv[1:])
  332. if opts.function=="scan":
  333. logger.info("Running function 'scan'")
  334. _doScan()
  335. logger.info("Completed running function 'scan'")
  336. elif opts.function=="fix":
  337. logger.info("Running function 'fix'")
  338. _doFix()
  339. logger.info("Completed running function 'fix'")
  340. else:
  341. logger.error("Unknown Function '%s'. Choose scan/fix", opts.function)
  342. sys.exit()
  343. if __name__ == '__main__':
  344. logger = _setupLogging()
  345. main()