Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protect with tls frontend #938

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/gcf-am.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,34 @@ def main(argv=None):
resource_manager = \
getInstanceFromClassname(opts.authorizer_resource_manager)

# by default, the agregate manager will expect to be reached through https protocol
# and handle the tls client authentication itself but the python classes used to do
# this are not suitable for production use (as per python's doc).
# Therefore allow for the server to sit behind an apache (or other) frontend doing the
# tls client authentification and only see requests that are forwarded to it, with the
# authenticated client certificate sent to it as an http header
proto = 'https'
if hasattr(opts, 'proto') and opts.proto is not None and str(opts.proto).strip() != "":
if str(opts.proto).strip().lower() == "http":
proto = 'http'
elif str(opts.proto).strip().lower() == "https":
proto = 'https'
else:
proto = 'https'
logging.getLogger('gcf-am').warning("Invalid argument for 'proto', set default : https")

certheader='X-Geni-Client-Cert'
if proto is 'http':
if hasattr(opts, 'certheader') and opts.certheader is not None and str(opts.certheader).strip() != "":
certheader=str(opts.certheader).strip()

delegate=None
if hasattr(opts, 'delegate') and opts.delegate is not None and str(opts.delegate).strip() != "":
try:
delegate = getInstanceFromClassname(opts.delegate,
getAbsPath(opts.rootcadir),
config['global']['base_name'],
"https://%s:%d/" % (opts.host, int(opts.port)),
"%s://%s:%d/" % (proto, opts.host, int(opts.port)),
**vars(opts)
)
except AttributeError, e:
Expand Down Expand Up @@ -210,7 +231,8 @@ def main(argv=None):
base_name=config['global']['base_name'],
authorizer=authorizer,
resource_manager=resource_manager,
delegate=delegate, multithread=multithread)
delegate=delegate, multithread=multithread,
proto=proto, certheader=certheader)
else:
msg = "Unknown API version: %d. Valid choices are \"1\", \"2\", or \"3\""
sys.exit(msg % (opts.api_version))
Expand Down
102 changes: 102 additions & 0 deletions src/gcf/geni/XMLRPCServer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#----------------------------------------------------------------------
# Copyright (c) 2010-2016 Raytheon BBN Technologies
# Copyright (c) 2019 Inria by David Margery for the Fed4FIRE+ project
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and/or hardware specification (the "Work") to
# deal in the Work without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Work, and to permit persons to whom the Work
# is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Work.
#
# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
# IN THE WORK.
#----------------------------------------------------------------------

"""A simple XML RPC server supporting getting client cert from HTTP header.

Based on this article:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81549

"""

from __future__ import absolute_import

import ssl
import base64
import textwrap
import os

from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
from ..sfa.trust.certificate import Certificate
from OpenSSL import crypto

class XMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
"""A request handler that grabs the peer's certificate from
the http headers and makes it available while the request is handled

This implementation can only be used in a single-threaded, one
request at a time model. The peer certificate is stashed on the
XML RPC server at the start of a call and removed at the end of a
call. This is the only way I could find to access this
information.
"""

def parse_request(self):
parse_success=True
SimpleXMLRPCRequestHandler.parse_request(self)
client_cert_string=self.headers.get(self.server.certheader_name, "")
# going through headers in python loose end of line caraters
# massage the string back to proper PEM format
client_cert_string=client_cert_string.replace(' ',"\n")
client_cert_string=client_cert_string.replace("BEGIN\n","BEGIN ")
client_cert_string=client_cert_string.replace("END\n","END ")

if client_cert_string is "":
self.server.pem_cert = None
parse_success=False
self.send_error(400, "Bad request - client cert required")
else:
self.server.pem_cert = client_cert_string

if self.server.logRequests:
client_cert=Certificate(string=client_cert_string)
self.log_message("Got call from client cert: %s", client_cert.get_printable_subject())
return parse_success



class XMLRPCServer(SimpleXMLRPCServer):
"""An extension to SimpleXMLRPCServer that expects TLS transaction.
has been proxied through a production quality web server, and that the
client's peer cert is passed to it as an http header
"""

def __init__(self, addr, requestHandler=XMLRPCRequestHandler,
logRequests=False, allow_none=False, encoding=None,
bind_and_activate=True, certheader='X-Geni-Client-Cert'):
SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
allow_none, encoding, False)
self.certheader_name=certheader
if bind_and_activate:
# This next throws a socket.error on error, eg
# Address already in use or Permission denied.
# Catch for clearer error message?
self.server_bind()
self.server_activate()

# Return the PEM cert for current XMLRPC client connection
# This works for the single threaded case. Need to override
# This method for the threaded case
def get_pem_cert(self):
return self.pem_cert
27 changes: 18 additions & 9 deletions src/gcf/geni/am/am3.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from ..util.tz_util import tzd
from ..util.urn_util import publicid_to_urn
from ..util import urn_util as urn
from ..XMLRPCServer import XMLRPCServer
from ..SecureXMLRPCServer import SecureXMLRPCServer
from ..SecureThreadedXMLRPCServer import SecureThreadedXMLRPCServer

Expand Down Expand Up @@ -1387,29 +1388,37 @@ def __init__(self, addr, keyfile=None, certfile=None,
trust_roots_dir=None,
ca_certs=None, base_name=None,
authorizer=None, resource_manager=None,
delegate=None, multithread=False):
# ca_certs arg here must be a file of concatenated certs
if ca_certs is None:
raise Exception('Missing CA Certs')
elif not os.path.isfile(os.path.expanduser(ca_certs)):
raise Exception('CA Certs must be an existing file of accepted root certs: %s' % ca_certs)
delegate=None, multithread=False,
proto='https', certheader=None):
use_https=proto=='https'
if use_https:
# ca_certs arg here must be a file of concatenated certs
if ca_certs is None:
raise Exception('Missing CA Certs')
elif not os.path.isfile(os.path.expanduser(ca_certs)):
raise Exception('CA Certs must be an existing file of accepted root certs: %s' % ca_certs)

# Decode the addr into a URL. Is there a pythonic way to do this?
server_url = "https://%s:%d/" % addr
server_url = "%s://%s:%d/" % (proto, addr[0], addr[1])
if delegate is None:
delegate = ReferenceAggregateManager(trust_roots_dir, base_name,
server_url)

# FIXED: set logRequests=true if --debug
logRequest=logging.getLogger().getEffectiveLevel()==logging.DEBUG
if multithread:
if multithread and use_https:
self._server = SecureThreadedXMLRPCServer(addr, keyfile=keyfile,
certfile=certfile, ca_certs=ca_certs,
logRequests=logRequest)
else:
elif not multithread and use_https:
self._server = SecureXMLRPCServer(addr, keyfile=keyfile,
certfile=certfile, ca_certs=ca_certs,
logRequests=logRequest)
else:
self._server = XMLRPCServer(addr,
logRequests=logRequest,
certheader=certheader)

aggregate_manager = AggregateManager(trust_roots_dir, delegate,
authorizer, resource_manager)
self._server.register_instance(aggregate_manager)
Expand Down