Added base challenge class, converted sni_challenge to class, client uses challenge factory and generic perform, cleanup functions

This commit is contained in:
James Kasten
2012-09-04 23:14:36 -04:00
parent d4cd1c22e6
commit 3a2fca1961
4 changed files with 248 additions and 203 deletions
+10
View File
@@ -0,0 +1,10 @@
from trustify.client import logger
class Challenge(object):
def __init__(self, configurator):
self.config = configurator
def perform(self):
logger.error("Error - base class challenge.perform()")
def clean(self):
logger.error("Error - base class challenge.clean()")
+30 -20
View File
@@ -8,12 +8,12 @@ import os, grp, pwd, sys, time, random, sys
import hashlib
import subprocess
import getopt
import logger
# TODO: support a mode where use of interactive prompting is forbidden
from trustify.protocol.chocolate_pb2 import chocolatemessage
from trustify.client import sni_challenge
from trustify.client import configurator
from trustify.client import logger
from trustify.client.CONFIG import difficulty, cert_file, chain_file
from trustify.client.CONFIG import KEY_DIR, CERT_DIR
@@ -261,6 +261,26 @@ def gen_https_names(domains):
result = result + "https://" + domains[len(domains)-1]
return result
def challenge_factory(r, req_filepath, key_filepath, config):
sni_todo = []
dn = []
challenges = []
logger.info("Received %s challenges from server." % len(r.challenge))
for chall in r.challenge:
logger.debug(chall)
if chall.type == r.DomainValidateSNI:
logger.info("\tDomainValidateSNI challenge for name %s." % chall.name)
dvsni_nonce, dvsni_y, dvsni_ext = chall.data
sni_todo.append( (chall.name, dvsni_y, dvsni_nonce, dvsni_ext) )
dn.append(chall.name)
if sni_todo:
challenges.append(sni_todo, req_filepath, key_filepath, config)
logger.debug(sni_todo)
return challenges, dn
def send_request(key_pem, csr_pem, quiet=curses):
global server
upstream = "https://%s/chocolate.py" % server
@@ -291,9 +311,9 @@ def send_request(key_pem, csr_pem, quiet=curses):
return r, k
def handle_verification_response(r, dn, sni_todo, vhost, key_file, config):
def handle_verification_response(r, dn, challenge, vhost, key_file, config):
if r.success.IsInitialized():
sni_challenge.cleanup(sni_todo, config)
challenge.cleanup()
cert_chain_abspath = None
with open(cert_file, "w") as f:
f.write(r.success.certificate)
@@ -401,19 +421,8 @@ def authenticate():
r, k = send_request(key_pem, csr_pem)
sni_todo = []
dn = []
logger.info("Received %s challenges from server." % len(r.challenge))
for chall in r.challenge:
logger.debug(chall)
if chall.type == r.DomainValidateSNI:
logger.info("\tDomainValidateSNI challenge for name %s." % chall.name)
dvsni_nonce, dvsni_y, dvsni_ext = chall.data
sni_todo.append( (chall.name, dvsni_y, dvsni_nonce, dvsni_ext) )
dn.append(chall.name)
logger.debug(sni_todo)
challenges, dn = challenge_factory(r, os.path.abspath(req_file), os.path.abspath(key_file), config)
# Find virtual hosts to deploy certificates too
vhost = set()
@@ -422,9 +431,10 @@ def authenticate():
if host is not None:
vhost.add(host)
if not sni_challenge.perform_sni_cert_challenge(sni_todo, os.path.abspath(req_file), os.path.abspath(key_file), config, quiet=curses):
logger.fatal("sni_challenge failed")
sys.exit(1)
for challenge in challenges:
if not challenge.perform(quiet=curses):
logger.fatal("challenge failed")
sys.exit(1)
logger.info("Configured Apache for challenge; waiting for verification...")
logger.debug("waiting 3")
@@ -438,8 +448,8 @@ def authenticate():
k.session = r.session
r = decode(do(upstream, k))
logger.debug(r)
handle_verification_response(r, dn, sni_todo, vhost, key_file, config)
# TODO: This needs to be rewritten to handle multiple challenges
handle_verification_response(r, dn, challenges[0], vhost, key_file, config)
# vim: set expandtab tabstop=4 shiftwidth=4
+10 -2
View File
@@ -6,11 +6,11 @@ import sys
import socket
import time
import shutil
import logger
from trustify.client.CONFIG import SERVER_ROOT, BACKUP_DIR, MODIFIED_FILES
#from CONFIG import SERVER_ROOT, BACKUP_DIR, MODIFIED_FILES, REWRITE_HTTPS_ARGS
from trustify.client.CONFIG import REWRITE_HTTPS_ARGS
from trustify.client import logger
#TODO - Need an initialization routine... make sure directories exist..ect
@@ -545,8 +545,16 @@ LogLevel warn \n\
return True, new_vhost
def __conflicting_host(self, ssl_vhost):
'''
Checks for a conflicting host, such that a new port 80 host could not
be created without ruining the apache config
Used with redirection
returns: conflict, hostOrAddrs - boolean
if conflict: returns conflicting vhost
if not conflict: returns space separated list of new host addrs
'''
# Consider changing this to a dictionary check
# Make sure adding the vhost will be safe
redirect_addrs = ""
for ssl_a in ssl_vhost.addrs:
# Add space on each new addr, combine "VirtualHost"+redirect_addrs
+198 -181
View File
@@ -16,42 +16,58 @@ from trustify.client import configurator
from trustify.client.CONFIG import CONFIG_DIR, WORK_DIR, SERVER_ROOT
from trustify.client.CONFIG import CHOC_CERT_CONF, OPTIONS_SSL_CONF, APACHE_CHALLENGE_CONF
from trustify.client.CONFIG import S_SIZE, NONCE_SIZE
from trustify.client import logger
from trustify.client.challenge import Challenge
def getChocCertFile(nonce):
"""
Returns standardized name for challenge certificate
class SNI_Challenge(Challenge):
def __init__(self, sni_todos, req_filepath, key_filepath, config):
'''
sni_todos: List of tuples with form (addr, y, nonce, ext_oid)
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
'''
self.listSNITuple = sni_todos
self.csr = req_filepath
self.key = key_filepath
self.configurator = config
nonce: string - hex
result: returns certificate file name
"""
def getChocCertFile(self, nonce):
"""
Returns standardized name for challenge certificate
return WORK_DIR + nonce + ".crt"
nonce: string - hex
def findApacheConfigFile():
"""
Locates the file path to the user's main apache config
result: returns certificate file name
"""
TODO: This needs to use true server_root
result: returns file path if present
"""
if path.isfile(SERVER_ROOT + "httpd.conf"):
return SERVER_ROOT + "httpd.conf"
print "Unable to find httpd.conf, file does not exist in Apache ServerRoot"
return None
return WORK_DIR + nonce + ".crt"
def getConfigText(nonce, ip_addrs, key):
"""
Chocolate virtual server configuration text
def findApacheConfigFile(self):
"""
Locates the file path to the user's main apache config
nonce: string - hex
ip_addr: string - address of challenged domain
key: string - file path to key
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
result: returns virtual host configuration text
"""
configText = "<VirtualHost " + " ".join(ip_addrs) + "> \n \
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 + ".chocolate \n \
UseCanonicalName on \n \
SSLStrictSNIVHostCheck on \n \
@@ -65,190 +81,191 @@ SSLCertificateKeyFile " + key + " \n \
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
</VirtualHost> \n\n "
return configText
return configText
def modifyApacheConfig(mainConfig, listSNITuple, listlistAddrs, key, configurator):
"""
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
def modifyApacheConfig(self, mainConfig, listlistAddrs):
"""
Modifies Apache config files to include the challenge virtual servers
result: Apache config includes virtual servers for issued challenges
"""
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
# 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 += getConfigText(listSNITuple[idx][2], lis, key)
configText += "</IfModule> \n"
result: Apache config includes virtual servers for issued challenges
"""
checkForApacheConfInclude(mainConfig, configurator)
newConf = open(APACHE_CHALLENGE_CONF, 'w')
newConf.write(configText)
newConf.close()
# 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"
# Need to add NameVirtualHost IP_ADDR or does the chocolate install do this?
def checkForApacheConfInclude(mainConfig, configurator):
"""
Adds chocolate challenge include file if it does not already exist
within mainConfig
mainConfig: string - file path to main user apache config file
self.checkForApacheConfInclude(mainConfig)
newConf = open(APACHE_CHALLENGE_CONF, 'w')
newConf.write(configText)
newConf.close()
result: User Apache configuration includes chocolate sni challenge file
"""
if len(configurator.find_directive("Include", APACHE_CHALLENGE_CONF)) == 0:
#print "Including challenge virtual host(s)"
configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
def checkForApacheConfInclude(self, mainConfig):
"""
Adds chocolate challenge include file if it does not already exist
within mainConfig
def createChallengeCert(oid, ext, nonce, csr, key):
"""
Modifies challenge certificate configuration and calls openssl binary to create a certificate
mainConfig: string - file path to main user apache config file
oid: string
ext: string - hex z value
nonce: string - hex
csr: string - file path to csr
key: string - file path to key
result: User Apache configuration includes chocolate sni challenge file
"""
if len(self.configurator.find_directive("Include", APACHE_CHALLENGE_CONF)) == 0:
#print "Including challenge virtual host(s)"
self.configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
result: certificate created at getChocCertFile(nonce)
"""
def createChallengeCert(self, oid, ext, nonce, csr, key):
"""
Modifies challenge certificate configuration and calls openssl binary to create a certificate
updateCertConf(oid, ext)
subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", getChocCertFile(nonce), "-in", csr], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
oid: string
ext: string - hex z value
nonce: string - hex
csr: string - file path to csr
key: string - file path to key
def generateExtension(key, y):
"""
Generates z to be placed in certificate extension
result: certificate created at getChocCertFile(nonce)
"""
key: string - File path to key
y: byte array
updateCertConf(oid, ext)
subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", getChocCertFile(nonce), "-in", csr], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
result: returns z value
"""
rsaPrivKey = M2Crypto.RSA.load_key(key)
r = rsaPrivKey.private_decrypt(y, M2Crypto.RSA.pkcs1_oaep_padding)
def generateExtension(self, key, y):
"""
Generates z to be placed in certificate extension
s = Random.get_random_bytes(S_SIZE)
extHMAC = hmac.new(r, str(s), hashlib.sha256)
return byteToHex(s) + extHMAC.hexdigest()
key: string - File path to key
y: byte array
def byteToHex(byteStr):
"""
Converts binary array to hex string
byteStr: byte array
result: returns hex representation of byteStr
"""
result: returns z value
"""
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
rsaPrivKey = M2Crypto.RSA.load_key(key)
r = rsaPrivKey.private_decrypt(y, M2Crypto.RSA.pkcs1_oaep_padding)
#Searches for the first extension specified in binary
def updateCertConf(oid, value):
"""
Updates the sni_challenge openssl certificate config file
s = Random.get_random_bytes(S_SIZE)
extHMAC = hmac.new(r, str(s), hashlib.sha256)
return self.byteToHex(s) + extHMAC.hexdigest()
oid: string - ex. 1.3.3.7
value string hex - value of OID
def byteToHex(self, byteStr):
"""
Converts binary array to hex string
result: updated certificate config file
"""
byteStr: byte array
confOld = open(CHOC_CERT_CONF)
confNew = open(CHOC_CERT_CONF + ".tmp", 'w')
flag = False
for line in confOld:
if "=critical, DER:" in line:
confNew.write(oid + "=critical, DER:" + value + "\n")
flag = True
else:
confNew.write(line)
if flag is False:
print "Error: Could not find extension in CHOC_CERT_CONF"
exit()
confNew.close()
confOld.close()
remove(CHOC_CERT_CONF)
move(CHOC_CERT_CONF + ".tmp", CHOC_CERT_CONF)
result: returns hex representation of byteStr
"""
def cleanup(listSNITuple, configurator):
"""
Remove all temporary changes necessary to perform the challenge
configurator: Configurator object
listSNITuple: The initial challenge tuple
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
result: Apache server is restored to the pre-challenge state
"""
configurator.revert_config()
configurator.restart(True)
remove_files(listSNITuple)
#Searches for the first extension specified in binary
def updateCertConf(self, oid, value):
"""
Updates the sni_challenge openssl certificate config file
def remove_files(listSNITuple):
"""
Removes all of the temporary SNI files
"""
for tup in listSNITuple:
remove(getChocCertFile(tup[2]))
remove(APACHE_CHALLENGE_CONF)
oid: string - ex. 1.3.3.7
value string hex - value of OID
#main call
def perform_sni_cert_challenge(listSNITuple, csr, key, configurator, quiet=False):
"""
Sets up and reloads Apache server to handle SNI challenges
result: updated certificate config file
"""
listSNITuple: List of tuples with form (addr, y, nonce, ext_oid)
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
"""
# Save any changes to the configuration as a precaution
# About to make temporary changes to the config
configurator.save("Before performing sni_challenge")
confOld = open(CHOC_CERT_CONF)
confNew = open(CHOC_CERT_CONF + ".tmp", 'w')
flag = False
for line in confOld:
if "=critical, DER:" in line:
confNew.write(oid + "=critical, DER:" + value + "\n")
flag = True
else:
confNew.write(line)
if flag is False:
print "Error: Could not find extension in CHOC_CERT_CONF"
exit()
confNew.close()
confOld.close()
remove(CHOC_CERT_CONF)
move(CHOC_CERT_CONF + ".tmp", CHOC_CERT_CONF)
addresses = []
default_addr = "*:443"
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, default_addr):
return False
def cleanup(self):
"""
Remove all temporary changes necessary to perform the challenge
for a in vhost.addrs:
if "_default_" in a:
addresses.append([default_addr])
break
else:
addresses.append(vhost.addrs)
configurator: Configurator object
listSNITuple: The initial challenge tuple
for tup in listSNITuple:
ext = generateExtension(key, tup[1])
createChallengeCert(tup[3], ext, tup[2], csr, key)
modifyApacheConfig(findApacheConfigFile(), listSNITuple, addresses, key, configurator)
# Save reversible changes and restart the server
configurator.save("SNI Challenge", True)
configurator.restart(quiet)
return True
result: Apache server is restored to the pre-challenge state
"""
self.configurator.revert_config()
self.configurator.restart(True)
self.__remove_files()
def __remove_files(self):
"""
Removes all of the temporary SNI files
"""
for tup in self.listSNITuple:
remove(getChocCertFile(tup[2]))
remove(APACHE_CHALLENGE_CONF)
#main call
def perform(self, quiet=False):
"""
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)
csr: string - File path to chocolate csr
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("Before performing sni_challenge")
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 False
if not self.configurator.make_server_sni_ready(vhost, default_addr):
return False
for a in vhost.addrs:
if "_default_" in a:
addresses.append([default_addr])
break
else:
addresses.append(vhost.addrs)
for tup in self.listSNITuple:
ext = self.generateExtension(self.key, tup[1])
self.createChallengeCert(tup[3], ext, tup[2], self.csr, self.key)
self.modifyApacheConfig(self.findApacheConfigFile(), addresses, self.key)
# Save reversible changes and restart the server
self.configurator.save("SNI Challenge", True)
self.configurator.restart(quiet)
return True
# This main function is just used for testing
def main():
key = path.abspath("key.pem")
csr = path.abspath("req.pem")
logger.setLogger(sys.stdout)
logger.setLogLevel(logger.INFO)
testkey = M2Crypto.RSA.load_key(key)
@@ -274,15 +291,15 @@ def main():
challenges = [("example.com", y, nonce, "1.3.3.7"), ("www.example.com",y2, nonce2, "1.3.3.7")]
#challenges = [("127.0.0.1", y, nonce, "1.3.3.7"), ("localhost", y2, nonce2, "1.3.3.7")]
if perform_sni_cert_challenge(challenges, csr, key, config):
sni_chall = SNI_Challenge(challenges, csr, 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"
cleanup(challenges, config)
sni_chall.cleanup()
else:
print "Failed SNI challenge..."