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()")
+29 -19
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,8 +431,9 @@ 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")
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...")
@@ -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
+57 -40
View File
@@ -16,8 +16,26 @@ 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):
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
def getChocCertFile(self, nonce):
"""
Returns standardized name for challenge certificate
@@ -28,20 +46,18 @@ def getChocCertFile(nonce):
return WORK_DIR + nonce + ".crt"
def findApacheConfigFile():
def findApacheConfigFile(self):
"""
Locates the file path to the user's main apache config
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"
logger.error("Unable to find httpd.conf, file does not exist in Apache ServerRoot")
return None
def getConfigText(nonce, ip_addrs, key):
def __getConfigText(self, nonce, ip_addrs, key):
"""
Chocolate virtual server configuration text
@@ -67,7 +83,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
return configText
def modifyApacheConfig(mainConfig, listSNITuple, listlistAddrs, key, configurator):
def modifyApacheConfig(self, mainConfig, listlistAddrs):
"""
Modifies Apache config files to include the challenge virtual servers
@@ -82,16 +98,15 @@ def modifyApacheConfig(mainConfig, listSNITuple, listlistAddrs, key, configurato
# 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 += self.__getConfigText(self.listSNITuple[idx][2], lis, self.key)
configText += "</IfModule> \n"
checkForApacheConfInclude(mainConfig, configurator)
self.checkForApacheConfInclude(mainConfig)
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, configurator):
def checkForApacheConfInclude(self, mainConfig):
"""
Adds chocolate challenge include file if it does not already exist
within mainConfig
@@ -100,11 +115,11 @@ def checkForApacheConfInclude(mainConfig, configurator):
result: User Apache configuration includes chocolate sni challenge file
"""
if len(configurator.find_directive("Include", APACHE_CHALLENGE_CONF)) == 0:
if len(self.configurator.find_directive("Include", APACHE_CHALLENGE_CONF)) == 0:
#print "Including challenge virtual host(s)"
configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
self.configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
def createChallengeCert(oid, ext, nonce, csr, key):
def createChallengeCert(self, oid, ext, nonce, csr, key):
"""
Modifies challenge certificate configuration and calls openssl binary to create a certificate
@@ -121,7 +136,7 @@ def createChallengeCert(oid, ext, nonce, csr, key):
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'))
def generateExtension(key, y):
def generateExtension(self, key, y):
"""
Generates z to be placed in certificate extension
@@ -136,9 +151,9 @@ def generateExtension(key, y):
s = Random.get_random_bytes(S_SIZE)
extHMAC = hmac.new(r, str(s), hashlib.sha256)
return byteToHex(s) + extHMAC.hexdigest()
return self.byteToHex(s) + extHMAC.hexdigest()
def byteToHex(byteStr):
def byteToHex(self, byteStr):
"""
Converts binary array to hex string
@@ -149,8 +164,8 @@ def byteToHex(byteStr):
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
#Searches for the first extension specified in binary
def updateCertConf(oid, value):
#Searches for the first extension specified in binary
def updateCertConf(self, oid, value):
"""
Updates the sni_challenge openssl certificate config file
@@ -177,7 +192,7 @@ def updateCertConf(oid, value):
remove(CHOC_CERT_CONF)
move(CHOC_CERT_CONF + ".tmp", CHOC_CERT_CONF)
def cleanup(listSNITuple, configurator):
def cleanup(self):
"""
Remove all temporary changes necessary to perform the challenge
@@ -186,21 +201,21 @@ def cleanup(listSNITuple, configurator):
result: Apache server is restored to the pre-challenge state
"""
configurator.revert_config()
configurator.restart(True)
remove_files(listSNITuple)
self.configurator.revert_config()
self.configurator.restart(True)
self.__remove_files()
def remove_files(listSNITuple):
def __remove_files(self):
"""
Removes all of the temporary SNI files
"""
for tup in listSNITuple:
for tup in self.listSNITuple:
remove(getChocCertFile(tup[2]))
remove(APACHE_CHALLENGE_CONF)
#main call
def perform_sni_cert_challenge(listSNITuple, csr, key, configurator, quiet=False):
#main call
def perform(self, quiet=False):
"""
Sets up and reloads Apache server to handle SNI challenges
@@ -213,19 +228,19 @@ def perform_sni_cert_challenge(listSNITuple, csr, key, configurator, quiet=False
"""
# Save any changes to the configuration as a precaution
# About to make temporary changes to the config
configurator.save("Before performing sni_challenge")
self.configurator.save("Before performing sni_challenge")
addresses = []
default_addr = "*:443"
for tup in listSNITuple:
vhost = configurator.choose_virtual_host(tup[0])
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 configurator.make_server_sni_ready(vhost, default_addr):
if not self.configurator.make_server_sni_ready(vhost, default_addr):
return False
for a in vhost.addrs:
@@ -235,20 +250,22 @@ def perform_sni_cert_challenge(listSNITuple, csr, key, configurator, quiet=False
else:
addresses.append(vhost.addrs)
for tup in listSNITuple:
ext = generateExtension(key, tup[1])
createChallengeCert(tup[3], ext, tup[2], csr, key)
for tup in self.listSNITuple:
ext = self.generateExtension(self.key, tup[1])
self.createChallengeCert(tup[3], ext, tup[2], self.csr, self.key)
modifyApacheConfig(findApacheConfigFile(), listSNITuple, addresses, key, configurator)
self.modifyApacheConfig(self.findApacheConfigFile(), addresses, self.key)
# Save reversible changes and restart the server
configurator.save("SNI Challenge", True)
configurator.restart(quiet)
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..."