diff --git a/websockify/auth_plugins.py b/websockify/auth_plugins.py index 93f1385..dd6a9e8 100644 --- a/websockify/auth_plugins.py +++ b/websockify/auth_plugins.py @@ -81,3 +81,23 @@ class ExpectOrigin(object): origin = headers.get('Origin', None) if origin is None or origin not in self.source: raise InvalidOriginError(expected=self.source, actual=origin) + +class ClientCertAuth(object): + """Verifies client by SSL certificate. Specify src as whitespace separated list of common names.""" + + def __init__(self, src=None): + if src is None: + self.source = [] + else: + self.source = src.split() + + def authenticate(self, headers, target_host, target_port): + try: + if (headers.get('SSL_CLIENT_S_DN_CN') not in self.source): + raise AuthenticationError(response_code=403) + except AuthenticationError: + # re-raise AuthenticationError (raised by common name not in configured source list) + raise + except: + # deny access in case any error occurs (i.e. no data provided) + raise AuthenticationError(response_code=403) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 6fd773a..09feee3 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -60,6 +60,20 @@ Traffic Legend: self.server.target_port = port if self.server.auth_plugin: + + try: + # get client certificate data + client_cert_data = self.request.getpeercert() + # extract subject information + client_cert_subject = client_cert_data['subject'] + # flatten data structure + client_cert_subject = dict([x[0] for x in client_cert_subject]) + # add common name to headers (apache +StdEnvVars style) + self.headers['SSL_CLIENT_S_DN_CN'] = client_cert_subject['commonName'] + except: + # not a SSL connection or client presented no certificate with valid data + pass + try: self.server.auth_plugin.authenticate( headers=self.headers, target_host=self.server.target_host, @@ -392,6 +406,12 @@ def websockify_init(): help="disallow non-encrypted client connections") parser.add_option("--ssl-target", action="store_true", help="connect to SSL target as SSL client") + parser.add_option("--verify-client", action="store_true", + help="require encrypted client to present a valid certificate") + parser.add_option("--cafile", metavar="FILE", + help="file of concatenated certificates of authorities trusted " + "for validating clients (only effective with --verify-client). " + "If omitted, system default list of CAs is used.") parser.add_option("--unix-target", help="connect to unix socket target", metavar="FILE") parser.add_option("--inetd", diff --git a/websockify/websockifyserver.py b/websockify/websockifyserver.py index 1b5b15e..2c8b745 100644 --- a/websockify/websockifyserver.py +++ b/websockify/websockifyserver.py @@ -319,6 +319,7 @@ class WebSockifyServer(object): def __init__(self, RequestHandlerClass, listen_fd=None, listen_host='', listen_port=None, source_is_ipv6=False, verbose=False, cert='', key='', ssl_only=None, + verify_client=False, cafile=None, daemon=False, record='', web='', file_only=False, run_once=False, timeout=0, idle_timeout=0, traffic=False, @@ -333,6 +334,7 @@ class WebSockifyServer(object): self.listen_port = listen_port self.prefer_ipv6 = source_is_ipv6 self.ssl_only = ssl_only + self.verify_client = verify_client self.daemon = daemon self.run_once = run_once self.timeout = timeout @@ -352,13 +354,15 @@ class WebSockifyServer(object): # Make paths settings absolute self.cert = os.path.abspath(cert) - self.key = self.web = self.record = '' + self.key = self.web = self.record = self.cafile = '' if key: self.key = os.path.abspath(key) if web: self.web = os.path.abspath(web) if record: self.record = os.path.abspath(record) + if cafile: + self.cafile = os.path.abspath(cafile) if self.web: os.chdir(self.web) @@ -518,7 +522,6 @@ class WebSockifyServer(object): """ ready = select.select([sock], [], [], 3)[0] - if not ready: raise self.EClose("ignoring socket not ready") # Peek, but do not read the data so that we have a opportunity @@ -538,11 +541,16 @@ class WebSockifyServer(object): % self.cert) retsock = None try: - retsock = ssl.wrap_socket( + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(certfile=self.cert, keyfile=self.key) + if self.verify_client: + context.verify_mode = ssl.CERT_REQUIRED + context.set_default_verify_paths() + if self.cafile: + context.load_verify_locations(cafile=self.cafile) + retsock = context.wrap_socket( sock, - server_side=True, - certfile=self.cert, - keyfile=self.key) + server_side=True) except ssl.SSLError: _, x, _ = sys.exc_info() if x.args[0] == ssl.SSL_ERROR_EOF: