Move SNIChallenge into apache_configurator.py as the dvsni challenge should be a feature for the configurator

This commit is contained in:
James Kasten 2014-11-20 03:47:35 -08:00
parent 3b26f6c526
commit d57dd9faee
3 changed files with 175 additions and 289 deletions

View file

@ -1052,8 +1052,181 @@ LogLevel warn \n\
return True
###########################################################################
# Challenges Section
###########################################################################
def perform(self, chall_type, tup):
if chall_type == 'dvsni':
return dvsni_perform(tup)
return None
def dvsni_perform(self, tup):
"""
Sets up and reloads Apache server to handle SNI challenges
listSNITuple: List of tuples with form (addr, r, nonce)
addr (string), r (base64 string), nonce (hex string)
key: string - File path to key
configurator: Configurator obj
"""
# Save any changes to the configuration as a precaution
# About to make temporary changes to the config
self.save()
if len(tup) != 2:
logger.fatal("Incorrect parameter given to Apache DVSNI challenge")
sys.exit(1)
listSNITuple = tup[0]
dvsni_key = tup[1]
addresses = []
default_addr = "*:443"
for tup in listSNITuple:
vhost = self.choose_virtual_host(tup[0])
if vhost is None:
logger.error("No vhost exists with servername or alias of:%s" % tup[0])
logger.error("No _default_:443 vhost exists")
logger.error("Please specify servernames in the Apache config")
return None
# TODO - @jdkasten review this code to make sure it makes sense
if not self.make_server_sni_ready(vhost, default_addr):
return None
for a in vhost.addrs:
if "_default_" in a:
addresses.append([default_addr])
break
else:
addresses.append(vhost.addrs)
# Generate S
s = Random.get_random_bytes(S_SIZE)
# Create all of the challenge certs
for t in listSNITuple:
# Need to decode from base64
r = le_util.b64_url_dec(t[1])
ext = self.generateExtension(r, s)
self.createChallengeCert(t[0], ext, t[2], dvsni_key)
self.dvsni_mod_config(self.user_config_file, listSNITuple, addresses)
# Save reversible changes and restart the server
self.save("SNI Challenge", True)
self.restart(quiet)
s = le_util.b64_url_enc(s)
return {"type":"dvsni", "s":s}
def cleanup(self):
self.revert_challenge_config()
self.restart(True)
def dvsni_get_cert_file(self, nonce):
"""
Returns standardized name for challenge certificate
nonce: string - hex
result: returns certificate file name
"""
return WORK_DIR + nonce + ".crt"
def __getConfigText(self, nonce, ip_addrs, key):
"""
Chocolate virtual server configuration text
nonce: string - hex
ip_addr: string - address of challenged domain
key: string - file path to key
result: returns virtual host configuration text
"""
configText = "<VirtualHost " + " ".join(ip_addrs) + "> \n \
ServerName " + nonce + INVALID_EXT + " \n \
UseCanonicalName on \n \
SSLStrictSNIVHostCheck on \n \
\n \
LimitRequestBody 1048576 \n \
\n \
Include " + OPTIONS_SSL_CONF + " \n \
SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + " \n \
SSLCertificateKeyFile " + key + " \n \
\n \
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
</VirtualHost> \n\n "
return configText
def dvsni_mod_config(self, mainConfig, listSNITuple, listlistAddrs):
"""
Modifies Apache config files to include the challenge virtual servers
mainConfig: string - file path to Apache user config file
listSNITuple: list of tuples with form (addr, y, nonce, ext_oid)
addr (string), y (byte array), nonce (hex string), ext_oid (string)
key: string - file path to 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 idx, lis in enumerate(listlistAddrs):
configText += self.__getConfigText(listSNITuple[idx][2], lis, self.key)
configText += "</IfModule> \n"
self.dvsni_conf_include_check(mainConfig)
self.register_file_creation(True, APACHE_CHALLENGE_CONF)
newConf = open(APACHE_CHALLENGE_CONF, 'w')
newConf.write(configText)
newConf.close()
def dvsni_conf_include_check(self, mainConfig):
"""
Adds DVSNI 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
"""
if len(self.find_directive(self.case_i("Include"), APACHE_CHALLENGE_CONF)) == 0:
#print "Including challenge virtual host(s)"
self.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
def createChallengeCert(self, name, ext, nonce, key):
"""
Modifies challenge certificate configuration and calls openssl binary to create a certificate
ext: string - hex z value
nonce: string - hex
key: string - file path to key
result: certificate created at dvsni_get_cert_file(nonce)
"""
self.register_file_creation(True, self.dvsni_get_cert_file(nonce))
cert_pem = crypto_util.make_ss_cert(key, [nonce + INVALID_EXT, name, ext])
with open(self.dvsni_get_cert_file(nonce), 'w') as f:
f.write(cert_pem)
def dvsni_gen_ext(self, r, s):
"""
Generates z to be placed in certificate extension
r: byte array
s: byte array
result: returns z + INVALID_EXT
"""
h = hashlib.new('sha256')
h.update(r)
h.update(s)
return h.hexdigest() + INVALID_EXT
def main():
config = Configurator()

View file

@ -1,6 +1,8 @@
import abc
from abc_base import Configurator
import augeas
from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
class AugeasConfigurator(Configurator):

View file

@ -1,289 +0,0 @@
#!/usr/bin/env python
import M2Crypto
from Crypto import Random
import hashlib
from os import path
import sys
import binascii
from letsencrypt.client import apache_configurator
from letsencrypt.client.CONFIG import CONFIG_DIR, WORK_DIR, SERVER_ROOT
from letsencrypt.client.CONFIG import OPTIONS_SSL_CONF, APACHE_CHALLENGE_CONF, INVALID_EXT
from letsencrypt.client.CONFIG import S_SIZE
from letsencrypt.client import logger, crypto_util, le_util
from letsencrypt.client.challenge import Challenge
# import configurator
# from CONFIG import CONFIG_DIR, WORK_DIR, SERVER_ROOT
# from CONFIG import CHOC_CERT_CONF, OPTIONS_SSL_CONF, APACHE_CHALLENGE_CONF, INVALID_EXT
# from CONFIG import S_SIZE, NONCE_SIZE
# import logger, le_util
# from challenge import Challenge
class SNI_Challenge(Challenge):
def __init__(self, sni_todos, key_filepath, config):
'''
sni_todos: List of tuples with form (addr, r, nonce)
addr (string), r (base64 string), nonce (hex string)
key: string - File path to key
configurator: Configurator obj
'''
self.listSNITuple = sni_todos
self.key = key_filepath
self.configurator = config
self.s = None
def getDvsniCertFile(self, nonce):
"""
Returns standardized name for challenge certificate
nonce: string - hex
result: returns certificate file name
"""
return WORK_DIR + nonce + ".crt"
def findApacheConfigFile(self):
"""
Locates the file path to the user's main apache config
result: returns file path if present
"""
if path.isfile(SERVER_ROOT + "httpd.conf"):
return SERVER_ROOT + "httpd.conf"
logger.error("Unable to find httpd.conf, file does not exist in Apache ServerRoot")
return None
def __getConfigText(self, nonce, ip_addrs, key):
"""
Chocolate virtual server configuration text
nonce: string - hex
ip_addr: string - address of challenged domain
key: string - file path to key
result: returns virtual host configuration text
"""
configText = "<VirtualHost " + " ".join(ip_addrs) + "> \n \
ServerName " + nonce + INVALID_EXT + " \n \
UseCanonicalName on \n \
SSLStrictSNIVHostCheck on \n \
\n \
LimitRequestBody 1048576 \n \
\n \
Include " + OPTIONS_SSL_CONF + " \n \
SSLCertificateFile " + self.getDvsniCertFile(nonce) + " \n \
SSLCertificateKeyFile " + key + " \n \
\n \
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
</VirtualHost> \n\n "
return configText
def modifyApacheConfig(self, mainConfig, listlistAddrs):
"""
Modifies Apache config files to include the challenge virtual servers
mainConfig: string - file path to Apache user config file
listSNITuple: list of tuples with form (addr, y, nonce, ext_oid)
addr (string), y (byte array), nonce (hex string), ext_oid (string)
key: string - file path to 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 idx, lis in enumerate(listlistAddrs):
configText += self.__getConfigText(self.listSNITuple[idx][2], lis, self.key)
configText += "</IfModule> \n"
self.checkForApacheConfInclude(mainConfig)
self.configurator.register_file_creation(True, APACHE_CHALLENGE_CONF)
newConf = open(APACHE_CHALLENGE_CONF, 'w')
newConf.write(configText)
newConf.close()
def checkForApacheConfInclude(self, mainConfig):
"""
Adds DVSNI 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
"""
if len(self.configurator.find_directive(self.configurator.case_i("Include"), APACHE_CHALLENGE_CONF)) == 0:
#print "Including challenge virtual host(s)"
self.configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
def createChallengeCert(self, name, ext, nonce, key):
"""
Modifies challenge certificate configuration and calls openssl binary to create a certificate
ext: string - hex z value
nonce: string - hex
key: string - file path to key
result: certificate created at getDvsniCertFile(nonce)
"""
self.configurator.register_file_creation(True, self.getDvsniCertFile(nonce))
cert_pem = crypto_util.make_ss_cert(key, [nonce + INVALID_EXT, name, ext])
with open(self.getDvsniCertFile(nonce), 'w') as f:
f.write(cert_pem)
def generateExtension(self, r, s):
"""
Generates z to be placed in certificate extension
r: byte array
s: byte array
result: returns z + INVALID_EXT
"""
h = hashlib.new('sha256')
h.update(r)
h.update(s)
return h.hexdigest() + INVALID_EXT
def byteToHex(self, byteStr):
"""
Converts binary array to hex string
byteStr: byte array
result: returns hex representation of byteStr
"""
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
def cleanup(self):
"""
Remove all temporary changes necessary to perform the challenge
configurator: Configurator object
listSNITuple: The initial challenge tuple
result: Apache server is restored to the pre-challenge state
"""
self.configurator.revert_challenge_config()
self.configurator.restart(True)
def generate_response(self):
"""
Generates a response for a completed challenge
"""
if self.s:
return {"type":"dvsni", "s":self.s}
logger.error("DVSNI Challenge was not completed before calling generate_response")
return None
#main call
def perform(self, quiet=False):
"""
Sets up and reloads Apache server to handle SNI challenges
listSNITuple: List of tuples with form (addr, r, nonce)
addr (string), r (base64 string), nonce (hex string)
key: string - File path to key
configurator: Configurator obj
"""
# Save any changes to the configuration as a precaution
# About to make temporary changes to the config
self.configurator.save()
addresses = []
default_addr = "*:443"
for tup in self.listSNITuple:
vhost = self.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 None
if not self.configurator.make_server_sni_ready(vhost, default_addr):
return None
for a in vhost.addrs:
if "_default_" in a:
addresses.append([default_addr])
break
else:
addresses.append(vhost.addrs)
# Generate S
s = Random.get_random_bytes(S_SIZE)
# Create all of the challenge certs
for tup in self.listSNITuple:
# Need to decode from base64
r = le_util.b64_url_dec(tup[1])
ext = self.generateExtension(r, s)
self.createChallengeCert(tup[0], ext, tup[2], self.key)
self.modifyApacheConfig(self.configurator.user_config_file, addresses)
# Save reversible changes and restart the server
self.configurator.save("SNI Challenge", True)
self.configurator.restart(quiet)
self.s = le_util.b64_url_enc(s)
return self.s
# This main function is just used for testing
def main():
key = path.abspath("/home/ubuntu/key.pem")
csr = path.abspath("/home/ubuntu/req.pem")
logger.setLogger(logger.FileLogger(sys.stdout))
logger.setLogLevel(logger.INFO)
testkey = M2Crypto.RSA.load_key(key)
#r = Random.get_random_bytes(S_SIZE)
r = "testValueForR"
#nonce = Random.get_random_bytes(NONCE_SIZE)
nonce = "nonce"
r2 = "testValueForR2"
nonce2 = "nonce2"
r = le_util.b64_url_enc(r)
r2 = le_util.b64_url_enc(r2)
#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)
#y2 = testkey.public_encrypt(r2, M2Crypto.RSA.pkcs1_oaep_padding)
nonce = binascii.hexlify(nonce)
nonce2 = binascii.hexlify(nonce2)
config = configurator.Configurator()
challenges = [("client.theobroma.info", r, nonce), ("foo.theobroma.info",r2, nonce2)]
#challenges = [("127.0.0.1", y, nonce, "1.3.3.7"), ("localhost", y2, nonce2, "1.3.3.7")]
sni_chall = SNI_Challenge(challenges, key, config)
if sni_chall.perform():
# Waste some time without importing time module... just for testing
for i in range(0, 12000):
if i % 2000 == 0:
print "Waiting:", i
#print "Cleaning up"
#sni_chall.cleanup()
else:
print "Failed SNI challenge..."
if __name__ == "__main__":
main()