From d57dd9faeec6fe9c6bc905a2402f7f3f4d422a62 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Thu, 20 Nov 2014 03:47:35 -0800 Subject: [PATCH] Move SNIChallenge into apache_configurator.py as the dvsni challenge should be a feature for the configurator --- letsencrypt/client/apache_configurator.py | 173 +++++++++++++ letsencrypt/client/augeas_configurator.py | 2 + letsencrypt/client/sni_challenge.py | 289 ---------------------- 3 files changed, 175 insertions(+), 289 deletions(-) delete mode 100755 letsencrypt/client/sni_challenge.py diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index dcf5ac9ef..4d6879dbb 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -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 = " \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 \ + \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 = " \n" + for idx, lis in enumerate(listlistAddrs): + configText += self.__getConfigText(listSNITuple[idx][2], lis, self.key) + configText += " \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() diff --git a/letsencrypt/client/augeas_configurator.py b/letsencrypt/client/augeas_configurator.py index b9c06b49a..af12dfffe 100644 --- a/letsencrypt/client/augeas_configurator.py +++ b/letsencrypt/client/augeas_configurator.py @@ -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): diff --git a/letsencrypt/client/sni_challenge.py b/letsencrypt/client/sni_challenge.py deleted file mode 100755 index 8170f714a..000000000 --- a/letsencrypt/client/sni_challenge.py +++ /dev/null @@ -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 = " \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 \ - \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 = " \n" - for idx, lis in enumerate(listlistAddrs): - configText += self.__getConfigText(self.listSNITuple[idx][2], lis, self.key) - configText += " \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()