1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-26 07:41:33 +03:00

Added code to auto-configure the Apache server for SNI challenges

This commit is contained in:
James Kasten
2012-08-01 19:31:21 -04:00
parent a0ca971c2e
commit fc56a875d0
4 changed files with 97 additions and 38 deletions

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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 \
</VirtualHost> \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 = "<IfModule mod_ssl.c> \n"
for tup in listSNITuple:
configText += getConfigText(tup[2], tup[0], key)
configText += "</IfModule> \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()

View File

@@ -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()