diff options
author | Adam Goodman <akgood@duosecurity.com> | 2012-06-19 12:06:32 -0400 |
---|---|---|
committer | Adam Goodman <akgood@duosecurity.com> | 2012-06-19 12:06:32 -0400 |
commit | 0b4a0af8343c7a459b2b7cf15ce496c2edfcf80a (patch) | |
tree | a21c89b1a3093d3527784554d6bedb4beee859da | |
parent | 070d9c7d9dac848b0407acd8071f6ab2ff192cfe (diff) | |
download | openvpn-duo-plugin-0b4a0af8343c7a459b2b7cf15ce496c2edfcf80a.tar.gz openvpn-duo-plugin-0b4a0af8343c7a459b2b7cf15ce496c2edfcf80a.zip |
add SSL certificate validation
-rw-r--r-- | LICENSE | 44 | ||||
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | ca_certs.pem | 30 | ||||
-rwxr-xr-x | duo_openvpn.pl | 17 | ||||
-rwxr-xr-x | duo_openvpn.py | 9 | ||||
-rw-r--r-- | https_wrapper.py | 146 |
6 files changed, 254 insertions, 7 deletions
@@ -1,3 +1,28 @@ +duo_openvpn is distributed under the terms of the GNU General Public +License v2 with the following clarification and special exception: + +Linking duo_openvpn statically or dynamically with other modules is +making a combined work based on duo_openvpn. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +In addition, as a special exception, the copyright holders of +duo_openvpn give you permission to combine duo_openvpn with the +included https_wrapper.py, which is distributed under version 2.0 of +the Apache License. You may copy and distribute such a system +following the terms of the GNU GPL for duo_openvpn and the licenses of +the other code concerned, provided that you include the source code of +that other code when and as the GNU GPL requires distribution of +source code. + +Note that people who make modified versions of duo_openvpn are not +obligated to grant this special exception for their modified versions; +it is their choice whether to do so. The GNU General Public License +gives permission to release a modified version without this exception; +this exception also makes it possible to release a modified version +which carries forward this exception. + + GNU GENERAL PUBLIC LICENSE Version 2, June 1991 @@ -338,3 +363,22 @@ proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. + +--- + +https_wrapper.py + +Copyright 2007 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + @@ -11,14 +11,23 @@ duo_openvpn.o: duo_openvpn.c gcc $(CFLAGS) -fPIC -c duo_openvpn.c duo_openvpn.so: duo_openvpn.o - gcc -fPIC -shared -Wl,-soname,duo_openvpn.so -o duo_openvpn.so duo_openvpn.o -lc + gcc -fPIC -shared -Wl,-soname,duo_openvpn.so -o duo_openvpn.so duo_openvpn.o -lc install: duo_openvpn.so mkdir -p /opt/duo cp duo_openvpn.so /opt/duo - cp $(SCRIPT_NAME) /opt/duo chmod 755 /opt/duo/duo_openvpn.so - chmod 755 /opt/duo/$(SCRIPT_NAME) + cp ca_certs.pem /opt/duo + chmod 644 /opt/duo/ca_certs.pem +ifdef USE_PERL + cp duo_openvpn.pl /opt/duo + chmod 755 /opt/duo/duo_openvpn.pl +else + cp duo_openvpn.py /opt/duo + cp https_wrapper.py /opt/duo + chmod 755 /opt/duo/duo_openvpn.py + chmod 644 /opt/duo/https_wrapper.py +endif uninstall: rm -rf /opt/duo diff --git a/ca_certs.pem b/ca_certs.pem new file mode 100644 index 0000000..5626757 --- /dev/null +++ b/ca_certs.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UE +BhMCVVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50 +cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UE +AxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQsw +CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3 +dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlh +Yi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVkMTow +OAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0 +VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHIN +iC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3wkrYKZImZNHk +mGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcwggHT +MBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHY +pIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChs +aW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBM +aW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNo +dHRwOi8vd3d3LmVudHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAi +gA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMC +AQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYE +FPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9 +B0EABAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKn +CqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2Zcgx +xufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd2cNgQ4xYDiKWL2KjLB+6 +rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- diff --git a/duo_openvpn.pl b/duo_openvpn.pl index aaf962f..5ca4e0a 100755 --- a/duo_openvpn.pl +++ b/duo_openvpn.pl @@ -9,6 +9,7 @@ use MIME::Base64; use JSON::XS; use Digest::HMAC_SHA1 qw(hmac_sha1_hex); use Data::Dumper; +use File::Spec; $Data::Dumper::Indent = 0; $Data::Dumper::Terse = 1; @@ -38,10 +39,18 @@ if (not $ikey or not $skey or not $host) { failure(); } +my $ca_certs = get_ca_certs(); + preauth(); auth(); failure(); +sub get_ca_certs { + my $abspath = File::Spec->rel2abs(__FILE__); + my ($volume, $directories, $file) = File::Spec->splitpath($abspath); + + return File::Spec->catpath($volume, $directories, 'ca_certs.pem'); +} sub canonicalize { my $host = shift; @@ -72,7 +81,13 @@ sub sign { sub call { my ($ikey, $skey, $host, $path, $kwargs) = @_; - my $ua = LWP::UserAgent->new(); + my $ssl_opts = { + verify_hostname => 1, + SSL_ca_file => $ca_certs, + SSL_ca_path => undef + }; + + my $ua = LWP::UserAgent->new(ssl_opts => $ssl_opts); $ua->default_header( 'Authorization' => sign($ikey, $skey, $host, $path, $kwargs), diff --git a/duo_openvpn.py b/duo_openvpn.py index 95c5f04..90c0b49 100755 --- a/duo_openvpn.py +++ b/duo_openvpn.py @@ -5,13 +5,16 @@ # Copyright 2011 Duo Security, Inc. # -import os, sys, urllib, hashlib, httplib, hmac, base64, json, syslog +import os, sys, urllib, hashlib, hmac, base64, json, syslog +from https_wrapper import CertValidatingHTTPSConnection API_RESULT_AUTH = 'auth' API_RESULT_ALLOW = 'allow' API_RESULT_DENY = 'deny' API_RESULT_ENROLL = 'enroll' +ca_certs = os.path.join(os.path.dirname(__file__), 'ca_certs.pem') + def canonicalize(method, host, uri, params): canon = [method.upper(), host.lower(), uri] @@ -40,12 +43,12 @@ def call(ikey, skey, host, method, path, **kwargs): body = None uri = path + '?' + urllib.urlencode(kwargs, doseq=True) - conn = httplib.HTTPSConnection(host, 443) + conn = CertValidatingHTTPSConnection(host, 443, ca_certs=ca_certs) conn.request(method, uri, body, headers) response = conn.getresponse() data = response.read() conn.close() - + return (response.status, response.reason, data) def api(ikey, skey, host, method, path, **kwargs): diff --git a/https_wrapper.py b/https_wrapper.py new file mode 100644 index 0000000..c3a9ce3 --- /dev/null +++ b/https_wrapper.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# +# Adapted from: +# https://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py +# +# Copyright 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Extensions to allow HTTPS requests with SSL certificate validation.""" + + +import httplib +import re +import socket +import urllib2 +import ssl + + +class InvalidCertificateException(httplib.HTTPException): + """Raised when a certificate is provided with an invalid hostname.""" + + def __init__(self, host, cert, reason): + """Constructor. + + Args: + host: The hostname the connection was made to. + cert: The SSL certificate (as a dictionary) the host returned. + """ + httplib.HTTPException.__init__(self) + self.host = host + self.cert = cert + self.reason = reason + + def __str__(self): + return ('Host %s returned an invalid certificate (%s): %s\n' + 'To learn more, see ' + 'http://code.google.com/appengine/kb/general.html#rpcssl' % + (self.host, self.reason, self.cert)) + +class CertValidatingHTTPSConnection(httplib.HTTPConnection): + """An HTTPConnection that connects over SSL and validates certificates.""" + + default_port = httplib.HTTPS_PORT + + def __init__(self, host, port=None, key_file=None, cert_file=None, + ca_certs=None, strict=None, **kwargs): + """Constructor. + + Args: + host: The hostname. Can be in 'host:port' form. + port: The port. Defaults to 443. + key_file: A file containing the client's private key + cert_file: A file containing the client's certificates + ca_certs: A file contianing a set of concatenated certificate authority + certs for validating the server against. + strict: When true, causes BadStatusLine to be raised if the status line + can't be parsed as a valid HTTP/1.0 or 1.1 status line. + """ + httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs) + self.key_file = key_file + self.cert_file = cert_file + self.ca_certs = ca_certs + if self.ca_certs: + self.cert_reqs = ssl.CERT_REQUIRED + else: + self.cert_reqs = ssl.CERT_NONE + + def _GetValidHostsForCert(self, cert): + """Returns a list of valid host globs for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + Returns: + list: A list of valid host globs. + """ + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def _ValidateCertificateHostname(self, cert, hostname): + """Validates that a given hostname is valid for an SSL certificate. + + Args: + cert: A dictionary representing an SSL certificate. + hostname: The hostname to test. + Returns: + bool: Whether or not the hostname is valid for this certificate. + """ + hosts = self._GetValidHostsForCert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + + def connect(self): + "Connect to a host on a given (SSL) port." + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.host, self.port)) + self.sock = ssl.wrap_socket(sock, keyfile=self.key_file, + certfile=self.cert_file, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs) + if self.cert_reqs & ssl.CERT_REQUIRED: + cert = self.sock.getpeercert() + hostname = self.host.split(':', 0)[0] + if not self._ValidateCertificateHostname(cert, hostname): + raise InvalidCertificateException(hostname, cert, 'hostname mismatch') + + +class CertValidatingHTTPSHandler(urllib2.HTTPSHandler): + """An HTTPHandler that validates SSL certificates.""" + + def __init__(self, **kwargs): + """Constructor. Any keyword args are passed to the httplib handler.""" + urllib2.HTTPSHandler.__init__(self) + self._connection_args = kwargs + + def https_open(self, req): + def http_class_wrapper(host, **kwargs): + full_kwargs = dict(self._connection_args) + full_kwargs.update(kwargs) + return CertValidatingHTTPSConnection(host, **full_kwargs) + try: + return self.do_open(http_class_wrapper, req) + except urllib2.URLError, e: + if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: + raise InvalidCertificateException(req.host, '', + e.reason.args[1]) + raise + + https_request = urllib2.HTTPSHandler.do_request_ |