diff --git a/NEWS b/NEWS index 0f88f5ce..39fd0904 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,8 @@ - 0.123 + - IMPORTANT: ssl certificate checks are actually performed now to + prevent man-in-the-middle-attacks. python-m2crypto is needed to + make this work. Certificate checks can be turned off per server + via 'sslcertck = 0' in .oscrc. - 'osc list' option -D now only limits non-'new' requests. In state 'new' all are shown. - suggest 'osc list' --bugowner option. Not implemented. - implemented 'osc ls .' to take proj/pack name from current directory. diff --git a/osc/babysitter.py b/osc/babysitter.py index 32260f09..81a1b67c 100644 --- a/osc/babysitter.py +++ b/osc/babysitter.py @@ -7,6 +7,14 @@ import sys import signal from osc import oscerr from urllib2 import URLError, HTTPError +from oschttps import NoSecureSSLError +try: + from M2Crypto.SSL.Checker import SSLVerificationError + from M2Crypto.SSL import SSLError as SSLError +except: + SSLError = None + SSLVerificationError = None + try: # import as RPMError because the class "error" is too generic from rpm import error as RPMError @@ -138,6 +146,18 @@ def run(prg): print >>sys.stderr, e return 1 + except SSLError, e: + print >>sys.stderr, "SSL Error:", e + return 1 + + except SSLVerificationError, e: + print >>sys.stderr, "Certificate Verification Error:", e + return 1 + + except NoSecureSSLError, e: + print >>sys.stderr, e + return 1 + except OSError, e: print >>sys.stderr, e return 1 diff --git a/osc/conf.py b/osc/conf.py index a26b29e1..cf2778f6 100644 --- a/osc/conf.py +++ b/osc/conf.py @@ -35,6 +35,7 @@ The configuration dictionary could look like this: """ import OscConfigParser from osc import oscerr +from oschttps import NoSecureSSLError GENERIC_KEYRING = False GNOME_KEYRING = False @@ -72,6 +73,7 @@ DEFAULTS = { 'apiurl': 'https://api.opensuse.org', 'build-memory' : '',# required for VM builds 'build-swap' : '', # optional for VM builds + 'sslcertck': '1', 'debug': '0', 'http_debug': '0', 'traceback': '0', @@ -269,13 +271,51 @@ def get_apiurl_usr(apiurl): % (apiurl, config['user']) return config['user'] + +def verify_cb(ok, store): + # XXX: this is not really smart. It only detects one error. + # Potentially in the chain which is not that useful to the user. + # We should do this after the ssl handshake. + if(not ok): + err = store.get_error() + cert = store.get_current_cert() + print "*** Certificate verify failed (depth=%s) ***" % store.get_error_depth() + print "Subject: ", cert.get_subject() + print "Issuer: ", cert.get_issuer() + print "Fingerprint: ", cert.get_fingerprint() + print "Valid: ", cert.get_not_before(), "-", cert.get_not_before() + try: + import M2Crypto.Err + reason = M2Crypto.Err.get_x509_verify_error(err) + print "Reason: ", reason + except: + pass + while True: + r = raw_input("continue anyways (y/p/N)? ") + if r == 'y': + return 1 + elif r == 'p': + print cert.as_text() + else: + break + return ok + def init_basicauth(config): """initialize urllib2 with the credentials for Basic Authentication""" from osc.core import __version__ - import os, urllib2 + import os import cookielib + if config['api_host_options'][config['apiurl']]['sslcertck'] != '0': + try: + from M2Crypto import m2urllib2, SSL + except: + raise NoSecureSSLError("M2Crypto is needed to access %s in a secure way.\nPlease install python-m2crypto." % config['apiurl']) + + import urllib2 + + global cookiejar # HTTPS proxy is not supported by urllib2. It only leads to an error @@ -308,7 +348,24 @@ def init_basicauth(config): #print 'Unable to create cookiejar file: \'%s\'. Using RAM-based cookies.' % cookie_file cookiejar = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar), authhandler) + + if config['api_host_options'][config['apiurl']]['sslcertck'] != '0': + cafile = capath = None + if 'capath' in config['api_host_options'][config['apiurl']]: + capath = config['api_host_options'][config['apiurl']]['capath'] + if 'cafile' in config['api_host_options'][config['apiurl']]: + cafile = config['api_host_options'][config['apiurl']]['cafile'] + if not cafile and not capath: + capath = '/etc/ssl/certs' + ctx = SSL.Context('sslv3') + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9, callback=verify_cb) + if ctx.load_verify_locations(capath=capath, cafile=cafile) != 1: raise Exception('No CA certificates found') + opener = m2urllib2.build_opener(ctx, urllib2.HTTPCookieProcessor(cookiejar), authhandler) + else: + import sys; + print >>sys.stderr, "WARNING: SSL certificate checks disabled. Connection is insecure!\n" + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar), authhandler) + urllib2.install_opener(opener) opener.addheaders = [('User-agent', 'osc/%s' % __version__)] @@ -565,8 +622,14 @@ def get_config(override_conffile = None, api_host_options[apiurl] = { 'user': user, 'pass': password, 'http_headers': http_headers} - if cp.has_option(url, 'email'): - api_host_options[apiurl]['email'] = cp.get(url, 'email') + + optional = ('email', 'sslcertck', 'cafile', 'capath') + for key in optional: + if cp.has_option(url, key): + api_host_options[apiurl][key] = cp.get(url, key) + + if not 'sslcertck' in api_host_options[apiurl]: + api_host_options[apiurl]['sslcertck'] = 1 # add the auth data we collected to the config dict config['api_host_options'] = api_host_options diff --git a/osc/oschttps.py b/osc/oschttps.py new file mode 100644 index 00000000..f1a50f1b --- /dev/null +++ b/osc/oschttps.py @@ -0,0 +1,7 @@ +#!/usr/bin/python + +class NoSecureSSLError(Exception): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return self.msg