Merge branch 'configurator_refactor', Enable modular configuration

editing to support development for other webservers.
This commit is contained in:
James Kasten 2014-11-20 15:43:31 -08:00
commit f64570c5db
8 changed files with 2086 additions and 1740 deletions

View file

@ -55,5 +55,8 @@ CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]
# Mutually Exclusive Challenges - only solve 1
EXCLUSIVE_CHALLENGES = [set(["dvsni", "simpleHttps"])]
# These are challenges that must be solved by a Configurator object
CONFIG_CHALLENGES = {"dvsni", "simpleHttps"}
# Rewrite rule arguments used for redirections to https vhost
REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,424 @@
import abc, os, sys, shutil, time
from letsencrypt.client.configurator import Configurator
import augeas
from letsencrypt.client import le_util, logger
from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
from letsencrypt.client.CONFIG import BACKUP_DIR
class AugeasConfigurator(Configurator):
def __init__(self):
super(AugeasConfigurator, self).__init__()
# TODO: this instantiation can be optimized to only load
# relevant files - I believe -> NO_MODL_AUTOLOAD
# Set Augeas flags to save backup
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
self.save_notes = ""
def deploy_cert(self, vhost, cert, key , cert_chain=None):
raise Exception("Error: augeas Configurator class")
def choose_virtual_host(self, name):
"""
Chooses a virtual host based on a given domain name
"""
raise Exception("Error: augeas Configurator class")
def get_all_names(self):
"""
Return all names found in the Configuration
"""
raise Exception("Error: augeas Configurator class")
def enable_redirect(self, ssl_vhost):
"""
Makes all traffic redirect to the given ssl_vhost
ie. port 80 => 443
"""
raise Exception("Error: augeas Configurator class")
def enable_hsts(self, ssl_vhost):
"""
Enable HSTS on the given ssl_vhost
"""
raise Exception("Error: augeas Configurator class")
def enable_ocsp_stapling(self, ssl_vhost):
"""
Enable OCSP stapling on given ssl_vhost
"""
raise Exception("Error: augeas Configurator class")
def get_all_certs_keys(self):
"""
Retrieve all certs and keys set in configuration
return list of tuples with form [(cert, key, path)]
"""
raise Exception("Error: augeas Configurator class")
def enable_site(self, vhost):
"""
Enable the site at the given vhost
"""
raise Exception("Error: augeas Configurator class")
def check_parsing_errors(self, lens):
"""
This function checks to see if Augeas was unable to parse any of the
lens files
"""
error_files = self.aug.match("/augeas//error")
for e in error_files:
# Check to see if it was an error resulting from the use of
# the httpd lens
lens_path = self.aug.get(e + '/lens')
# As aug.get may return null
if lens_path and lens in lens_path:
# Strip off /augeas/files and /error
logger.error('There has been an error in parsing the file: %s' % e[13:len(e) - 6])
logger.error(self.aug.get(e + '/message'))
def save(self, title=None, temporary=False):
"""
Saves all changes to the configuration files
This function is not transactional
TODO: Instead rely on challenge to backup all files before modifications
title: string - The title of the save. If a title is given, the
configuration will be saved as a new checkpoint
and put in a timestamped directory.
`title` has no effect if temporary is true.
temporary: boolean - Indicates whether the changes made will be
quickly reversed in the future (challenges)
"""
save_state = self.aug.get("/augeas/save")
self.aug.set("/augeas/save", "noop")
# Existing Errors
ex_errs = self.aug.match("/augeas//error")
try:
# This is a noop save
self.aug.save()
except:
# Check for the root of save problems
new_errs = self.aug.match("/augeas//error")
logger.error("During Save - " + mod_conf)
# Only print new errors caused by recent save
for err in new_errs:
if err not in ex_errs:
logger.error("Unable to save file - %s" % err[13:len(err)-6])
logger.error("Attempted Save Notes")
logger.error(self.save_notes)
# Erase Save Notes
self.save_notes = ""
return False
# Retrieve list of modified files
# Note: Noop saves can cause the file to be listed twice, I used a
# set to remove this possibility. This is a known augeas 0.10 error.
save_paths = self.aug.match("/augeas/events/saved")
# If the augeas tree didn't change, no files were saved and a backup
# should not be created
if save_paths:
save_files = set()
for p in save_paths:
save_files.add(self.aug.get(p)[6:])
valid, message = self.check_tempfile_saves(save_files, temporary)
if not valid:
logger.fatal(message)
# What is the protocol in this situation?
# This shouldn't happen if the challenge codebase is correct
return False
# Create Checkpoint
if temporary:
self.add_to_checkpoint(TEMP_CHECKPOINT_DIR, save_files)
else:
self.add_to_checkpoint(IN_PROGRESS_DIR, save_files)
if title and not temporary and os.path.isdir(IN_PROGRESS_DIR):
success = self.__finalize_checkpoint(IN_PROGRESS_DIR, title)
if not success:
# This should never happen
# This will be hopefully be cleaned up on the recovery
# routine startup
sys.exit(9)
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
self.aug.save()
return True
def revert_challenge_config(self):
"""
This function should reload the users original configuration files
for all saves with reversible=True
"""
if os.path.isdir(TEMP_CHECKPOINT_DIR):
result = self.__recover_checkpoint(TEMP_CHECKPOINT_DIR)
changes = True
if result != 0:
# We have a partial or incomplete recovery
logger.fatal("Incomplete or failed recovery for %s" % TEMP_CHECKPOINT_DIR)
sys.exit(67)
# Remember to reload Augeas
self.aug.load()
def rollback_checkpoints(self, rollback = 1):
""" Revert 'rollback' number of configuration checkpoints """
try:
rollback = int(rollback)
except:
logger.error("Rollback argument must be a positive integer")
# Sanity check input
if rollback < 1:
logger.error("Rollback argument must be a positive integer")
return
backups = os.listdir(BACKUP_DIR)
backups.sort()
if len(backups) < rollback:
logger.error("Unable to rollback %d checkpoints, only %d exist" % (rollback, len(backups)))
while rollback > 0 and backups:
cp_dir = BACKUP_DIR + backups.pop()
result = self.__recover_checkpoint(cp_dir)
if result != 0:
logger.fatal("Failed to load checkpoint during rollback")
sys.exit(39)
rollback -= 1
self.aug.load()
def display_checkpoints(self):
"""
Displays all saved checkpoints
Note: Any 'IN_PROGRESS' checkpoints will be removed by the cleanup
script found in the constructor, before this function would ever be
called
"""
backups = os.listdir(BACKUP_DIR)
backups.sort(reverse=True)
if not backups:
print "Letsencrypt has not saved any backups of your apache configuration"
# Make sure there isn't anything unexpected in the backup folder
# There should only be timestamped (float) directories
try:
for bu in backups:
float(bu)
except:
assert False, "Invalid files in %s" % BACKUP_DIR
for bu in backups:
print time.ctime(float(bu))
with open(BACKUP_DIR + bu + "/CHANGES_SINCE") as f:
print f.read()
print "Affected files:"
with open(BACKUP_DIR + bu + "/FILEPATHS") as f:
filepaths = f.read().splitlines()
for fp in filepaths:
print " %s" % fp
try:
with open(BACKUP_DIR + bu + "/NEW_FILES") as f:
print "New Configuration Files:"
filepaths = f.read().splitlines()
for fp in filepaths:
print " %s" % fp
except:
pass
print ""
def __finalize_checkpoint(self, cp_dir, title):
"""
Add title to cp_dir CHANGES_SINCE
Move cp_dir to Backups directory and rename with timestamp
"""
final_dir = BACKUP_DIR + str(time.time())
try:
with open(cp_dir + "CHANGES_SINCE.tmp", 'w') as ft:
ft.write("-- %s --\n" % title)
with open(cp_dir + "CHANGES_SINCE", 'r') as f:
ft.write(f.read())
shutil.move(cp_dir + "CHANGES_SINCE.tmp", cp_dir + "CHANGES_SINCE")
except:
logger.error("Unable to finalize checkpoint - adding title")
return False
try:
os.rename(cp_dir, final_dir)
except:
logger.error("Unable to finalize checkpoint, %s -> %s" % cp_dir, final_dir)
return False
return True
def add_to_checkpoint(self, cp_dir, save_files):
le_util.make_or_verify_dir(cp_dir, 0755)
existing_filepaths = []
op_fd = None
# Open up FILEPATHS differently depending on if it already exists
if os.path.isfile(cp_dir + "FILEPATHS"):
op_fd = open(cp_dir + "FILEPATHS", 'r+')
existing_filepaths = op_fd.read().splitlines()
else:
op_fd = open(cp_dir + "FILEPATHS", 'w')
idx = len(existing_filepaths)
for filename in save_files:
if filename not in existing_filepaths:
# Tag files with index so multiple files can
# have the same filename
logger.debug("Creating backup of %s" % filename)
shutil.copy2(filename, cp_dir + os.path.basename(filename) + "_" + str(idx))
op_fd.write(filename + '\n')
idx += 1
op_fd.close()
with open(cp_dir + "CHANGES_SINCE", 'a') as notes_fd:
notes_fd.write(self.save_notes)
def __recover_checkpoint(self, cp_dir):
"""
Recover a specific checkpoint provided by cp_dir
Note: this function does not reload augeas.
returns: 0 success, 1 Unable to revert, -1 Unable to delete
"""
if os.path.isfile(cp_dir + "/FILEPATHS"):
try:
with open(cp_dir + "/FILEPATHS") as f:
filepaths = f.read().splitlines()
for idx, fp in enumerate(filepaths):
shutil.copy2(cp_dir + '/' + os.path.basename(fp) + '_' + str(idx), fp)
except:
# This file is required in all checkpoints.
logger.error("Unable to recover files from %s" % cp_dir)
return 1
# Remove any newly added files if they exist
self.__remove_contained_files(cp_dir + "/NEW_FILES")
try:
shutil.rmtree(cp_dir)
except:
logger.error("Unable to remove directory: %s" % cp_dir)
return -1
return 0
def check_tempfile_saves(self, save_files, temporary):
temp_path = "%sFILEPATHS" % TEMP_CHECKPOINT_DIR
if os.path.isfile(temp_path):
with open(temp_path, 'r') as protected_fd:
protected_files = protected_fd.read().splitlines()
for filename in protected_files:
if filename in save_files:
return False, "Attempting to overwrite challenge file - %s" % filename
return True, "Successful"
def register_file_creation(self, temporary, *files):
"""
This is used to register the creation of all files during Letsencrypt
execution. Call this method before writing to the file to make sure
that the file will be cleaned up if the program exits unexpectedly.
(Before a save occurs)
"""
if temporary:
cp_dir = TEMP_CHECKPOINT_DIR
else:
cp_dir = IN_PROGRESS_DIR
le_util.make_or_verify_dir(cp_dir)
try:
with open(cp_dir + "NEW_FILES", 'a') as fd:
for f in files:
fd.write("%s\n" % f)
except:
logger.error("ERROR: Unable to register file creation")
def recovery_routine(self):
"""
Revert all previously modified files. First, any changes found in
TEMP_CHECKPOINT_DIR are removed, then IN_PROGRESS changes are removed
The order is important. IN_PROGRESS is unable to add files that are
already added by a TEMP change. Thus TEMP must be rolled back first
because that will be the 'latest' occurrence of the file.
"""
self.revert_challenge_config()
if os.path.isdir(IN_PROGRESS_DIR):
result = self.__recover_checkpoint(IN_PROGRESS_DIR)
if result != 0:
# We have a partial or incomplete recovery
# Not as egregious
# TODO: Additional tests? recovery
logger.fatal("Incomplete or failed recovery for %s" % IN_PROGRESS_DIR)
sys.exit(68)
# Need to reload configuration after these changes take effect
self.aug.load()
def __remove_contained_files(self, file_list):
"""
Erase any files contained within the text file, file_list
"""
# Check to see that file exists to differentiate can't find file_list
# and can't remove filepaths within file_list errors.
if not os.path.isfile(file_list):
return False
try:
with open(file_list, 'r') as f:
filepaths = f.read().splitlines()
for fp in filepaths:
# Files are registered before they are added... so check to see if file
# exists first
if os.path.lexists(fp):
os.remove(fp)
else:
logger.warn("File: %s - Could not be found to be deleted\nProgram was probably shut down unexpectedly, in which case this is not a problem" % fp)
except IOError:
logger.fatal("Unable to remove filepaths contained within %s" % file_list)
sys.exit(41)
return True
def config_test(self):
"""
Make sure the configuration is valid
"""
raise Exception("Error: augeas Configurator class")
def restart(self):
"""
Restart or refresh the server content
"""
raise Exception("Error: augeas Configurator class")
def perform(self, challenge):
""" Perform the challenge """
raise Exception("Error: augeas Configurator class")
def cleanup(self):
""" Clean up any challenge configurations """
raise Exception("Error: augeas Configurator class")

