Files
Iristyle a000ce8acc feat(ST2.UtilPackages): bump up all packages
- Refresh PackageCache with latest versions of everything
2013-09-16 22:35:46 -04:00

379 lines
12 KiB
Python

import hashlib
import os
import re
import time
import sys
from .cmd import Cli
from .console_write import console_write
from .open_compat import open_compat, read_compat
# Have somewhere to store the CA bundle, even when not running in Sublime Text
try:
import sublime
ca_bundle_dir = None
except (ImportError):
ca_bundle_dir = os.path.join(os.path.expanduser('~'), '.package_control')
if not os.path.exists(ca_bundle_dir):
os.mkdir(ca_bundle_dir)
def find_root_ca_cert(settings, domain):
runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug'))
binary = runner.retrieve_binary()
args = [binary, 's_client', '-showcerts', '-connect', domain + ':443']
result = runner.execute(args, os.path.dirname(binary))
certs = []
temp = []
in_block = False
for line in result.splitlines():
if line.find('BEGIN CERTIFICATE') != -1:
in_block = True
if in_block:
temp.append(line)
if line.find('END CERTIFICATE') != -1:
in_block = False
certs.append(u"\n".join(temp))
temp = []
# Remove the cert for the domain itself, just leaving the
# chain cert and the CA cert
certs.pop(0)
# Look for the "parent" root CA cert
subject = openssl_get_cert_subject(settings, certs[-1])
issuer = openssl_get_cert_issuer(settings, certs[-1])
cert = get_ca_cert_by_subject(settings, issuer)
cert_hash = hashlib.md5(cert.encode('utf-8')).hexdigest()
return [cert, cert_hash]
def get_system_ca_bundle_path(settings):
"""
Get the filesystem path to the system CA bundle. On Linux it looks in a
number of predefined places, however on OS X it has to be programatically
exported from the SystemRootCertificates.keychain. Windows does not ship
with a CA bundle, but also we use WinINet on Windows, so we don't need to
worry about CA certs.
:param settings:
A dict to look in for `debug` and `openssl_binary` keys
:return:
The full filesystem path to the .ca-bundle file, or False on error
"""
# If the sublime module is available, we bind this value at run time
# since the sublime.packages_path() is not available at import time
global ca_bundle_dir
platform = sys.platform
debug = settings.get('debug')
ca_path = False
if platform == 'win32':
console_write(u"Unable to get system CA cert path since Windows does not ship with them", True)
return False
# OS X
if platform == 'darwin':
if not ca_bundle_dir:
ca_bundle_dir = os.path.join(sublime.packages_path(), 'User')
ca_path = os.path.join(ca_bundle_dir, 'Package Control.system-ca-bundle')
exists = os.path.exists(ca_path)
# The bundle is old if it is a week or more out of date
is_old = exists and os.stat(ca_path).st_mtime < time.time() - 604800
if not exists or is_old:
if debug:
console_write(u"Generating new CA bundle from system keychain", True)
_osx_create_ca_bundle(settings, ca_path)
if debug:
console_write(u"Finished generating new CA bundle at %s" % ca_path, True)
elif debug:
console_write(u"Found previously exported CA bundle at %s" % ca_path, True)
# Linux
else:
# Common CA cert paths
paths = [
'/usr/lib/ssl/certs/ca-certificates.crt',
'/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem'
]
for path in paths:
if os.path.exists(path):
ca_path = path
break
if debug and ca_path:
console_write(u"Found system CA bundle at %s" % ca_path, True)
return ca_path
def get_ca_cert_by_subject(settings, subject):
bundle_path = get_system_ca_bundle_path(settings)
with open_compat(bundle_path, 'r') as f:
contents = read_compat(f)
temp = []
in_block = False
for line in contents.splitlines():
if line.find('BEGIN CERTIFICATE') != -1:
in_block = True
if in_block:
temp.append(line)
if line.find('END CERTIFICATE') != -1:
in_block = False
cert = u"\n".join(temp)
temp = []
if openssl_get_cert_subject(settings, cert) == subject:
return cert
return False
def openssl_get_cert_issuer(settings, cert):
"""
Uses the openssl command line client to extract the issuer of an x509
certificate.
:param settings:
A dict to look in for `debug` and `openssl_binary` keys
:param cert:
A string containing the PEM-encoded x509 certificate to extract the
issuer from
:return:
The cert issuer
"""
runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug'))
binary = runner.retrieve_binary()
args = [binary, 'x509', '-noout', '-issuer']
output = runner.execute(args, os.path.dirname(binary), cert)
return re.sub('^issuer=\s*', '', output)
def openssl_get_cert_name(settings, cert):
"""
Uses the openssl command line client to extract the name of an x509
certificate. If the commonName is set, that is used, otherwise the first
organizationalUnitName is used. This mirrors what OS X uses for storing
trust preferences.
:param settings:
A dict to look in for `debug` and `openssl_binary` keys
:param cert:
A string containing the PEM-encoded x509 certificate to extract the
name from
:return:
The cert subject name, which is the commonName (if available), or the
first organizationalUnitName
"""
runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug'))
binary = runner.retrieve_binary()
args = [binary, 'x509', '-noout', '-subject', '-nameopt',
'sep_multiline,lname,utf8']
result = runner.execute(args, os.path.dirname(binary), cert)
# First look for the common name
cn = None
# If there is no common name for the cert, the trust prefs use the first
# orginizational unit name
first_ou = None
for line in result.splitlines():
match = re.match('^\s+commonName=(.*)$', line)
if match:
cn = match.group(1)
break
match = re.match('^\s+organizationalUnitName=(.*)$', line)
if first_ou is None and match:
first_ou = match.group(1)
continue
# This is the name of the cert that would be used in the trust prefs
return cn or first_ou
def openssl_get_cert_subject(settings, cert):
"""
Uses the openssl command line client to extract the subject of an x509
certificate.
:param settings:
A dict to look in for `debug` and `openssl_binary` keys
:param cert:
A string containing the PEM-encoded x509 certificate to extract the
subject from
:return:
The cert subject
"""
runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug'))
binary = runner.retrieve_binary()
args = [binary, 'x509', '-noout', '-subject']
output = runner.execute(args, os.path.dirname(binary), cert)
return re.sub('^subject=\s*', '', output)
def _osx_create_ca_bundle(settings, destination):
"""
Uses the OS X `security` command line tool to export the system's list of
CA certs from /System/Library/Keychains/SystemRootCertificates.keychain.
Checks the cert names against the trust preferences, ensuring that
distrusted certs are not exported.
:param settings:
A dict to look in for `debug` and `openssl_binary` keys
:param destination:
The full filesystem path to the destination .ca-bundle file
"""
distrusted_certs = _osx_get_distrusted_certs(settings)
# Export the root certs
args = ['/usr/bin/security', 'export', '-k',
'/System/Library/Keychains/SystemRootCertificates.keychain', '-t',
'certs', '-p']
result = Cli(None, settings.get('debug')).execute(args, '/usr/bin')
certs = []
temp = []
in_block = False
for line in result.splitlines():
if line.find('BEGIN CERTIFICATE') != -1:
in_block = True
if in_block:
temp.append(line)
if line.find('END CERTIFICATE') != -1:
in_block = False
cert = u"\n".join(temp)
temp = []
if distrusted_certs:
# If it is a distrusted cert, we move on to the next
cert_name = openssl_get_cert_name(settings, cert)
if cert_name in distrusted_certs:
if settings.get('debug'):
console_write(u'Skipping root certficate %s because it is distrusted' % cert_name, True)
continue
certs.append(cert)
with open_compat(destination, 'w') as f:
f.write(u"\n".join(certs))
def _osx_get_distrusted_certs(settings):
"""
Uses the OS X `security` binary to get a list of admin trust settings,
which is what is set when a user changes the trust setting on a root
certificate. By looking at the SSL policy, we can properly exclude
distrusted certs from out export.
Tested on OS X 10.6 and 10.8
:param settings:
A dict to look in for `debug` key
:return:
A list of CA cert names, where the name is the commonName (if
available), or the first organizationalUnitName
"""
args = ['/usr/bin/security', 'dump-trust-settings', '-d']
result = Cli(None, settings.get('debug')).execute(args, '/usr/bin')
distrusted_certs = []
cert_name = None
ssl_policy = False
for line in result.splitlines():
if line == '':
continue
# Reset for each cert
match = re.match('Cert\s+\d+:\s+(.*)$', line)
if match:
cert_name = match.group(1)
continue
# Reset for each trust setting
if re.match('^\s+Trust\s+Setting\s+\d+:', line):
ssl_policy = False
continue
# We are only interested in SSL policies
if re.match('^\s+Policy\s+OID\s+:\s+SSL', line):
ssl_policy = True
continue
distrusted = re.match('^\s+Result\s+Type\s+:\s+kSecTrustSettingsResultDeny', line)
if ssl_policy and distrusted and cert_name not in distrusted_certs:
if settings.get('debug'):
console_write(u'Found SSL distrust setting for root certificate %s' % cert_name, True)
distrusted_certs.append(cert_name)
return distrusted_certs
class OpensslCli(Cli):
cli_name = 'openssl'
def retrieve_binary(self):
"""
Returns the path to the openssl executable
:return: The string path to the executable or False on error
"""
name = 'openssl'
if os.name == 'nt':
name += '.exe'
binary = self.find_binary(name)
if binary and os.path.isdir(binary):
full_path = os.path.join(binary, name)
if os.path.exists(full_path):
binary = full_path
if not binary:
show_error((u'Unable to find %s. Please set the openssl_binary ' +
u'setting by accessing the Preferences > Package Settings > ' +
u'Package Control > Settings \u2013 User menu entry. The ' +
u'Settings \u2013 Default entry can be used for reference, ' +
u'but changes to that will be overwritten upon next upgrade.') % name)
return False
return binary