mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 10:51:53 +02:00
Initial ACME compliant DVSNI commit
This commit is contained in:
@@ -33,5 +33,8 @@ difficulty = 23
|
|||||||
cert_file = CERT_DIR + "trustify-cert.pem"
|
cert_file = CERT_DIR + "trustify-cert.pem"
|
||||||
chain_file = CERT_DIR + "trustify-chain.pem"
|
chain_file = CERT_DIR + "trustify-chain.pem"
|
||||||
|
|
||||||
|
#Invalid Extension
|
||||||
|
INVALID_EXT = ".acme.invalid"
|
||||||
|
|
||||||
# Rewrite rule arguments used for redirections to https vhost
|
# Rewrite rule arguments used for redirections to https vhost
|
||||||
REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
|
REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
|
||||||
|
|||||||
Regular → Executable
+1
-25
@@ -327,7 +327,7 @@ class Client(object):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not self.csr_file:
|
if not self.csr_file:
|
||||||
csr_pem = self.make_csr(self.names)
|
csr_pem = trustify_util.make_csr(self.key_file, self.names)
|
||||||
# Save CSR
|
# Save CSR
|
||||||
trustify_util.make_or_verify_dir(CERT_DIR, 0755)
|
trustify_util.make_or_verify_dir(CERT_DIR, 0755)
|
||||||
csr_f, self.csr_file = trustify_util.unique_file(CERT_DIR + "csr-trustify.pem", 0644)
|
csr_f, self.csr_file = trustify_util.unique_file(CERT_DIR + "csr-trustify.pem", 0644)
|
||||||
@@ -356,30 +356,6 @@ class Client(object):
|
|||||||
|
|
||||||
return key_pem
|
return key_pem
|
||||||
|
|
||||||
def make_csr(self, domains):
|
|
||||||
"""
|
|
||||||
Returns new CSR in PEM form using self.key_file containing all domains
|
|
||||||
"""
|
|
||||||
assert domains, "Must provide one or more hostnames for the CSR."
|
|
||||||
rsa_key = M2Crypto.RSA.load_key(self.key_file)
|
|
||||||
pk = EVP.PKey()
|
|
||||||
pk.assign_rsa(rsa_key)
|
|
||||||
|
|
||||||
x = X509.Request()
|
|
||||||
x.set_pubkey(pk)
|
|
||||||
name = x.get_subject()
|
|
||||||
name.CN = domains[0]
|
|
||||||
extstack = X509.X509_Extension_Stack()
|
|
||||||
for d in domains:
|
|
||||||
ext = X509.new_extension('subjectAltName', 'DNS:%s' % d)
|
|
||||||
extstack.push(ext)
|
|
||||||
x.add_extensions(extstack)
|
|
||||||
x.sign(pk,'sha1')
|
|
||||||
assert x.verify(pk)
|
|
||||||
pk2 = x.get_pubkey()
|
|
||||||
assert x.verify(pk2)
|
|
||||||
return x.as_pem()
|
|
||||||
|
|
||||||
def __rsa_sign(self, key, data):
|
def __rsa_sign(self, key, data):
|
||||||
"""
|
"""
|
||||||
Sign this data with this private key. For client-side use.
|
Sign this data with this private key. For client-side use.
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import shutil
|
|||||||
import errno
|
import errno
|
||||||
|
|
||||||
from trustify.client.CONFIG import SERVER_ROOT, BACKUP_DIR
|
from trustify.client.CONFIG import SERVER_ROOT, BACKUP_DIR
|
||||||
#from CONFIG import SERVER_ROOT, BACKUP_DIR, MODIFIED_FILES, REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR
|
|
||||||
from trustify.client.CONFIG import REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR
|
from trustify.client.CONFIG import REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR
|
||||||
from trustify.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
|
from trustify.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
|
||||||
|
from trustify.client.CONFIG import OPTIONS_SSL_CONF
|
||||||
from trustify.client import logger, trustify_util
|
from trustify.client import logger, trustify_util
|
||||||
#import logger
|
#from CONFIG import SERVER_ROOT, BACKUP_DIR, REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR, TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR, OPTIONS_SSL_CONF
|
||||||
|
#import logger, trustify_util
|
||||||
|
|
||||||
# Question: Am I missing any attacks that can result from modifying CONFIG file?
|
# Question: Am I missing any attacks that can result from modifying CONFIG file?
|
||||||
# Configurator should be turned into a Singleton
|
# Configurator should be turned into a Singleton
|
||||||
@@ -66,6 +67,8 @@ class Configurator(object):
|
|||||||
# relevant files - I believe -> NO_MODL_AUTOLOAD
|
# relevant files - I believe -> NO_MODL_AUTOLOAD
|
||||||
# TODO: Use server_root instead SERVER_ROOT
|
# TODO: Use server_root instead SERVER_ROOT
|
||||||
|
|
||||||
|
self.server_root = server_root
|
||||||
|
|
||||||
# Set Augeas flags to save backup
|
# Set Augeas flags to save backup
|
||||||
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
|
self.aug = augeas.Augeas(flags=augeas.Augeas.NONE)
|
||||||
|
|
||||||
@@ -78,6 +81,9 @@ class Configurator(object):
|
|||||||
self.check_parsing_errors()
|
self.check_parsing_errors()
|
||||||
# This problem has been fixed in Augeas 1.0
|
# This problem has been fixed in Augeas 1.0
|
||||||
self.standardize_excl()
|
self.standardize_excl()
|
||||||
|
|
||||||
|
# Determine user's main config file
|
||||||
|
self.__set_user_config_file()
|
||||||
|
|
||||||
self.save_notes = ""
|
self.save_notes = ""
|
||||||
|
|
||||||
@@ -88,9 +94,9 @@ class Configurator(object):
|
|||||||
self.verify_setup()
|
self.verify_setup()
|
||||||
|
|
||||||
# Note: initialization doesn't check to see if the config is correct
|
# Note: initialization doesn't check to see if the config is correct
|
||||||
# by Apache's standards. This should be done by the client if it is
|
# by Apache's standards. This should be done by the client (client.py)
|
||||||
# desired. There may be instances where correct configuration isn't
|
# if it is desired. There may be instances where correct configuration
|
||||||
# required on startup.
|
# isn't required on startup.
|
||||||
|
|
||||||
# TODO: This function can be improved to ensure that the final directives
|
# 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
|
# are being modified whether that be in the include files or in the
|
||||||
@@ -217,8 +223,19 @@ class Configurator(object):
|
|||||||
|
|
||||||
return all_names
|
return all_names
|
||||||
|
|
||||||
def __is_private_ip(ipaddr):
|
def __set_user_config_file(self, filename = ''):
|
||||||
re.compile()
|
if filename:
|
||||||
|
self.user_config_file = filename
|
||||||
|
else:
|
||||||
|
# Basic check to see if httpd.conf exists and is included via direct include
|
||||||
|
# httpd.conf was very common as a user file in Apache 2.2
|
||||||
|
if os.path.isfile(self.server_root + 'httpd.conf') and self.find_directive(self.case_i("Include"), self.case_i("httpd.conf")):
|
||||||
|
self.user_config_file = self.server_root + 'httpd.conf'
|
||||||
|
else:
|
||||||
|
self.user_config_file = self.server_root + 'apache2.conf'
|
||||||
|
|
||||||
|
#def __is_private_ip(ipaddr):
|
||||||
|
# re.compile()
|
||||||
|
|
||||||
|
|
||||||
def __add_servernames(self, host):
|
def __add_servernames(self, host):
|
||||||
@@ -293,7 +310,7 @@ class Configurator(object):
|
|||||||
aug_file_path = "/files%sports.conf" % SERVER_ROOT
|
aug_file_path = "/files%sports.conf" % SERVER_ROOT
|
||||||
self.add_dir_to_ifmodssl(aug_file_path, "NameVirtualHost", addr)
|
self.add_dir_to_ifmodssl(aug_file_path, "NameVirtualHost", addr)
|
||||||
|
|
||||||
if len(self.find_directive(self.case_i("NameVirtualHost"), addr)) == 0:
|
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("ports.conf is not included in your Apache config...")
|
||||||
logger.warn("Adding NameVirtualHost directive to httpd.conf")
|
logger.warn("Adding NameVirtualHost directive to httpd.conf")
|
||||||
self.add_dir_to_ifmodssl("/files" + SERVER_ROOT + "httpd.conf", "NameVirtualHost", addr)
|
self.add_dir_to_ifmodssl("/files" + SERVER_ROOT + "httpd.conf", "NameVirtualHost", addr)
|
||||||
@@ -390,6 +407,10 @@ class Configurator(object):
|
|||||||
transformation by calling case_i() on everything to maintain
|
transformation by calling case_i() on everything to maintain
|
||||||
compatibility.
|
compatibility.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
#Debug code
|
||||||
|
#print "find_dir:", directive, "arg:", arg, " | Looking in:", start
|
||||||
|
# No regexp code
|
||||||
# if arg is None:
|
# if arg is None:
|
||||||
# matches = self.aug.match(start + "//*[self::directive='"+directive+"']/arg")
|
# matches = self.aug.match(start + "//*[self::directive='"+directive+"']/arg")
|
||||||
# else:
|
# else:
|
||||||
@@ -413,11 +434,15 @@ class Configurator(object):
|
|||||||
def case_i(self, string):
|
def case_i(self, string):
|
||||||
"""
|
"""
|
||||||
Returns a sloppy, but necessary version of a case insensitive regex.
|
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.
|
supported.
|
||||||
"""
|
"""
|
||||||
return '[' + "][".join([c.upper()+c.lower() for c in string]) + ']'
|
|
||||||
|
#return '[' + "][".join([c.upper()+c.lower() if c.isalpha() else c for c in re.escape(string)]) + ']'
|
||||||
|
return "".join(["["+c.upper()+c.lower()+"]" if c.isalpha() else c for c in re.escape(string)])
|
||||||
|
|
||||||
def strip_dir(self, path):
|
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
|
||||||
@@ -550,7 +575,7 @@ class Configurator(object):
|
|||||||
|
|
||||||
self.add_dir(vh_p[0], "SSLCertificateFile", "/etc/ssl/certs/ssl-cert-snakeoil.pem")
|
self.add_dir(vh_p[0], "SSLCertificateFile", "/etc/ssl/certs/ssl-cert-snakeoil.pem")
|
||||||
self.add_dir(vh_p[0], "SSLCertificateKeyFile", "/etc/ssl/private/ssl-cert-snakeoil.key")
|
self.add_dir(vh_p[0], "SSLCertificateKeyFile", "/etc/ssl/private/ssl-cert-snakeoil.key")
|
||||||
self.add_dir(vh_p[0], "Include", CONFIG_DIR + "options-ssl.conf")
|
self.add_dir(vh_p[0], "Include", OPTIONS_SSL_CONF)
|
||||||
|
|
||||||
# Log actions and create save notes
|
# Log actions and create save notes
|
||||||
logger.info("Created an SSL vhost at %s" % ssl_fp)
|
logger.info("Created an SSL vhost at %s" % ssl_fp)
|
||||||
@@ -927,7 +952,12 @@ LogLevel warn \n\
|
|||||||
with open(file_list, 'r') as f:
|
with open(file_list, 'r') as f:
|
||||||
filepaths = f.read().splitlines()
|
filepaths = f.read().splitlines()
|
||||||
for fp in filepaths:
|
for fp in filepaths:
|
||||||
os.remove(fp)
|
# Files are registered before they are added... so check to see if file
|
||||||
|
# exists first
|
||||||
|
if os.path.isfile(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:
|
except IOError:
|
||||||
logger.fatal("Unable to remove filepaths contained within %s" % file_list)
|
logger.fatal("Unable to remove filepaths contained within %s" % file_list)
|
||||||
sys.exit(41)
|
sys.exit(41)
|
||||||
@@ -975,11 +1005,14 @@ LogLevel warn \n\
|
|||||||
for e in error_files:
|
for e in error_files:
|
||||||
# Check to see if it was an error resulting from the use of
|
# Check to see if it was an error resulting from the use of
|
||||||
# the httpd lens
|
# the httpd lens
|
||||||
if 'httpd.aug' in self.aug.get(e + '/lens'):
|
lens_path = self.aug.get(e + '/lens')
|
||||||
|
# As aug.get may return null
|
||||||
|
if lens_path and 'httpd.aug' in lens_path:
|
||||||
# Strip off /augeas/files and /error
|
# 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('There has been an error in parsing the file: %s' % e[13:len(e) - 6])
|
||||||
logger.error(self.aug.get(e + '/message'))
|
logger.error(self.aug.get(e + '/message'))
|
||||||
|
|
||||||
|
|
||||||
def revert_challenge_config(self):
|
def revert_challenge_config(self):
|
||||||
"""
|
"""
|
||||||
This function should reload the users original configuration files
|
This function should reload the users original configuration files
|
||||||
@@ -1201,15 +1234,17 @@ LogLevel warn \n\
|
|||||||
|
|
||||||
returns: 0 success, 1 Unable to revert, -1 Unable to delete
|
returns: 0 success, 1 Unable to revert, -1 Unable to delete
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
with open(cp_dir + "/FILEPATHS") as f:
|
if os.path.isfile(cp_dir + "/FILEPATHS"):
|
||||||
filepaths = f.read().splitlines()
|
try:
|
||||||
for idx, fp in enumerate(filepaths):
|
with open(cp_dir + "/FILEPATHS") as f:
|
||||||
shutil.copy2(cp_dir + '/' + os.path.basename(fp) + '_' + str(idx), fp)
|
filepaths = f.read().splitlines()
|
||||||
except:
|
for idx, fp in enumerate(filepaths):
|
||||||
# This file is required in all checkpoints.
|
shutil.copy2(cp_dir + '/' + os.path.basename(fp) + '_' + str(idx), fp)
|
||||||
logger.error("Unable to recover files from %s" % cp_dir)
|
except:
|
||||||
return 1
|
# 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
|
# Remove any newly added files if they exist
|
||||||
self.__remove_contained_files(cp_dir + "/NEW_FILES")
|
self.__remove_contained_files(cp_dir + "/NEW_FILES")
|
||||||
@@ -1315,12 +1350,15 @@ def main():
|
|||||||
config = Configurator()
|
config = Configurator()
|
||||||
logger.setLogger(logger.FileLogger(sys.stdout))
|
logger.setLogger(logger.FileLogger(sys.stdout))
|
||||||
logger.setLogLevel(logger.DEBUG)
|
logger.setLogLevel(logger.DEBUG)
|
||||||
|
"""
|
||||||
for v in config.vhosts:
|
for v in config.vhosts:
|
||||||
print v.file
|
print v.file
|
||||||
print v.addrs
|
print v.addrs
|
||||||
for name in v.names:
|
for name in v.names:
|
||||||
print name
|
print name
|
||||||
|
"""
|
||||||
|
print config.find_directive(config.case_i("NameVirtualHost"), config.case_i("holla:443"))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for m in config.find_directive("Listen", "443"):
|
for m in config.find_directive("Listen", "443"):
|
||||||
print "Directive Path:", m, "Value:", config.aug.get(m)
|
print "Directive Path:", m, "Value:", config.aug.get(m)
|
||||||
|
|||||||
Regular → Executable
+82
-88
@@ -10,32 +10,39 @@ from os import remove, close, path
|
|||||||
import sys
|
import sys
|
||||||
import binascii
|
import binascii
|
||||||
import augeas
|
import augeas
|
||||||
|
import jose
|
||||||
|
|
||||||
from trustify.client import configurator
|
from trustify.client import configurator
|
||||||
|
|
||||||
from trustify.client.CONFIG import CONFIG_DIR, WORK_DIR, SERVER_ROOT
|
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 CHOC_CERT_CONF, OPTIONS_SSL_CONF, APACHE_CHALLENGE_CONF, INVALID_EXT
|
||||||
from trustify.client.CONFIG import S_SIZE, NONCE_SIZE
|
from trustify.client.CONFIG import S_SIZE, NONCE_SIZE
|
||||||
from trustify.client import logger
|
from trustify.client import logger, trustify_util
|
||||||
from trustify.client.challenge import Challenge
|
from trustify.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, trustify_util
|
||||||
|
# from challenge import Challenge
|
||||||
|
|
||||||
|
|
||||||
class SNI_Challenge(Challenge):
|
class SNI_Challenge(Challenge):
|
||||||
def __init__(self, sni_todos, req_filepath, key_filepath, config):
|
def __init__(self, sni_todos, key_filepath, config):
|
||||||
'''
|
'''
|
||||||
sni_todos: List of tuples with form (addr, y, nonce, ext_oid)
|
sni_todos: List of tuples with form (addr, r, nonce)
|
||||||
addr (string), y (byte array), nonce (hex string),
|
addr (string), r (base64 string), nonce (hex string)
|
||||||
ext_oid (string)
|
|
||||||
csr: string - File path to chocolate csr
|
|
||||||
key: string - File path to key
|
key: string - File path to key
|
||||||
configurator: Configurator obj
|
configurator: Configurator obj
|
||||||
'''
|
'''
|
||||||
self.listSNITuple = sni_todos
|
self.listSNITuple = sni_todos
|
||||||
self.csr = req_filepath
|
|
||||||
self.key = key_filepath
|
self.key = key_filepath
|
||||||
self.configurator = config
|
self.configurator = config
|
||||||
|
|
||||||
|
|
||||||
def getChocCertFile(self, nonce):
|
def getDvsniCertFile(self, nonce):
|
||||||
"""
|
"""
|
||||||
Returns standardized name for challenge certificate
|
Returns standardized name for challenge certificate
|
||||||
|
|
||||||
@@ -46,17 +53,6 @@ class SNI_Challenge(Challenge):
|
|||||||
|
|
||||||
return WORK_DIR + nonce + ".crt"
|
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):
|
def __getConfigText(self, nonce, ip_addrs, key):
|
||||||
"""
|
"""
|
||||||
Chocolate virtual server configuration text
|
Chocolate virtual server configuration text
|
||||||
@@ -68,14 +64,14 @@ class SNI_Challenge(Challenge):
|
|||||||
result: returns virtual host configuration text
|
result: returns virtual host configuration text
|
||||||
"""
|
"""
|
||||||
configText = "<VirtualHost " + " ".join(ip_addrs) + "> \n \
|
configText = "<VirtualHost " + " ".join(ip_addrs) + "> \n \
|
||||||
ServerName " + nonce + ".chocolate \n \
|
ServerName " + nonce + INVALID_EXT + " \n \
|
||||||
UseCanonicalName on \n \
|
UseCanonicalName on \n \
|
||||||
SSLStrictSNIVHostCheck on \n \
|
SSLStrictSNIVHostCheck on \n \
|
||||||
\n \
|
\n \
|
||||||
LimitRequestBody 1048576 \n \
|
LimitRequestBody 1048576 \n \
|
||||||
\n \
|
\n \
|
||||||
Include " + OPTIONS_SSL_CONF + " \n \
|
Include " + OPTIONS_SSL_CONF + " \n \
|
||||||
SSLCertificateFile " + self.getChocCertFile(nonce) + " \n \
|
SSLCertificateFile " + self.getDvsniCertFile(nonce) + " \n \
|
||||||
SSLCertificateKeyFile " + key + " \n \
|
SSLCertificateKeyFile " + key + " \n \
|
||||||
\n \
|
\n \
|
||||||
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
||||||
@@ -109,7 +105,7 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
|
|
||||||
def checkForApacheConfInclude(self, mainConfig):
|
def checkForApacheConfInclude(self, mainConfig):
|
||||||
"""
|
"""
|
||||||
Adds chocolate challenge include file if it does not already exist
|
Adds DVSNI challenge include file if it does not already exist
|
||||||
within mainConfig
|
within mainConfig
|
||||||
|
|
||||||
mainConfig: string - file path to main user apache config file
|
mainConfig: string - file path to main user apache config file
|
||||||
@@ -120,40 +116,59 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
#print "Including challenge virtual host(s)"
|
#print "Including challenge virtual host(s)"
|
||||||
self.configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
|
self.configurator.add_dir("/files" + mainConfig, "Include", APACHE_CHALLENGE_CONF)
|
||||||
|
|
||||||
def createChallengeCert(self, oid, ext, nonce, csr, key):
|
def createChallengeCert(self, name, ext, nonce, key):
|
||||||
"""
|
"""
|
||||||
Modifies challenge certificate configuration and calls openssl binary to create a certificate
|
Modifies challenge certificate configuration and calls openssl binary to create a certificate
|
||||||
|
|
||||||
oid: string
|
|
||||||
ext: string - hex z value
|
ext: string - hex z value
|
||||||
nonce: string - hex
|
nonce: string - hex
|
||||||
csr: string - file path to csr
|
|
||||||
key: string - file path to key
|
key: string - file path to key
|
||||||
|
|
||||||
result: certificate created at getChocCertFile(nonce)
|
result: certificate created at getChocCertFile(nonce)
|
||||||
"""
|
"""
|
||||||
|
self.createCHOC_CERT_CONF(name, ext)
|
||||||
|
|
||||||
self.updateCertConf(oid, ext)
|
self.configurator.register_file_creation(True, self.getDvsniCertFile(nonce))
|
||||||
self.configurator.register_file_creation(True, self.getChocCertFile(nonce))
|
cert_pem = trustify_util.make_ss_cert(key, [nonce + INVALID_EXT, name, ext])
|
||||||
subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", self.getChocCertFile(nonce), "-in", csr], stdout=open("/dev/null", 'w'), stderr=open("/dev/null", 'w'))
|
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 generateExtension(self, key, y):
|
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
|
Generates z to be placed in certificate extension
|
||||||
|
|
||||||
key: string - File path to key
|
r: byte array
|
||||||
y: byte array
|
s: byte array
|
||||||
|
|
||||||
result: returns z value
|
result: returns z + INVALID_EXT
|
||||||
"""
|
"""
|
||||||
|
h = hashlib.new('sha256')
|
||||||
rsaPrivKey = M2Crypto.RSA.load_key(key)
|
h.update(r)
|
||||||
r = rsaPrivKey.private_decrypt(y, M2Crypto.RSA.pkcs1_oaep_padding)
|
h.update(s)
|
||||||
|
|
||||||
s = Random.get_random_bytes(S_SIZE)
|
return h.hexdigest() + INVALID_EXT
|
||||||
extHMAC = hmac.new(r, str(s), hashlib.sha256)
|
|
||||||
return self.byteToHex(s) + extHMAC.hexdigest()
|
|
||||||
|
|
||||||
def byteToHex(self, byteStr):
|
def byteToHex(self, byteStr):
|
||||||
"""
|
"""
|
||||||
@@ -166,33 +181,6 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
|
|
||||||
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
|
return ''.join(["%02X" % ord(x) for x in byteStr]).strip()
|
||||||
|
|
||||||
#Searches for the first extension specified in binary
|
|
||||||
def updateCertConf(self, oid, value):
|
|
||||||
"""
|
|
||||||
Updates the sni_challenge openssl certificate config file
|
|
||||||
|
|
||||||
oid: string - ex. 1.3.3.7
|
|
||||||
value string hex - value of OID
|
|
||||||
|
|
||||||
result: updated certificate config file
|
|
||||||
"""
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""
|
"""
|
||||||
@@ -212,10 +200,8 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
"""
|
"""
|
||||||
Sets up and reloads Apache server to handle SNI challenges
|
Sets up and reloads Apache server to handle SNI challenges
|
||||||
|
|
||||||
listSNITuple: List of tuples with form (addr, y, nonce, ext_oid)
|
listSNITuple: List of tuples with form (addr, r, nonce)
|
||||||
addr (string), y (byte array), nonce (hex string),
|
addr (string), r (base64 string), nonce (hex string)
|
||||||
ext_oid (string)
|
|
||||||
csr: string - File path to chocolate csr
|
|
||||||
key: string - File path to key
|
key: string - File path to key
|
||||||
configurator: Configurator obj
|
configurator: Configurator obj
|
||||||
"""
|
"""
|
||||||
@@ -231,10 +217,10 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
print "No vhost exists with servername or alias of:", tup[0]
|
print "No vhost exists with servername or alias of:", tup[0]
|
||||||
print "No _default_:443 vhost exists"
|
print "No _default_:443 vhost exists"
|
||||||
print "Please specify servernames in the Apache config"
|
print "Please specify servernames in the Apache config"
|
||||||
return False
|
return None
|
||||||
|
|
||||||
if not self.configurator.make_server_sni_ready(vhost, default_addr):
|
if not self.configurator.make_server_sni_ready(vhost, default_addr):
|
||||||
return False
|
return None
|
||||||
|
|
||||||
for a in vhost.addrs:
|
for a in vhost.addrs:
|
||||||
if "_default_" in a:
|
if "_default_" in a:
|
||||||
@@ -243,56 +229,64 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
|||||||
else:
|
else:
|
||||||
addresses.append(vhost.addrs)
|
addresses.append(vhost.addrs)
|
||||||
|
|
||||||
|
# Generate S
|
||||||
|
s = Random.get_random_bytes(S_SIZE)
|
||||||
|
# Create all of the challenge certs
|
||||||
for tup in self.listSNITuple:
|
for tup in self.listSNITuple:
|
||||||
ext = self.generateExtension(self.key, tup[1])
|
# Need to decode from base64
|
||||||
self.createChallengeCert(tup[3], ext, tup[2], self.csr, self.key)
|
r = jose.b64decode_url(tup[1])
|
||||||
|
ext = self.generateExtension(r, s)
|
||||||
|
self.createChallengeCert(tup[0], ext, tup[2], self.key)
|
||||||
|
|
||||||
self.modifyApacheConfig(self.findApacheConfigFile(), addresses)
|
self.modifyApacheConfig(self.configurator.user_config_file, addresses)
|
||||||
# Save reversible changes and restart the server
|
# Save reversible changes and restart the server
|
||||||
self.configurator.save("SNI Challenge", True)
|
self.configurator.save("SNI Challenge", True)
|
||||||
self.configurator.restart(quiet)
|
self.configurator.restart(quiet)
|
||||||
return True
|
return jose.b64encode_url(s)
|
||||||
|
|
||||||
# This main function is just used for testing
|
# This main function is just used for testing
|
||||||
def main():
|
def main():
|
||||||
key = path.abspath("key.pem")
|
key = path.abspath("/home/ubuntu/key.pem")
|
||||||
csr = path.abspath("req.pem")
|
csr = path.abspath("/home/ubuntu/req.pem")
|
||||||
logger.setLogger(sys.stdout)
|
logger.setLogger(logger.FileLogger(sys.stdout))
|
||||||
logger.setLogLevel(logger.INFO)
|
logger.setLogLevel(logger.INFO)
|
||||||
|
|
||||||
testkey = M2Crypto.RSA.load_key(key)
|
testkey = M2Crypto.RSA.load_key(key)
|
||||||
|
|
||||||
r = Random.get_random_bytes(S_SIZE)
|
#r = Random.get_random_bytes(S_SIZE)
|
||||||
r = "testValueForR"
|
r = "testValueForR"
|
||||||
nonce = Random.get_random_bytes(NONCE_SIZE)
|
#nonce = Random.get_random_bytes(NONCE_SIZE)
|
||||||
nonce = "nonce"
|
nonce = "nonce"
|
||||||
r2 = "testValueForR2"
|
r2 = "testValueForR2"
|
||||||
nonce2 = "nonce2"
|
nonce2 = "nonce2"
|
||||||
|
|
||||||
|
r = jose.b64encode_url(r)
|
||||||
|
r2 = jose.b64encode_url(r2)
|
||||||
|
|
||||||
#ans = dns.resolver.query("google.com")
|
#ans = dns.resolver.query("google.com")
|
||||||
#print ans.rrset
|
#print ans.rrset
|
||||||
#return
|
#return
|
||||||
#the second parameter is ignored
|
#the second parameter is ignored
|
||||||
#https://www.dlitz.net/software/pycrypto/api/current/
|
#https://www.dlitz.net/software/pycrypto/api/current/
|
||||||
y = testkey.public_encrypt(r, M2Crypto.RSA.pkcs1_oaep_padding)
|
#y = testkey.public_encrypt(r, M2Crypto.RSA.pkcs1_oaep_padding)
|
||||||
y2 = testkey.public_encrypt(r2, M2Crypto.RSA.pkcs1_oaep_padding)
|
#y2 = testkey.public_encrypt(r2, M2Crypto.RSA.pkcs1_oaep_padding)
|
||||||
|
|
||||||
nonce = binascii.hexlify(nonce)
|
nonce = binascii.hexlify(nonce)
|
||||||
nonce2 = binascii.hexlify(nonce2)
|
nonce2 = binascii.hexlify(nonce2)
|
||||||
|
|
||||||
config = configurator.Configurator()
|
config = configurator.Configurator()
|
||||||
|
|
||||||
challenges = [("example.com", y, nonce, "1.3.3.7"), ("www.example.com",y2, nonce2, "1.3.3.7")]
|
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")]
|
#challenges = [("127.0.0.1", y, nonce, "1.3.3.7"), ("localhost", y2, nonce2, "1.3.3.7")]
|
||||||
sni_chall = SNI_Challenge(challenges, csr, key, config)
|
sni_chall = SNI_Challenge(challenges, key, config)
|
||||||
if sni_chall.perform():
|
if sni_chall.perform():
|
||||||
# Waste some time without importing time module... just for testing
|
# Waste some time without importing time module... just for testing
|
||||||
for i in range(0, 12000):
|
for i in range(0, 12000):
|
||||||
if i % 2000 == 0:
|
if i % 2000 == 0:
|
||||||
print "Waiting:", i
|
print "Waiting:", i
|
||||||
|
|
||||||
print "Cleaning up"
|
#print "Cleaning up"
|
||||||
sni_chall.cleanup()
|
#sni_chall.cleanup()
|
||||||
else:
|
else:
|
||||||
print "Failed SNI challenge..."
|
print "Failed SNI challenge..."
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,76 @@
|
|||||||
import errno
|
import errno
|
||||||
import stat
|
import stat
|
||||||
import os, pwd, grp
|
import os, pwd, grp
|
||||||
|
import M2Crypto
|
||||||
|
import time
|
||||||
|
from M2Crypto import EVP, X509, RSA, ASN1
|
||||||
from trustify.client import logger
|
from trustify.client import logger
|
||||||
|
#import logger
|
||||||
|
|
||||||
|
|
||||||
|
def make_csr(key_file, domains):
|
||||||
|
"""
|
||||||
|
Returns new CSR in PEM form using key_file containing all domains
|
||||||
|
"""
|
||||||
|
assert domains, "Must provide one or more hostnames for the CSR."
|
||||||
|
rsa_key = M2Crypto.RSA.load_key(key_file)
|
||||||
|
pk = EVP.PKey()
|
||||||
|
pk.assign_rsa(rsa_key)
|
||||||
|
|
||||||
|
x = X509.Request()
|
||||||
|
x.set_pubkey(pk)
|
||||||
|
name = x.get_subject()
|
||||||
|
name.CN = domains[0]
|
||||||
|
extstack = X509.X509_Extension_Stack()
|
||||||
|
for d in domains:
|
||||||
|
ext = X509.new_extension('subjectAltName', 'DNS:%s' % d)
|
||||||
|
extstack.push(ext)
|
||||||
|
x.add_extensions(extstack)
|
||||||
|
x.sign(pk,'sha256')
|
||||||
|
assert x.verify(pk)
|
||||||
|
pk2 = x.get_pubkey()
|
||||||
|
assert x.verify(pk2)
|
||||||
|
return x.as_pem()
|
||||||
|
|
||||||
|
def make_ss_cert(key_file, domains):
|
||||||
|
"""
|
||||||
|
Returns new self-signed cert in PEM form using key_file containing all domains
|
||||||
|
"""
|
||||||
|
assert domains, "Must provide one or more hostnames for the CSR."
|
||||||
|
rsa_key = M2Crypto.RSA.load_key(key_file)
|
||||||
|
pk = EVP.PKey()
|
||||||
|
pk.assign_rsa(rsa_key)
|
||||||
|
|
||||||
|
x = X509.X509()
|
||||||
|
x.set_pubkey(pk)
|
||||||
|
x.set_serial_number(1337)
|
||||||
|
x.set_version(2)
|
||||||
|
|
||||||
|
t = long(time.time())
|
||||||
|
current = ASN1.ASN1_UTCTIME()
|
||||||
|
current.set_time(t)
|
||||||
|
expire = ASN1.ASN1_UTCTIME()
|
||||||
|
expire.set_time((7 * 24 * 60 * 60) + t)
|
||||||
|
x.set_not_before(current)
|
||||||
|
x.set_not_after(expire)
|
||||||
|
|
||||||
|
name = x.get_subject()
|
||||||
|
name.C = "US"
|
||||||
|
name.ST = "Michigan"
|
||||||
|
name.L = "Ann Arbor"
|
||||||
|
name.O = "University of Michigan"
|
||||||
|
name.OU = "Halderman's Research Group"
|
||||||
|
name.CN = domains[0]
|
||||||
|
x.set_issuer(x.get_subject())
|
||||||
|
|
||||||
|
x.add_ext(X509.new_extension('subjectAltName', ",".join(["DNS:%s" % d for d in domains])))
|
||||||
|
|
||||||
|
x.sign(pk, 'sha1')
|
||||||
|
assert x.verify(pk)
|
||||||
|
pk2 = x.get_pubkey()
|
||||||
|
assert x.verify(pk2)
|
||||||
|
return x.as_pem()
|
||||||
|
|
||||||
def make_or_verify_dir(directory, permissions=0755, uid=0):
|
def make_or_verify_dir(directory, permissions=0755, uid=0):
|
||||||
try:
|
try:
|
||||||
os.makedirs(directory, permissions)
|
os.makedirs(directory, permissions)
|
||||||
|
|||||||
Reference in New Issue
Block a user