From 2f573bbb106e664ddb726ee8b34135513f75c7d4 Mon Sep 17 00:00:00 2001 From: Shane Kearns Date: Tue, 20 Oct 2015 19:28:03 +0100 Subject: [PATCH] python api: allow control over the server certificate verification The --insecure option ("insecure=true" in .zuliprc) disables verification entirely, similar to other tools like curl. The --cert_bundle ("cert_bundle=" in .zuliprc) allows a file to be specified containing the CA certificates to verify against. When using self-signed certificates, the server's public key can be used as the only cerificate in the file. This change incidentally fixes an issue where the "site" parameter in .zuliprc was ignored when specifying --user and --api-key on the command line. Fixes: #104 --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++-- zulip/__init__.py | 58 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5fb82a5..c9c6b84 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,24 @@ file is as follows: key= email= site= + insecure= + cert_bundle= + +If omitted, these settings have the following defaults: + + site=https://api.zulip.com + insecure=false + cert_bundle= Alternatively, you may explicitly use "--user" and "--api-key" in our examples, which is especially useful if you are running several bots -which share a home directory. There is also a "--site" option for -setting the Zulip server on the command line. +which share a home directory. + +The command line equivalents for other configuration options are: + + --site= + --insecure + --cert-bundle= You can obtain your Zulip API key, create bots, and manage bots all from your Zulip [settings page](https://zulip.com/#settings). @@ -101,3 +114,46 @@ Alternatively, if you don't want to use your ~/.zuliprc file: --api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 \ hamlet@example.com cordelia@example.com -m \ "Conscience doth make cowards of us all." + +#### Working with an untrusted server certificate + +If your server has either a self-signed certificate, or a certificate signed +by a CA that you don't wish to globally trust then by default the API will +fail with an SSL verification error. + +You can add `insecure=true` to your .zuliprc file. + + [api] + site=https://zulip.example.com + insecure=true + +This disables verification of the server certificate, so connections are +encrypted but unauthenticated. This is not secure, but may be good enough +for a development environment. + + +You can explicitly trust the server certificate using `cert_bundle=` +in your .zuliprc file. + + [api] + site=https://zulip.example.com + cert_bundle=/home/bots/certs/zulip.example.com.crt + +You can also explicitly trust a different set of Certificate Authorities from +the default bundle that is trusted by Python. For example to trust a company +internal CA. + + [api] + site=https://zulip.example.com + cert_bundle=/home/bots/certs/example.com.ca-bundle + +Save the server certificate (or the CA certificate) in its own file, +converting to PEM format first if necessary. +Verify that the certificate you have saved is the same as the one on the +server. + +The `cert_bundle` option trusts the server / CA certificate only for +interaction with the zulip site, and is relatively secure. + +Note that a certificate bundle is merely one or more certificates combined +into a single file. diff --git a/zulip/__init__.py b/zulip/__init__.py index c0ac410..d8d8a9e 100644 --- a/zulip/__init__.py +++ b/zulip/__init__.py @@ -117,6 +117,19 @@ def generate_option_group(parser, prefix=''): default=None, dest="zulip_client", help=optparse.SUPPRESS_HELP) + group.add_option('--insecure', + action='store_true', + dest='insecure', + help='''Do not verify the server certificate. + The https connection will not be secure.''') + group.add_option('--cert-bundle', + action='store', + dest='cert_bundle', + help='''Specify a file containing either the + server certificate, or a set of trusted + CA certificates. This will be used to + verify the server's identity. All + certificates should be PEM encoded.''') return group def init_from_options(options, client=None): @@ -126,7 +139,8 @@ def init_from_options(options, client=None): client = _default_client() return Client(email=options.zulip_email, api_key=options.zulip_api_key, config_file=options.zulip_config_file, verbose=options.verbose, - site=options.zulip_site, client=client) + site=options.zulip_site, client=client, + cert_bundle=options.cert_bundle, insecure=options.insecure) def get_default_config_filename(): config_file = os.path.join(os.environ["HOME"], ".zuliprc") @@ -138,15 +152,14 @@ def get_default_config_filename(): class Client(object): def __init__(self, email=None, api_key=None, config_file=None, verbose=False, retry_on_errors=True, - site=None, client=None): + site=None, client=None, + cert_bundle=None, insecure=None): if client is None: client = _default_client() - if None in (api_key, email): - if config_file is None: - config_file = get_default_config_filename() - if not os.path.exists(config_file): - raise RuntimeError("api_key or email not specified and %s does not exist" - % (config_file,)) + + if config_file is None: + config_file = get_default_config_filename() + if os.path.exists(config_file): config = SafeConfigParser() with file(config_file, 'r') as f: config.readfp(f, config_file) @@ -156,6 +169,22 @@ class Client(object): email = config.get("api", "email") if site is None and config.has_option("api", "site"): site = config.get("api", "site") + if cert_bundle is None and config.has_option("api", "cert_bundle"): + cert_bundle = config.get("api", "cert_bundle") + if insecure is None and config.has_option("api", "insecure"): + # Be quite strict about what is accepted so that users don't + # disable security unintentionally. + insecure_setting = config.get("api", "insecure").lower() + if insecure_setting == "true": + insecure = True + elif insecure_setting == "false": + insecure = False + else: + raise RuntimeError("insecure is set to '%s', it must be 'true' or 'false' if it is used in %s" + % (insecure_setting, config_file)) + elif None in (api_key, email): + raise RuntimeError("api_key or email not specified and %s does not exist" + % (config_file,)) self.api_key = api_key self.email = email @@ -175,6 +204,17 @@ class Client(object): self.retry_on_errors = retry_on_errors self.client_name = client + if insecure: + self.tls_verification=False + elif cert_bundle is not None: + if not os.path.isfile(cert_bundle): + raise RuntimeError("tls bundle '%s' does not exist" + %(cert_bundle,)) + self.tls_verification=cert_bundle + else: + # Default behavior: verify against system CA certificates + self.tls_verification=True + def get_user_agent(self): vendor = '' vendor_version = '' @@ -249,7 +289,7 @@ class Client(object): urlparse.urljoin(self.base_url, url), auth=requests.auth.HTTPBasicAuth(self.email, self.api_key), - verify=True, timeout=90, + verify=self.tls_verification, timeout=90, headers={"User-agent": self.get_user_agent()}, **kwargs)