From fc56a875d0e36af4c5c3bcd31eb4dba0dd89fe17 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 1 Aug 2012 19:31:21 -0400 Subject: [PATCH] Added code to auto-configure the Apache server for SNI challenges --- client-webserver/client.py | 2 +- client-webserver/configurator.py | 53 ++++++++++++++++------- client-webserver/sni_challenge.py | 72 ++++++++++++++++++++++--------- server-ca/sni_challenge/verify.py | 8 +++- 4 files changed, 97 insertions(+), 38 deletions(-) diff --git a/client-webserver/client.py b/client-webserver/client.py index f9793ed00..56c2c1783 100755 --- a/client-webserver/client.py +++ b/client-webserver/client.py @@ -131,7 +131,7 @@ for name in dn: if host is not None: vhost.add(host) -sni_challenge.perform_sni_cert_challenge(sni_todo, req_file, key_file) +sni_challenge.perform_sni_cert_challenge(sni_todo, os.path.abspath(req_file), os.path.abspath(key_file)) print "waiting", 3 time.sleep(3) diff --git a/client-webserver/configurator.py b/client-webserver/configurator.py index 658854a41..f364ea5e8 100644 --- a/client-webserver/configurator.py +++ b/client-webserver/configurator.py @@ -25,7 +25,7 @@ class Configurator(object): # relevant files # Set Augeas flags to save backup self.aug = augeas.Augeas(None, None, 1 << 0) - self.vhosts = [] + self.vhosts = self.get_virtual_hosts() # httpd_files - All parsable Httpd files # add_transform overwrites all currently loaded files so we must # maintain state @@ -43,6 +43,7 @@ class Configurator(object): the "included" confs. The function verifies that it has located the three directives and finally modifies them to point to the correct destination + TODO: Should add/remove chain directives """ search = {} path = {} @@ -92,6 +93,7 @@ class Configurator(object): def choose_virtual_host(self, name): """ TODO: Finish this function correctly + TODO: This should return vhost of :443 if both 80 and 443 exist This is currently just a very basic demo version """ for v in self.vhosts: @@ -99,6 +101,11 @@ class Configurator(object): # TODO: Or a converted FQDN address if n == name: return v + for v in self.vhosts: + for a in v.addrs: + tup = a.partition(":") + if tup[0] == name: + return v for v in self.vhosts: for a in v.addrs: if a == "_default_:443": @@ -125,17 +132,18 @@ class Configurator(object): def get_virtual_hosts(self): #Search sites-available, httpd.conf for possible virtual hosts paths = self.aug.match("/files" + BASE_DIR + "sites-available//VirtualHost") + vhs = [] for p in paths: addrs = [] args = self.aug.match(p + "/arg") for arg in args: addrs.append(self.aug.get(arg)) - self.vhosts.append(VH(p, addrs)) + vhs.append(VH(p, addrs)) - for host in self.vhosts: + for host in vhs: self.add_servernames(host) - return self.vhosts + return vhs def is_name_vhost(self, addr): # search for NameVirtualHost directive for ip_addr @@ -197,7 +205,7 @@ class Configurator(object): return False return True - def make_server_sni_ready(self, addr): + def make_server_sni_ready(self, vhost): """ Checks to see if the server is ready for SNI challenges """ @@ -207,15 +215,27 @@ class Configurator(object): return False # Check for Listen 443 + # TODO: This could be made to also look for ip:443 combo + # TODO: Need to search only open directives and IfMod mod_ssl.c if len(self.find_directive("Listen", "443")) == 0: print self.find_directive("Listen", "443") print "Setting the Apache Server to Listen on port 443" self.add_dir_to_ifmodssl("/files" + BASE_DIR + "ports.conf", "Listen", "443") # Check for NameVirtualHost - if not self.is_name_vhost(addr): - print "Setting VirtualHost at", addr, "to be a name based virtual host" - self.add_name_vhost(addr) + # First see if any of the vhost addresses is a _default_ addr + for addr in vhost.addrs: + tup = addr.partition(":") + if tup[0] == "_default_": + if not self.is_name_vhost("*:443"): + print "Setting all VirtualHosts on *:443 to be name based virtual hosts" + self.add_name_vhost("*:443") + return True + # No default addresses... so set each one individually + for addr in vhost.addrs: + if not self.is_name_vhost(addr): + print "Setting VirtualHost at", addr, "to be a name based virtual host" + self.add_name_vhost(addr) return True @@ -227,7 +247,10 @@ class Configurator(object): ifMods = self.aug.match(aug_conf_path + "/IfModule/*[self::arg='" + mod + "']") # Strip off "arg" at end of first ifmod path return ifMods[0][:len(ifMods[0]) - 3] - + + def add_dir(self, aug_conf_path, directive, arg): + self.aug.set(aug_conf_path + "/directive[last() + 1]", directive) + self.aug.set(aug_conf_path + "/directive[last()]", arg) def find_directive(self, directive, arg, start="/files"+BASE_DIR+"apache2.conf"): """ @@ -306,6 +329,7 @@ class Configurator(object): p = subprocess.check_output(["sudo", "apache2ctl", "-M"], stderr=open("/dev/null")) except: print "Error accessing apache2ctl for loaded modules!" + print "This may be caused by an Apache Configuration Error" return False if "ssl_module" in p: return True @@ -376,20 +400,19 @@ def recurmatch(aug, path): def main(): config = Configurator() - config.get_virtual_hosts() for v in config.vhosts: - for a in v.addrs: - for name in v.names: - print a, name + print v.addrs + for name in v.names: + print name for m in config.find_directive("Listen", "443"): print "Directive Path:", m, "Value:", config.aug.get(m) for v in config.vhosts: for a in v.addrs: - print a, config.is_name_vhost(a) + print "Address:",a, "- Is name vhost?", config.is_name_vhost(a) - print config.make_server_sni_ready("example.com:443") + #print config.make_server_sni_ready("example.com:443") setHost = set() setHost.add(config.choose_virtual_host("example.com")) setHost.add(config.choose_virtual_host("example2.com")) diff --git a/client-webserver/sni_challenge.py b/client-webserver/sni_challenge.py index 48227aab6..c15d193f9 100644 --- a/client-webserver/sni_challenge.py +++ b/client-webserver/sni_challenge.py @@ -8,8 +8,12 @@ import hashlib from shutil import move from os import remove, close import binascii +import augeas +import configurator +#import dns.resolver -CHOC_DIR = "/home/ubuntu/chocolate/client-webserver/" +#CHOC_DIR = "/home/ubuntu/chocolate/client-webserver/" +CHOC_DIR = "/home/james/Documents/apache_choc/" CHOC_CERT_CONF = "choc_cert_extensions.cnf" OPTIONS_SSL_CONF = CHOC_DIR + "options-ssl.conf" APACHE_CHALLENGE_CONF = CHOC_DIR + "choc_sni_cert_challenge.conf" @@ -30,6 +34,8 @@ def getChocCertFile(nonce): def findApacheConfigFile(): """ Locates the file path to the user's main apache config + + TODO: This needs to be rewritten... should use true ServerRoot result: returns file path if present """ @@ -65,14 +71,14 @@ LimitRequestBody 1048576 \n \ \n \ Include " + OPTIONS_SSL_CONF + " \n \ SSLCertificateFile " + getChocCertFile(nonce) + " \n \ -SSLCertificateKeyFile " + CHOC_DIR + key + " \n \ +SSLCertificateKeyFile " + key + " \n \ \n \ DocumentRoot " + CHOC_DIR + "challenge_page/ \n \ \n\n " return configText -def modifyApacheConfig(mainConfig, listSNITuple, key): +def modifyApacheConfig(mainConfig, listSNITuple, key, configurator): """ Modifies Apache config files to include the challenge virtual servers @@ -84,36 +90,40 @@ def modifyApacheConfig(mainConfig, listSNITuple, key): result: Apache config includes virtual servers for issued challenges """ + # TODO: Use ip address of existing vhost instead of relying on FQDN configText = " \n" for tup in listSNITuple: configText += getConfigText(tup[2], tup[0], key) configText += " \n" - checkForApacheConfInclude(mainConfig) + checkForApacheConfInclude(mainConfig, configurator) newConf = open(APACHE_CHALLENGE_CONF, 'w') newConf.write(configText) newConf.close() # Need to add NameVirtualHost IP_ADDR or does the chocolate install do this? -def checkForApacheConfInclude(mainConfig): +def checkForApacheConfInclude(mainConfig, configurator): """ - Adds chocolate challenge include file if it does not already exist within mainConfig + Adds chocolate challenge include file if it does not already exist + within mainConfig mainConfig: string - file path to main user apache config file result: User Apache configuration includes chocolate sni challenge file """ - - searchStr = "Include " + APACHE_CHALLENGE_CONF + if len(configurator.find_directive("Include", APACHE_CHALLENGE_CONF)) == 0: + configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF) + #searchStr = "Include " + APACHE_CHALLENGE_CONF + #conf = open(mainConfig, 'r+') - conf = open(mainConfig, 'r') - if not any(line.startswith(searchStr) for line in conf): + #conf = open(mainConfig, 'r') + #if not any(line.startswith(searchStr) for line in conf): #conf.write(searchStr) - process = subprocess.Popen(["echo", "\n" + searchStr], stdout=subprocess.PIPE) - subprocess.check_output(["sudo", "tee", "-a", mainConfig], stdin=process.stdout) - process.stdout.close() + #process = subprocess.Popen(["echo", "\n" + searchStr], stdout=subprocess.PIPE) + #subprocess.check_output(["sudo", "tee", "-a", mainConfig], stdin=process.stdout) + #process.stdout.close() - conf.close() + #conf.close() def createChallengeCert(oid, ext, nonce, csr, key): @@ -142,6 +152,7 @@ def generateExtension(key, y): result: returns z value """ + rsaPrivKey = M2Crypto.RSA.load_key(key) r = rsaPrivKey.private_decrypt(y, M2Crypto.RSA.pkcs1_oaep_padding) #print r @@ -159,6 +170,7 @@ def byteToHex(byteStr): result: returns hex representation of byteStr """ + return ''.join(["%02X" % ord(x) for x in byteStr]).strip() #Searches for the first extension specified in binary @@ -171,6 +183,7 @@ def updateCertConf(oid, value): result: updated certificate config file """ + confOld = open(CHOC_CERT_CONF) confNew = open(CHOC_CERT_CONF + ".tmp", 'w') flag = False @@ -195,21 +208,34 @@ def apache_restart(): subprocess.call(["sudo", "/etc/init.d/apache2", "reload"]) #main call -def perform_sni_cert_challenge(listSNITuple, csr, key): +def perform_sni_cert_challenge(listSNITuple, csr, key, configurator): """ Sets up and reloads Apache server to handle SNI challenges listSNITuple: List of tuples with form (addr, y, nonce, ext_oid) - addr (string), y (byte array), nonce (hex string), ext_oid (string) + addr (string), y (byte array), nonce (hex string), + ext_oid (string) csr: string - File path to chocolate csr key: string - File path to key + configurator: Configurator obj """ - + + for tup in listSNITuple: + vhost = configurator.choose_virtual_host(tup[0]) + if vhost is None: + print "No vhost exists with servername or alias of:", tup[0] + print "No _default_:443 vhost exists" + print "Please specify servernames in the Apache config" + return False + + if not configurator.make_server_sni_ready(vhost): + return False + for tup in listSNITuple: ext = generateExtension(key, tup[1]) createChallengeCert(tup[3], ext, tup[2], csr, key) - modifyApacheConfig(findApacheConfigFile(), listSNITuple, key) + modifyApacheConfig(findApacheConfigFile(), listSNITuple, key, configurator) apache_restart() def main(): @@ -224,7 +250,10 @@ def main(): nonce = "nonce" r2 = "testValueForR2" nonce2 = "nonce2" - + + #ans = dns.resolver.query("google.com") + #print ans.rrset + #return #the second parameter is ignored #https://www.dlitz.net/software/pycrypto/api/current/ y = testkey.public_encrypt(r, M2Crypto.RSA.pkcs1_oaep_padding) @@ -232,8 +261,11 @@ def main(): nonce = binascii.hexlify(nonce) nonce2 = binascii.hexlify(nonce2) + + config = configurator.Configurator() - perform_sni_cert_challenge([("example.com", y, nonce, "1.3.3.7"), ("www.example.com",y2, nonce2, "1.3.3.7")], csr, key) + #perform_sni_cert_challenge([("example.com", y, nonce, "1.3.3.7"), ("www.example.com",y2, nonce2, "1.3.3.7")], csr, key, config) + perform_sni_cert_challenge([("127.0.0.1", y, nonce, "1.3.3.7"), ("localhost", y2, nonce2, "1.3.3.7")], csr, key, config) if __name__ == "__main__": main() diff --git a/server-ca/sni_challenge/verify.py b/server-ca/sni_challenge/verify.py index babc8a426..d7b2f4aec 100644 --- a/server-ca/sni_challenge/verify.py +++ b/server-ca/sni_challenge/verify.py @@ -59,10 +59,12 @@ def verify_challenge(address, r, nonce, socksify=False): M2Crypto.SSL.Connection.postConnectionCheck = None conn = M2Crypto.SSL.Connection(context) + if socksify: socksocket = socks.socksocket() socksocket.setproxy(socks.PROXY_TYPE_SOCKS4, "localhost", 9050) conn.socket = socksocket + sni_support.set_sni_ext(conn.ssl, sni_name) try: conn.connect((address, 443)) @@ -104,9 +106,11 @@ def main(): nonce = binascii.hexlify(nonce) nonce2 = binascii.hexlify(nonce2) - valid, response = verify_challenge("example.com", r, "33947bb5dd81f17f67305cb90aa5b8b5e95442e8ed4e78567092a63d04eb3db4") + valid, response = verify_challenge("example.com", r, nonce) + #valid, response = verify_challenge("127.0.0.1", r, nonce) print response - valid, response = verify_challenge("www.example.com", r2, "no123809214unce2") + valid, response = verify_challenge("www.example.com", r2, nonce2) + #valid, response = verify_challenge("localhost", r2, nonce2) print response if __name__ == "__main__": main()