Merge branch 'master' of github.com:letsencrypt/lets-encrypt-preview

This commit is contained in:
James Kasten 2014-11-18 18:00:41 -08:00
commit badd23b717
15 changed files with 185 additions and 197 deletions

View file

@ -16,10 +16,10 @@ def main():
sys.exit("\nOnly root can run letsencrypt.\n")
# Parse options
try:
opts, args = getopt.getopt(sys.argv[1:], "", ["text", "test",
"view-checkpoints",
"privkey=", "csr=",
"server=", "rollback=",
opts, args = getopt.getopt(sys.argv[1:], "", ["text", "test",
"view-checkpoints",
"privkey=", "csr=",
"server=", "rollback=",
"revoke", "agree-eula",
"redirect",
"no-redirect",

View file

@ -40,13 +40,13 @@ NONCE_SIZE = 16
RSA_KEY_SIZE = 2048
# bits of hashcash to generate
difficulty = 23
difficulty = 23
# Let's Encrypt cert and chain files
CERT_PATH = CERT_DIR + "cert-letsencrypt.pem"
CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem"
#Invalid Extension
#Invalid Extension
INVALID_EXT = ".acme.invalid"
# Challenge Preferences Dict for currently supported challenges

View file

@ -12,12 +12,20 @@ schemata = {schema: json.load(open("letsencrypt/client/schemata/%s.json" % schem
}
def acme_object_validate(j):
"""Validate a JSON object against the ACME protocol using JSON Schema.
Success will return None; failure to validate will raise a
jsonschema.ValidationError exception describing the reason that the
object could not be validated successfully."""
j = json.loads(j)
if not isinstance(j, dict):
raise jsonschema.ValidationError("this is not a dictionary object")
if not j.has_key("type"):
if "type" not in j:
raise jsonschema.ValidationError("missing type field")
if not schemata.has_key(j["type"]):
if j["type"] not in schemata:
raise jsonschema.ValidationError("unknown type %s" % j["type"])
jsonschema.validate(j, schemata[j["type"]])
def pretty(s):
"""Return a pretty-printed version of any JSON string (useful when
printing out protocol messages for debugging purposes."""
return json.dumps(json.loads(s), indent=4)

View file

@ -10,4 +10,3 @@ class Challenge(object):
logger.error("Error - base class challenge.generate_response()")
def clean(self):
logger.error("Error - base class challenge.clean()")

View file

@ -1,11 +1,8 @@
#!/usr/bin/env python
import M2Crypto
import urllib2, json
# XXX TODO: per https://docs.google.com/document/pub?
#id=1roBIeSJsYq3Ntpf6N0PIeeAAvu4ddn7mGo6Qb7aL7ew
# urllib2 is unsafe (!) and must be replaced
import os, grp, pwd, sys, time, random, sys, shutil
import json
import os, time, sys, shutil
# This line suppresses the no logging found for module 'jose' warning
# TODO: Check out this module and see if we should be using it for our
@ -15,20 +12,15 @@ logging.basicConfig(filename="/dev/null", level=logging.ERROR)
import jose, csv
import subprocess
from M2Crypto import EVP, X509, RSA
from Crypto.Random import get_random_bytes
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
import requests
from letsencrypt.client.acme import acme_object_validate
from letsencrypt.client.sni_challenge import SNI_Challenge
from letsencrypt.client.payment_challenge import Payment_Challenge
from letsencrypt.client import configurator
from letsencrypt.client import logger, display
from letsencrypt.client import le_util, crypto_util, display
from letsencrypt.client.CONFIG import NONCE_SIZE, RSA_KEY_SIZE, CERT_PATH
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
@ -51,7 +43,7 @@ class Client(object):
self.config = configurator.Configurator(SERVER_ROOT)
self.server = ca_server
self.csr_file = cert_signing_request
self.key_file = private_key
@ -71,7 +63,7 @@ class Client(object):
sys.exit(1)
self.redirect = redirect
# Display preview warning
if not eula:
with open('EULA') as f:
@ -203,7 +195,7 @@ class Client(object):
os.remove(c['backup_cert_file'])
os.remove(c['backup_key_file'])
def list_certs_keys(self):
list_file = CERT_KEY_BACKUP + "LIST"
certs = []
@ -215,10 +207,10 @@ class Client(object):
c_sha1_vh = {}
for x in self.config.get_all_certs_keys():
try:
c_sha1_vh[M2Crypto.X509.load_cert(x[0]).get_fingerprint(md='sha1')] = x[2]
c_sha1_vh[M2Crypto.X509.load_cert(x[0]).get_fingerprint(md='sha1')] = x[2]
except:
continue
with open(list_file, 'rb') as csvfile:
csvreader = csv.reader(csvfile)
for row in csvreader:
@ -316,7 +308,7 @@ class Client(object):
#if self.ocsp_stapling:
# TODO enable OCSP Stapling
# continue
def certificate_request(self, csr_der, key):
logger.info("Preparing and sending CSR..")
@ -477,11 +469,16 @@ class Client(object):
def send(self, json_obj):
try:
acme_object_validate(json.dumps(json_obj))
response = urllib2.urlopen(
self.server_url, json.dumps(json_obj)).read()
acme_object_validate(response)
return json.loads(response)
json_encoded = json.dumps(json_obj)
acme_object_validate(json_encoded)
response = requests.post(
self.server_url,
data=json_encoded,
headers={"Content-Type": "application/json"},
)
body = response.content
acme_object_validate(body)
return response.json()
except:
logger.fatal("Send() failed... may have lost connection to server")
sys.exit(8)
@ -691,7 +688,7 @@ class Client(object):
def sanity_check_names(self, names):
for name in names:
if not self.is_hostname_sane(name):
logger.fatal(`name` + " is an impossible hostname")
logger.fatal(repr(name) + " is an impossible hostname")
sys.exit(81)
def is_hostname_sane(self, hostname):

View file

@ -3,18 +3,16 @@ import subprocess
import re
import os
import sys
import stat
import socket
import time
import shutil
import errno
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
#from CONFIG import SERVER_ROOT, BACKUP_DIR, REWRITE_HTTPS_ARGS, CONFIG_DIR,
#from CONFIG import SERVER_ROOT, BACKUP_DIR, REWRITE_HTTPS_ARGS, CONFIG_DIR,
#from CONFIG import WORK_DIR, TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR, OPTIONS_SSL_CONF, TRUSTIFY_VHOST_EXT
#import logger, le_util
@ -38,7 +36,7 @@ from letsencrypt.client import logger, le_util
# Note: This protocol works for filenames with spaces in it, the sites are
# properly set up and directives are changed appropriately, but Apache won't
# recognize names in sites-enabled that have spaces. These are not added to the
# Apache configuration. It may be wise to warn the user if they are trying
# Apache configuration. It may be wise to warn the user if they are trying
# to use vhost filenames that contain spaces and offer to change ' ' to '_'
# Note: FILEPATHS and changes to files are transactional. They are copied
@ -69,24 +67,24 @@ class Configurator(object):
This class was originally developed for Apache 2.2 and has not seen a
an overhaul to include proper setup of new Apache configurations.
The biggest changes have been the IncludeOptional directive, the
deprecation of the NameVirtualHost directive, and the name change of
The biggest changes have been the IncludeOptional directive, the
deprecation of the NameVirtualHost directive, and the name change of
mod_ssl.c to ssl_module. Although these changes
have not been implemented yet, they will be shortly.
That being said, this class can still adequately configure most typical
Apache 2.4 servers as the deprecated NameVirtualHost has no effect
and the typical directories are parsed by the Augeas configuration
parser automatically.
The API of this class will change in the coming weeks as the exact
needs of client's are clarified with the new and developing protocol.
This class will eventually derive from a generic Configurator class
so that other Configurators (like Nginx) can be developed and interoperate
with the client.
"""
def __init__(self, server_root=SERVER_ROOT):
# TODO: this instantiation can be optimized to only load Httd
# TODO: this instantiation can be optimized to only load Httd
# relevant files - I believe -> NO_MODL_AUTOLOAD
self.server_root = server_root
@ -103,7 +101,7 @@ class Configurator(object):
self.check_parsing_errors()
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
# Determine user's main config file
self.__set_user_config_file()
@ -114,20 +112,20 @@ class Configurator(object):
self.assoc = dict()
# Verify that all directories and files exist with proper permissions
self.verify_setup()
# Note: initialization doesn't check to see if the config is correct
# by Apache's standards. This should be done by the client (client.py)
# if it is desired. There may be instances where correct configuration
# if it is desired. There may be instances where correct configuration
# isn't required on startup.
# TODO: This function can be improved to ensure that the final directives
# are being modified whether that be in the include files or in the
# TODO: This function can be improved to ensure that the final directives
# are being modified whether that be in the include files or in the
# virtualhost declaration - these directives can be overwritten
def deploy_cert(self, vhost, cert, key, cert_chain=None):
"""
Currently tries to find the last directives to deploy the cert in
the given virtualhost. If it can't find the directives, it searches
the "included" confs. The function verifies that it has located
the "included" confs. The function verifies that it has located
the three directives and finally modifies them to point to the correct
destination
TODO: Make sure last directive is changed
@ -136,23 +134,23 @@ class Configurator(object):
"""
search = {}
path = {}
path["cert_file"] = self.find_directive(self.case_i("SSLCertificateFile"), None, vhost.path)
path["cert_key"] = self.find_directive(self.case_i("SSLCertificateKeyFile"), None, vhost.path)
# Only include if a certificate chain is specified
if cert_chain is not None:
path["cert_chain"] = self.find_directive(self.case_i("SSLCertificateChainFile"), None, vhost.path)
if len(path["cert_file"]) == 0 or len(path["cert_key"]) == 0:
# Throw some "can't find all of the directives error"
logger.warn("Warn: cannot find a cert or key directive in " + vhost.path)
logger.warn("VirtualHost was not modified")
# Presumably break here so that the virtualhost is not modified
return False
logger.info("Deploying Certificate to VirtualHost %s" % vhost.file)
self.aug.set(path["cert_file"][0], cert)
self.aug.set(path["cert_key"][0], key)
if cert_chain is not None:
@ -160,7 +158,7 @@ class Configurator(object):
self.add_dir(vhost.path, "SSLCertificateChainFile", cert_chain)
else:
self.aug.set(path["cert_chain"][0], cert_chain)
self.save_notes += "Changed vhost at %s with addresses of %s\n" % (vhost.file, vhost.addrs)
self.save_notes += "\tSSLCertificateFile %s\n" % cert
self.save_notes += "\tSSLCertificateKeyFile %s\n" % key
@ -218,7 +216,7 @@ class Configurator(object):
"""
self.assoc[dn] = vh
return
def get_all_names(self):
"""
Returns all names found in the Apache Configuration
@ -259,7 +257,7 @@ class Configurator(object):
#def __is_private_ip(ipaddr):
# re.compile()
def __add_servernames(self, host):
"""
@ -270,7 +268,7 @@ class Configurator(object):
args = self.aug.match(name + "/*")
for arg in args:
host.add_name(self.aug.get(arg))
def __create_vhost(self, path):
"""
@ -307,13 +305,13 @@ class Configurator(object):
addr: string
"""
# search for NameVirtualHost directive for ip_addr
# check httpd.conf, ports.conf,
# check httpd.conf, ports.conf,
# note ip_addr can be FQDN although Apache does not recommend it
paths = self.find_directive(self.case_i("NameVirtualHost"), None)
name_vh = []
for p in paths:
name_vh.append(self.aug.get(p))
# Mixed and matched wildcard NameVirtualHost with VirtualHost
# behavior is undefined. Make sure that an exact match exists
@ -321,7 +319,7 @@ class Configurator(object):
for vh in name_vh:
if vh == addr:
return True
return False
def add_name_vhost(self, addr):
@ -332,17 +330,17 @@ class Configurator(object):
"""
aug_file_path = "/files%sports.conf" % self.server_root
self.add_dir_to_ifmodssl(aug_file_path, "NameVirtualHost", addr)
if len(self.find_directive(self.case_i("NameVirtualHost"), self.case_i(addr))) == 0:
logger.warn("ports.conf is not included in your Apache config...")
logger.warn("Adding NameVirtualHost directive to httpd.conf")
self.add_dir_to_ifmodssl("/files" + self.server_root + "httpd.conf", "NameVirtualHost", addr)
self.save_notes += 'Setting %s to be NameBasedVirtualHost\n' % addr
def add_dir_to_ifmodssl(self, aug_conf_path, directive, val):
"""
Adds given directived and value along configuration path within
Adds given directived and value along configuration path within
an IfMod mod_ssl.c block. If the IfMod block does not exist in
the file, it is created.
"""
@ -377,7 +375,7 @@ class Configurator(object):
# Check for NameVirtualHost
# First see if any of the vhost addresses is a _default_ addr
for addr in vhost.addrs:
tup = addr.partition(":")
tup = addr.partition(":")
if tup[0] == "_default_":
if not self.is_name_vhost(default_addr):
logger.debug("Setting all VirtualHosts on " + default_addr + " to be name based virtual hosts")
@ -389,7 +387,7 @@ class Configurator(object):
if not self.is_name_vhost(addr):
logger.debug("Setting VirtualHost at" + addr + "to be a name based virtual host")
self.add_name_vhost(addr)
return True
def get_ifmod(self, aug_conf_path, mod):
@ -404,7 +402,7 @@ class Configurator(object):
ifMods = self.aug.match("%s/IfModule/*[self::arg='%s']" % (aug_conf_path, mod))
# Strip off "arg" at end of first ifmod path
return ifMods[0][:len(ifMods[0]) - 3]
def add_dir(self, aug_conf_path, directive, arg):
"""
Appends directive to end of file given by aug_conf_path
@ -414,9 +412,9 @@ class Configurator(object):
self.aug.set(aug_conf_path + "/directive[last()]/arg", arg)
else:
for i in range(len(arg)):
self.aug.set(aug_conf_path + "/directive[last()]/arg["+str(i+1)+"]", arg[i])
self.aug.set(aug_conf_path + "/directive[last()]/arg["+str(i+1)+"]", arg[i])
def find_directive(self, directive, arg=None, start=""):
"""
Recursively searches through config files to find directives
@ -424,17 +422,17 @@ class Configurator(object):
TODO: arg should probably be a list
Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like
insensitive. Augeas 1.0 allows case insensitive regexes like
regexp(/Listen/, 'i'), however the version currently supported
by Ubuntu 0.10 does not. Thus I have included my own case insensitive
transformation by calling case_i() on everything to maintain
compatibility.
"""
# Cannot place member variable in the definition of the function so...
if not start:
start = "/files%sapache2.conf" % self.server_root
#Debug code
#print "find_dir:", directive, "arg:", arg, " | Looking in:", start
# No regexp code
@ -442,20 +440,20 @@ class Configurator(object):
# matches = self.aug.match(start + "//*[self::directive='"+directive+"']/arg")
# else:
# matches = self.aug.match(start + "//*[self::directive='" + directive+"']/* [self::arg='" + arg + "']")
# includes = self.aug.match(start + "//* [self::directive='Include']/* [label()='arg']")
if arg is None:
matches = self.aug.match(start + "//*[self::directive=~regexp('%s')]/arg" % directive)
else:
matches = self.aug.match(start + "//*[self::directive=~regexp('%s')]/*[self::arg=~regexp('%s')]" % (directive, arg))
includes = self.aug.match(start + "//* [self::directive=~regexp('%s')]/* [label()='arg']" % self.case_i('Include'))
for include in includes:
# start[6:] to strip off /files
matches.extend(self.find_directive(directive, arg, self.get_include_path(self.strip_dir(start[6:]), self.aug.get(include))))
return matches
def case_i(self, string):
@ -463,15 +461,15 @@ class Configurator(object):
Returns a sloppy, but necessary version of a case insensitive regex.
Any string should be able to be submitted and the string is
escaped and then made case insensitive.
May be replaced by a more proper /i once augeas 1.0 is widely
May be replaced by a more proper /i once augeas 1.0 is widely
supported.
"""
return "".join(["["+c.upper()+c.lower()+"]" if c.isalpha() else c for c in re.escape(string)])
def strip_dir(self, path):
"""
Precondition: file_path is a file path, ie. not an augeas section
Precondition: file_path is a file path, ie. not an augeas section
or directive path
Returns the current directory from a file_path along with the file
"""
@ -483,7 +481,7 @@ class Configurator(object):
def get_include_path(self, cur_dir, arg):
"""
Converts an Apache Include directive argument into an Augeas
Converts an Apache Include directive argument into an Augeas
searchable path
Returns path string
"""
@ -493,11 +491,11 @@ class Configurator(object):
# 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
# 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.*?_-/]*")
@ -513,10 +511,10 @@ class Configurator(object):
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
@ -524,14 +522,14 @@ class Configurator(object):
postfix = ""
splitArg = arg.split("/")
for idx, split in enumerate(splitArg):
# * and ? are the two special fnmatch characters
# * 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]
@ -563,7 +561,7 @@ class Configurator(object):
ssl_fp = avail_fp[:-(len(".conf"))] + LE_VHOST_EXT
else:
ssl_fp = avail_fp + LE_VHOST_EXT
# First register the creation so that it is properly removed if
# configuration is rolled back
self.register_file_creation(False, ssl_fp)
@ -617,7 +615,7 @@ class Configurator(object):
logger.info("Created an SSL vhost at %s" % ssl_fp)
self.save_notes += 'Created ssl vhost at %s\n' % ssl_fp
self.save()
# We know the length is one because of the assertion above
ssl_vhost = self.__create_vhost(vh_p[0])
self.vhosts.append(ssl_vhost)
@ -633,7 +631,7 @@ class Configurator(object):
self.add_name_vhost(ssl_addrs[i])
logger.info("Enabling NameVirtualHosts on " + ssl_addrs[i])
need_to_save = True
if need_to_save:
self.save()
@ -699,14 +697,14 @@ class Configurator(object):
return True, 0
# Rewrite path exists but is not a letsencrypt https rule
return True, 2
def create_redirect_vhost(self, ssl_vhost):
# Consider changing this to a dictionary check
# Make sure adding the vhost will be safe
conflict, hostOrAddrs = self.__conflicting_host(ssl_vhost)
if conflict:
return False, hostOrAddrs
redirect_addrs = hostOrAddrs
# get servernames and serveraliases
@ -729,7 +727,7 @@ RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [L,R=permanent]\n\
ErrorLog /var/log/apache2/redirect.error.log \n\
LogLevel warn \n\
</VirtualHost>\n"
# Write out the file
# This is the default name
redirect_filename = "letsencrypt-redirect.conf"
@ -758,12 +756,12 @@ LogLevel warn \n\
new_fp = self.server_root + "sites-available/" + redirect_filename
new_vhost = self.__create_vhost("/files" + new_fp)
self.vhosts.append(new_vhost)
# Finally create documentation for the change
self.save_notes += 'Created a port 80 vhost, %s, for redirection to ssl vhost %s\n' % (new_vhost.file, ssl_vhost.file)
return True, new_vhost
def __conflicting_host(self, ssl_vhost):
'''
Checks for a conflicting host, such that a new port 80 host could not
@ -798,7 +796,7 @@ LogLevel warn \n\
redirect_addrs = redirect_addrs + ssl_a_vhttp
return False, redirect_addrs
def __general_vhost(self, ssl_vhost):
"""
Function needs to be thoroughly tested and perhaps improved
@ -811,7 +809,7 @@ LogLevel warn \n\
ssl_addrs = ssl_vhost.addrs
if ssl_addrs == ["_default_:443"]:
ssl_addrs = ["*:443"]
for vh in self.vhosts:
found = 0
# Not the same vhost, and same number of addresses
@ -826,7 +824,7 @@ LogLevel warn \n\
if test_tup[2] == "80" or test_tup[2] == "" or test_tup[2] == "*":
found += 1
break
# Check to make sure all addresses were found
# Check to make sure all addresses were found
# and names are equal
if found == len(ssl_vhost.addrs) and set(vh.names) == set(ssl_vhost.names):
return vh
@ -875,7 +873,7 @@ LogLevel warn \n\
continue
break
return avail_fp
def is_site_enabled(self, avail_fp):
"""
Checks to see if the given site is enabled
@ -903,18 +901,18 @@ LogLevel warn \n\
self.save_notes += 'Enabled site %s\n' % vhost.file
return True
return False
def enable_mod(self, mod_name):
"""
Enables mod_ssl
"""
try:
# Use check_output so the command will finish before reloading
# Use check_output so the command will finish before reloading
subprocess.check_call(["sudo", "a2enmod", mod_name], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
# Hopefully this waits for output
# Hopefully this waits for output
subprocess.check_call(["sudo", "/etc/init.d/apache2", "restart"], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
except:
logger.error("Error enabling mod_" + mod_name)
logger.error("Error enabling mod_" + mod_name)
sys.exit(1)
def fnmatch_to_re(self, cleanFNmatch):
@ -949,12 +947,12 @@ LogLevel warn \n\
#self.aug.add_transform("Httpd.lns", self.httpd_incl, None, self.httpd_excl)
self.__add_httpd_transform(file_path)
self.aug.load()
def save_apache_config(self):
# Not currently used
# Should be safe because it is a protected directory
shutil.copytree(self.server_root, BACKUP_DIR + "apache2-" + str(time.time()))
def recovery_routine(self):
"""
Revert all previously modified files. First, any changes found in
@ -997,13 +995,13 @@ LogLevel warn \n\
except IOError:
logger.fatal("Unable to remove filepaths contained within %s" % file_list)
sys.exit(41)
return True
def verify_setup(self):
'''
Make sure that files/directories are setup with appropriate permissions
Aim for defensive coding... make sure all input files
Aim for defensive coding... make sure all input files
have permissions of root
'''
le_util.make_or_verify_dir(CONFIG_DIR, 0755)
@ -1025,7 +1023,7 @@ LogLevel warn \n\
# This is a hack... work around... submit to augeas if still not fixed
excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak", "*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew", "*~", self.server_root + "*.augsave", self.server_root + "*~", self.server_root + "*/*augsave", self.server_root + "*/*~", self.server_root + "*/*/*.augsave", self.server_root + "*/*/*~"]
for i in range(len(excl)):
self.aug.set("/augeas/load/Httpd/excl[%d]" % (i+1), excl[i])
@ -1040,7 +1038,7 @@ LogLevel warn \n\
for e in error_files:
# Check to see if it was an error resulting from the use of
# the httpd lens
# the httpd lens
lens_path = self.aug.get(e + '/lens')
# As aug.get may return null
if lens_path and 'httpd.aug' in lens_path:
@ -1072,7 +1070,7 @@ LogLevel warn \n\
try:
p = subprocess.Popen(['/etc/init.d/apache2', 'restart'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
text = p.communicate()
if p.returncode != 0:
# Enter recovery routine...
@ -1118,7 +1116,7 @@ LogLevel warn \n\
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.
@ -1148,7 +1146,7 @@ LogLevel warn \n\
return False
# Retrieve list of modified files
# Note: Noop saves can cause the file to be listed twice, I used a
# 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")
@ -1172,7 +1170,7 @@ LogLevel warn \n\
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)
@ -1181,7 +1179,7 @@ LogLevel warn \n\
# 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 = ""
@ -1213,7 +1211,7 @@ LogLevel warn \n\
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
@ -1226,7 +1224,7 @@ LogLevel warn \n\
idx = len(existing_filepaths)
for filename in save_files:
if filename not in existing_filepaths:
# Tag files with index so multiple files can
# 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))
@ -1252,7 +1250,7 @@ LogLevel warn \n\
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)
@ -1266,11 +1264,11 @@ LogLevel warn \n\
def __recover_checkpoint(self, cp_dir):
"""
Recover a specific checkpoint provided by cp_dir
Note: this function does not reload augeas.
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:
@ -1328,13 +1326,13 @@ LogLevel warn \n\
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:"
@ -1356,7 +1354,7 @@ LogLevel warn \n\
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:
@ -1364,8 +1362,8 @@ LogLevel warn \n\
fd.write("%s\n" % f)
except:
logger.error("ERROR: Unable to register file creation")
def main():
config = Configurator()
@ -1379,7 +1377,7 @@ def main():
print name
"""
print config.find_directive(config.case_i("NameVirtualHost"), config.case_i("holla:443"))
"""
for m in config.find_directive("Listen", "443"):
print "Directive Path:", m, "Value:", config.aug.get(m)
@ -1417,7 +1415,7 @@ def main():
if vh.addrs[0] == "23.20.47.131:80":
print "Here we go"
ssl_vh = config.make_vhost_ssl(vh)
config.redirect_all_ssl(ssl_vh)
"""
"""
@ -1425,6 +1423,6 @@ def main():
if len(vh.names) > 0:
config.deploy_cert(vh, "/home/james/Documents/apache_choc/req.pem", "/home/james/Documents/apache_choc/key.pem", "/home/james/Downloads/sub.class1.server.ca.pem")
"""
if __name__ == "__main__":
main()

View file

@ -8,7 +8,6 @@ from Crypto.Hash import SHA256
from M2Crypto import EVP, X509, ASN1
from letsencrypt.client import logger
from letsencrypt.client.CONFIG import NONCE_SIZE, RSA_KEY_SIZE
@ -59,7 +58,7 @@ def make_key(bits=RSA_KEY_SIZE):
#rsa = M2Crypto.RSA.gen_key(bits, 65537)
#key_pem = rsa.as_pem(cipher=None)
#rsa = None # should not be freed here
return key.exportKey(format='PEM')
@ -147,7 +146,7 @@ def get_cert_info(filename):
d["san"] = x.get_ext("subjectAltName").get_value()
except:
d["san"] = ""
d["serial"] = x.get_serial_number()
d["pub_key"] = "RSA " + str(x.get_pubkey().size() * 8)
return d

View file

@ -25,7 +25,7 @@ class Display(SingletonD):
raise Exception("Error no display defined")
def success_installation(self, domains):
raise Exception("Error no display defined")
def gen_https_names(self, domains):
"""
Returns a string of the domains formatted nicely with https:// prepended
@ -94,12 +94,12 @@ class NcursesDisplay(Display):
def generic_input(self, message):
return self.d.inputbox(message)
def generic_yesno(self, message, yes = "Yes", no = "No"):
a = self.d.yesno(message, HEIGHT, WIDTH)
return a == self.d.DIALOG_OK
def filter_names(self, names):
choices = [(n, "", 0) for n in names]
c, s = self.d.checklist("Which names would you like to activate \
@ -115,12 +115,12 @@ class NcursesDisplay(Display):
def display_certs(self, certs):
list_choices = [
(str(i+1),
"%s | %s | %s" %
(str(c["cn"].ljust(WIDTH - 39)),
c["not_before"].strftime("%m-%d-%y"),
"Installed" if c["installed"] else ""))
"%s | %s | %s" %
(str(c["cn"].ljust(WIDTH - 39)),
c["not_before"].strftime("%m-%d-%y"),
"Installed" if c["installed"] else ""))
for i, c in enumerate(certs)]
code, s = self.d.menu("Which certificates would you like to revoke?",
choices = list_choices, help_button=True,
help_label="More Info", ok_label="Revoke",
@ -204,7 +204,7 @@ class FileDisplay(Display):
menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] +
" - " + str(c["not_before"])[:-6])
for i, c in enumerate(certs)]
self.outfile.write("Which certificate would you like to revoke?\n")
for c in menu_choices:
wm = textwrap.fill("%s: %s - %s Signed (UTC): %s\n" %
@ -243,9 +243,9 @@ class FileDisplay(Display):
s_f = '*' * (79)
wm = textwrap.fill(("Congratulations! You have successfully " +
"enabled %s!") % self.gen_https_names(domains))
msg = "%s\n%s\n%s\n"
msg = "%s\n%s\n%s\n"
self.outfile.write(msg % (s_f, wm, s_f))
def confirm_revocation(self, cert):
self.outfile.write("Are you sure you would like to revoke \
the following certificate:\n")
@ -300,10 +300,10 @@ def redirect_by_default():
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
("Secure", "Make all requests redirect to secure HTTPS access")]
result = display.generic_menu("Please choose whether HTTPS access " +
"is required or optional.",
choices,
choices,
"Please enter the appropriate number",
width = WIDTH)

View file

@ -1,13 +1,12 @@
from letsencrypt.client.challenge import Challenge
from letsencrypt.client import logger
import textwrap
############################################################
# Possible addition to challenge structure: priority parameter
# If only DVSNI and Payment are required, the user might want
# to be validated before submitting payment, allowing the user
# to gain confidence in the system. If things do go poorly the
# user has less invested in that particular session/transaction.
# to gain confidence in the system. If things do go poorly the
# user has less invested in that particular session/transaction.
#############################################################
###########################################################
@ -22,7 +21,7 @@ class Interactive_Challenge(Challenge):
def __init__(self, string):
self.string = string
def perform(self, quiet=True):
if quiet:
dialog.Dialog().msgbox(get_display_string(), width=BOX_SIZE)
@ -31,7 +30,7 @@ class Interactive_Challenge(Challenge):
raw_input('')
return True
def get_display_string(self):
return textwrap.fill(self.string, width=BOX_SIZE) + "\n\nPlease Press Enter to Continue"

View file

@ -1,4 +1,3 @@
import sys
import time
from letsencrypt.client import display
@ -55,20 +54,20 @@ class FileLogger(Logger):
import dialog
class NcursesLogger(Logger):
def __init__(self,
firstmessage="",
height = display.HEIGHT,
def __init__(self,
firstmessage="",
height = display.HEIGHT,
width = display.WIDTH - 4):
self.lines = []
self.all_content = ""
self.d = dialog.Dialog()
self.height = height
self.width = width
self.add(firstmessage)
self.add(firstmessage)
'''
Only show the last (self.height) lines;
note that lines can wrap at self.width, so
note that lines can wrap at self.width, so
a single line could actually be multiple lines
'''
def add(self, s):
@ -93,7 +92,7 @@ class NcursesLogger(Logger):
if cur_out != '':
self.lines.append(cur_out)
# show last 16 lines
self.content = '\n'.join(self.lines[-self.height:])
self.show()
@ -110,7 +109,7 @@ log_instance = None
def setLogger(log_inst):
global log_instance
log_instance = log_inst
def setLogLevel(log_level):
global log_instance
log_instance.level = log_level
@ -140,7 +139,7 @@ def fatal(data):
def none(data):
# Uh...what?
pass
pass
if __name__ == "__main__":
# Unit test/example usage:
@ -165,12 +164,7 @@ if __name__ == "__main__":
time.sleep(0.3)
# Alternatively, use
# Alternatively, use
logger.error("errrrr")
logger.trace("some trace data: %d - %f - %s" % (5, 8.3, 'cows'))

View file

@ -1,13 +1,12 @@
from letsencrypt.client.challenge import Challenge
from letsencrypt.client import logger
import dialog
############################################################
# Possible addition to challenge structure: priority parameter
# If only DVSNI and Payment are required, the user might want
# to be validated before submitting payment, allowing the user
# to gain confidence in the system. If things do go poorly the
# user has less invested in that particular session/transaction.
# to gain confidence in the system. If things do go poorly the
# user has less invested in that particular session/transaction.
#############################################################
class Payment_Challenge(Challenge):
@ -21,7 +20,7 @@ class Payment_Challenge(Challenge):
def cleanup(self):
# Currently, payment challenges do not appear to require any cleanup.
pass
def perform(self, quiet=True):
if quiet:
dialog.Dialog().msgbox(self.get_display_string(), width=70)
@ -31,7 +30,7 @@ class Payment_Challenge(Challenge):
self.times_performed += 1
return True
def get_display_string(self):
if self.times_performed == 0:
@ -40,8 +39,7 @@ class Payment_Challenge(Challenge):
# The user has tried at least once... display a different message
else:
return "The CA did not record your payment, please visit " + self.url + " for more information or to finish processing your transaction.\nPress Enter to continue"
def formatted_reasons(self):
return "\n\t* %s\n" % self.reason

View file

@ -1,11 +1,12 @@
import requests
from letsencrypt.client.challenge import Challenge
from letsencrypt.client import logger
from letsencrypt.client.CONFIG import RECOVERY_TOKEN_EXT
# TODO: Replace urllib2 because of lack of certificate validation checks
import dialog, urllib2
import dialog
class RecoveryContact(Challenge):
def __init__(self, activationURL = "", successURL = "", contact = "", poll_delay = 3):
self.token = ""
self.activationURL = activationURL
@ -23,14 +24,14 @@ class RecoveryContact(Challenge):
exit, self.token = d.inputbox(self.get_display_string()))
if exit != d.OK:
return False
else:
print self.get_display_string()
if successURL:
return self.poll(10, quiet)
else:
self.token = raw_input("Enter the recovery token:")
return True
def cleanup(self):
@ -45,10 +46,10 @@ class RecoveryContact(Challenge):
string += " or respond to the recovery email sent to " + self.contact
elif self.contact:
string += "Recovery email sent to" + self.contact
def poll(self, rounds = 10, quiet = True):
for i in range(rounds):
if urllib2.urlopen(self.successURL).getcode() != 200:
if requests.get(self.successURL).status_code != 200:
time.sleep(self.poll_delay)
else:
return True

View file

@ -1,18 +1,16 @@
from letsencrypt.client.challenge import Challenge
from letsencrypt.client import logger
from letsencrypt.client.CONFIG import RECOVERY_TOKEN_EXT
class RecoveryToken(Challenge):
def __init__(self):
self.token = ""
def perform(self, quiet = True):
cancel, self.token = dialog.generic_input("Please Input Recovery Token: ")
if cancel == 1:
return False
return True
def cleanup(self):

View file

@ -1,22 +1,18 @@
#!/usr/bin/env python
import subprocess
import M2Crypto
from Crypto import Random
import hmac
import hashlib
from shutil import move
from os import remove, close, path
from os import path
import sys
import binascii
import augeas
import jose
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, NONCE_SIZE
from letsencrypt.client.CONFIG import S_SIZE
from letsencrypt.client import logger, crypto_util
from letsencrypt.client.challenge import Challenge
@ -41,7 +37,7 @@ class SNI_Challenge(Challenge):
self.key = key_filepath
self.configurator = config
self.s = None
def getDvsniCertFile(self, nonce):
"""
@ -117,7 +113,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
def checkForApacheConfInclude(self, mainConfig):
"""
Adds DVSNI challenge include file if it does not already exist
Adds DVSNI challenge include file if it does not already exist
within mainConfig
mainConfig: string - file path to main user apache config file
@ -147,7 +143,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
#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'))
@ -179,7 +175,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
h = hashlib.new('sha256')
h.update(r)
h.update(s)
return h.hexdigest() + INVALID_EXT
def byteToHex(self, byteStr):
@ -205,7 +201,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
"""
self.configurator.revert_challenge_config()
self.configurator.restart(True)
def generate_response(self):
"""
Generates a response for a completed challenge
@ -275,14 +271,14 @@ def main():
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 = jose.b64encode_url(r)
r2 = jose.b64encode_url(r2)
@ -296,7 +292,7 @@ def main():
nonce = binascii.hexlify(nonce)
nonce2 = binascii.hexlify(nonce2)
config = configurator.Configurator()
challenges = [("client.theobroma.info", r, nonce), ("foo.theobroma.info",r2, nonce2)]

View file

@ -71,6 +71,7 @@ setup(
],
install_requires=[
#'dialog',
'requests',
'protobuf',
'python-augeas',
'pycrypto',