View file

@ -10,13 +10,13 @@ import requests
from letsencrypt.client.acme import acme_object_validate
from letsencrypt.client.sni_challenge import SNI_Challenge
from letsencrypt.client import configurator
from letsencrypt.client import configurator, apache_configurator
from letsencrypt.client import logger, display
from letsencrypt.client import le_util, crypto_util
from letsencrypt.client.CONFIG import RSA_KEY_SIZE, CERT_PATH
from letsencrypt.client.CONFIG import CHAIN_PATH, SERVER_ROOT, KEY_DIR, CERT_DIR
from letsencrypt.client.CONFIG import CERT_KEY_BACKUP
from letsencrypt.client.CONFIG import CHALLENGE_PREFERENCES, EXCLUSIVE_CHALLENGES
from letsencrypt.client.CONFIG import CERT_KEY_BACKUP, EXCLUSIVE_CHALLENGES
from letsencrypt.client.CONFIG import CHALLENGE_PREFERENCES, CONFIG_CHALLENGES
# it's weird to point to chocolate servers via raw IPv6 addresses, and such
# addresses can be %SCARY in some contexts, so out of paranoia let's disable
# them by default
@ -33,7 +33,10 @@ class Client(object):
# Logger needs to be initialized before Configurator
self.init_logger()
self.config = configurator.Configurator(SERVER_ROOT)
# TODO: Can probably figure out which configurator to use without
# special packaging based on system info
# Command line arg or client function to discover
self.config = apache_configurator.ApacheConfigurator(SERVER_ROOT)
self.server = ca_server
@ -52,7 +55,7 @@ class Client(object):
def authenticate(self, domains = [], redirect = None, eula = False):
# Check configuration
if not self.config.configtest():
if not self.config.config_test():
sys.exit(1)
self.redirect = redirect
@ -60,7 +63,7 @@ class Client(object):
# Display preview warning
if not eula:
with open('EULA') as f:
if not display.generic_yesno(f.read(), "Agree", "Disagree"):
if not display.generic_yesno(f.read(), "Agree", "Cancel"):
sys.exit(0)
# Display screen to select domains to validate
@ -82,11 +85,6 @@ class Client(object):
# TODO: Use correct server depending on CA
#choice = self.choice_of_ca()
# Check first if mod_ssl is loaded
if not self.config.check_ssl_loaded():
logger.info("Loading mod_ssl into Apache Server")
self.config.enable_mod("ssl")
#Request Challenges
challenge_dict = self.handle_challenge()
@ -288,7 +286,6 @@ class Client(object):
self.redirect = display.redirect_by_default()
if self.redirect:
self.config.enable_mod("rewrite")
self.redirect_to_ssl(vhost)
self.config.restart(quiet=self.curses)
@ -312,7 +309,11 @@ class Client(object):
def cleanup_challenges(self, challenge_objs):
logger.info("Cleaning up challenges...")
for c in challenge_objs:
c.cleanup()
if c["type"] in CONFIG_CHALLENGES:
self.config.cleanup()
else:
#Handle other cleanup if needed
pass
def is_expected_msg(self, msg_dict, expected, delay=3, rounds = 20):
for i in range(rounds):
@ -372,16 +373,19 @@ class Client(object):
challenge_objs, indicies = self.challenge_factory(
self.names[0], c["challenges"], path)
responses = ["null"] * len(c["challenges"])
responses = [None] * len(c["challenges"])
# Perform challenges and populate responses
# Perform challenges
for i, c_obj in enumerate(challenge_objs):
if not c_obj.perform():
logger.fatal("Challenge Failed")
sys.exit(1)
response = "null"
if c_obj["type"] in CONFIG_CHALLENGES:
response = self.config.perform(c_obj)
else:
# Handle RecoveryToken type challenges
pass
for index in indicies[i]:
responses[index] = c_obj.generate_response()
responses[index] = response
logger.info("Configured Apache for challenges; " +
"waiting for verification...")
@ -392,7 +396,7 @@ class Client(object):
"""
Generate a plan to get authority over the identity
TODO: Make sure that the challenges are feasible...
TODO Example: Do you have the recovery key?
Example: Do you have the recovery key?
"""
if combos:
@ -515,14 +519,12 @@ class Client(object):
def redirect_to_ssl(self, vhost):
for ssl_vh in vhost:
success, redirect_vhost = self.config.redirect_all_ssl(ssl_vh)
logger.info("\nRedirect vhost: " + redirect_vhost.file +
success, redirect_vhost = self.config.enable_redirect(ssl_vh)
logger.info("\nRedirect vhost: " + redirect_vhost.file +
" - " + str(success))
# If successful, make sure redirect site is enabled
if success:
if not self.config.is_site_enabled(redirect_vhost.file):
self.config.enable_site(redirect_vhost)
logger.info("Enabling available site: " + redirect_vhost.file)
# If successful, make sure redirect site is enabled
if success:
self.config.enable_site(redirect_vhost)
def get_virtual_hosts(self, domains):
@ -551,7 +553,7 @@ class Client(object):
elif challenges[c]["type"] == "recoveryToken":
logger.info("\tRecovery Token Challenge for name: %s." % name)
challenge_objs_indicies.append(c)
challenge_objs.append(RecoveryToken())
challenge_objs.append({type:"recoveryToken"})
else:
logger.fatal("Challenge not currently supported")
@ -560,8 +562,8 @@ class Client(object):
if sni_todo:
# SNI_Challenge can satisfy many sni challenges at once so only
# one "challenge object" is issued for all sni_challenges
challenge_objs.append(SNI_Challenge(
sni_todo, os.path.abspath(self.key_file), self.config))
challenge_objs.append({"type":"dvsni", "listSNITuple":sni_todo,
"dvsni_key":os.path.abspath(self.key_file)})
challenge_obj_indicies.append(sni_satisfies)
logger.debug(sni_todo)
@ -627,33 +629,36 @@ class Client(object):
return selection
def get_cas(self):
DV_choices = []
OV_choices = []
EV_choices = []
choices = []
try:
with open("/etc/letsencrypt/.ca_offerings") as f:
for line in f:
choice = line.split(";", 1)
if 'DV' in choice[0]:
DV_choices.append(choice)
elif 'OV' in choice[0]:
OV_choices.append(choice)
else:
EV_choices.append(choice)
# Legacy Code: Although I would like to see a free and open marketplace
# in the future. The Let's Encrypt Client will not have this feature at
# launch
# def get_cas(self):
# DV_choices = []
# OV_choices = []
# EV_choices = []
# choices = []
# try:
# with open("/etc/letsencrypt/.ca_offerings") as f:
# for line in f:
# choice = line.split(";", 1)
# if 'DV' in choice[0]:
# DV_choices.append(choice)
# elif 'OV' in choice[0]:
# OV_choices.append(choice)
# else:
# EV_choices.append(choice)
# random.shuffle(DV_choices)
# random.shuffle(OV_choices)
# random.shuffle(EV_choices)
choices = DV_choices + OV_choices + EV_choices
choices = [(l[0], l[1]) for l in choices]
# # random.shuffle(DV_choices)
# # random.shuffle(OV_choices)
# # random.shuffle(EV_choices)
# choices = DV_choices + OV_choices + EV_choices
# choices = [(l[0], l[1]) for l in choices]
except IOError as e:
logger.fatal("Unable to find .ca_offerings file")
sys.exit(1)
# except IOError as e:
# logger.fatal("Unable to find .ca_offerings file")
# sys.exit(1)
return choices
# return choices
def get_all_names(self):
"""

File diff suppressed because it is too large Load diff

View file

@ -309,11 +309,11 @@ def redirect_by_default():
"Please enter the appropriate number",
width = WIDTH)
if result[0] != 0:
if result[0] != OK:
return False
# different answer for each type of display
return (result[1] == "Secure" or result[1] == 1)
return (str(result[1]) == "Secure" or result[1] == 1)
def confirm_revocation(cert):

View file

@ -0,0 +1,222 @@
from letsencrypt.client.CONFIG import SERVER_ROOT, BACKUP_DIR
from letsencrypt.client.CONFIG import REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR
from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
from letsencrypt.client.CONFIG import OPTIONS_SSL_CONF, LE_VHOST_EXT
from letsencrypt.client import logger, le_util, configurator
# This might be helpful... but feel free to use whatever you want
# class VH(object):
# def __init__(self, filename_path, vh_path, vh_addrs, is_ssl, is_enabled):
# self.file = filename_path
# self.path = vh_path
# self.addrs = vh_addrs
# self.names = []
# self.ssl = is_ssl
# self.enabled = is_enabled
# def set_names(self, listOfNames):
# self.names = listOfNames
# def add_name(self, name):
# self.names.append(name)
class NginxConfigurator(AugeasConfigurator):
def __init__(self, server_root=SERVER_ROOT):
self.server_root = server_root
# See if any temporary changes need to be recovered
# This needs to occur before VH objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.recovery_routine()
# Check for errors in parsing files with Augeas
# TODO - insert nginx lens info here???
#self.check_parsing_errors("httpd.aug")
def deploy_cert(self, vhost, cert, key, cert_chain=None):
"""
Deploy cert in nginx
"""
return
def choose_virtual_host(self, name):
"""
Chooses a virtual host based on the given domain name
"""
return None
def get_all_names(self):
"""
Returns all names found in the nginx configuration
"""
all_names = set()
return all_names
# Might be helpful... I know nothing about nginx lens
# def get_include_path(self, cur_dir, arg):
# """
# Converts an Apache Include directive argument into an Augeas
# searchable path
# Returns path string
# """
# # Sanity check argument - maybe
# # Question: what can the attacker do with control over this string
# # Effect parse file... maybe exploit unknown errors in Augeas
# # If the attacker can Include anything though... and this function
# # only operates on Apache real config data... then the attacker has
# # already won.
# # Perhaps it is better to simply check the permissions on all
# # included files?
# # check_config to validate apache config doesn't work because it
# # would create a race condition between the check and this input
# # TODO: Fix this
# # Check to make sure only expected characters are used <- maybe remove
# # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# # matchObj = validChars.match(arg)
# # if matchObj.group() != arg:
# # logger.error("Error: Invalid regexp characters in %s" % arg)
# # return []
# # Standardize the include argument based on server root
# if not arg.startswith("/"):
# arg = cur_dir + arg
# # conf/ is a special variable for ServerRoot in Apache
# elif arg.startswith("conf/"):
# arg = self.server_root + arg[5:]
# # TODO: Test if Apache allows ../ or ~/ for Includes
# # Attempts to add a transform to the file if one does not already exist
# self.parse_file(arg)
# # Argument represents an fnmatch regular expression, convert it
# # Split up the path and convert each into an Augeas accepted regex
# # then reassemble
# if "*" in arg or "?" in arg:
# postfix = ""
# splitArg = arg.split("/")
# for idx, split in enumerate(splitArg):
# # * and ? are the two special fnmatch characters
# if "*" in split or "?" in split:
# # Turn it into a augeas regex
# # TODO: Can this instead be an augeas glob instead of regex
# splitArg[idx] = "* [label()=~regexp('%s')]" % self.fnmatch_to_re(split)
# # Reassemble the argument
# arg = "/".join(splitArg)
# # If the include is a directory, just return the directory as a file
# if arg.endswith("/"):
# return "/files" + arg[:len(arg)-1]
# return "/files"+arg
def enable_redirect(self, ssl_vhost):
"""
Adds Redirect directive to the port 80 equivalent of ssl_vhost
First the function attempts to find the vhost with equivalent
ip addresses that serves on non-ssl ports
The function then adds the directive
"""
return
def enable_ocsp_stapling(self, ssl_vhost):
return False
def enable_hsts(self, ssl_vhost):
return False
def get_all_certs_keys(self):
"""
Retrieve all certs and keys set in VirtualHosts on the Apache server
returns: list of tuples with form [(cert, key, path)]
"""
return None
# Probably helpful reference
# def get_file_path(self, vhost_path):
# """
# Takes in Augeas path and returns the file name
# """
# # Strip off /files
# avail_fp = vhost_path[6:]
# # This can be optimized...
# while True:
# # Cast both to lowercase to be case insensitive
# find_if = avail_fp.lower().find("/ifmodule")
# if find_if != -1:
# avail_fp = avail_fp[:find_if]
# continue
# find_vh = avail_fp.lower().find("/virtualhost")
# if find_vh != -1:
# avail_fp = avail_fp[:find_vh]
# continue
# break
# return avail_fp
def enable_site(self, vhost):
"""
Enables an available site, Apache restart required
"""
return False
# Might be a usefule reference
# def parse_file(self, file_path):
# """
# Checks to see if file_path is parsed by Augeas
# If file_path isn't parsed, the file is added and Augeas is reloaded
# """
# # Test if augeas included file for Httpd.lens
# # Note: This works for augeas globs, ie. *.conf
# incTest = self.aug.match("/augeas/load/Httpd/incl [. ='" + file_path + "']")
# if not incTest:
# # Load up files
# #self.httpd_incl.append(file_path)
# #self.aug.add_transform("Httpd.lns", self.httpd_incl, None, self.httpd_excl)
# self.__add_httpd_transform(file_path)
# self.aug.load()
# Helpful reference?
# def verify_setup(self):
# '''
# Make sure that files/directories are setup with appropriate permissions
# Aim for defensive coding... make sure all input files
# have permissions of root
# '''
# le_util.make_or_verify_dir(CONFIG_DIR, 0755)
# le_util.make_or_verify_dir(WORK_DIR, 0755)
# le_util.make_or_verify_dir(BACKUP_DIR, 0755)
def restart(self, quiet=False):
"""
Restarts nginx server
"""
return
# May be of use?
# def __add_httpd_transform(self, incl):
# """
# This function will correctly add a transform to augeas
# The existing augeas.add_transform in python is broken
# """
# lastInclude = self.aug.match("/augeas/load/Httpd/incl [last()]")
# self.aug.insert(lastInclude[0], "incl", False)
# self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def config_test(self):
""" Check Configuration """
return False
def main():
return
if __name__ == "__main__":
main()

View file

@ -1,312 +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 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.createCHOC_CERT_CONF(name, ext)
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)
#print ["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", self.getDvsniCertFile(nonce), "-in", csr]
#subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", self.getDvsniCertFile(nonce), "-in", csr], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
# def createCHOC_CERT_CONF(self, name, ext):
# """
# Generates an OpenSSL certificate configuration file
# """
# text = " # OpenSSL configuration file. \n\n \
# [ v3_ca ] \n \
# basicConstraints = CA:TRUE\n\
# subjectAltName = @alt_names\n\n\
# [ alt_names ]\n"
# with open(CHOC_CERT_CONF, 'w') as f:
# f.write(text)
# f.write("DNS:1 = %s\n" % name)
# f.write("DNS:2 = %s\n" % ext)
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()