mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Move SNIChallenge into apache_configurator.py as the dvsni challenge should be a feature for the configurator
This commit is contained in:
parent
3b26f6c526
commit
d57dd9faee
3 changed files with 175 additions and 289 deletions
|
|
@ -1052,8 +1052,181 @@ LogLevel warn \n\
|
|||
|
||||
return True
|
||||
|
||||
###########################################################################
|
||||
# Challenges Section
|
||||
###########################################################################
|
||||
|
||||
def perform(self, chall_type, tup):
|
||||
if chall_type == 'dvsni':
|
||||
return dvsni_perform(tup)
|
||||
return None
|
||||
|
||||
def dvsni_perform(self, tup):
|
||||
"""
|
||||
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.save()
|
||||
|
||||
if len(tup) != 2:
|
||||
logger.fatal("Incorrect parameter given to Apache DVSNI challenge")
|
||||
sys.exit(1)
|
||||
|
||||
listSNITuple = tup[0]
|
||||
dvsni_key = tup[1]
|
||||
|
||||
addresses = []
|
||||
default_addr = "*:443"
|
||||
for tup in listSNITuple:
|
||||
vhost = self.choose_virtual_host(tup[0])
|
||||
if vhost is None:
|
||||
logger.error("No vhost exists with servername or alias of:%s" % tup[0])
|
||||
logger.error("No _default_:443 vhost exists")
|
||||
logger.error("Please specify servernames in the Apache config")
|
||||
return None
|
||||
|
||||
# TODO - @jdkasten review this code to make sure it makes sense
|
||||
if not self.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 t in listSNITuple:
|
||||
# Need to decode from base64
|
||||
r = le_util.b64_url_dec(t[1])
|
||||
ext = self.generateExtension(r, s)
|
||||
self.createChallengeCert(t[0], ext, t[2], dvsni_key)
|
||||
|
||||
self.dvsni_mod_config(self.user_config_file, listSNITuple, addresses)
|
||||
# Save reversible changes and restart the server
|
||||
self.save("SNI Challenge", True)
|
||||
self.restart(quiet)
|
||||
|
||||
s = le_util.b64_url_enc(s)
|
||||
return {"type":"dvsni", "s":s}
|
||||
|
||||
def cleanup(self):
|
||||
self.revert_challenge_config()
|
||||
self.restart(True)
|
||||
|
||||
|
||||
def dvsni_get_cert_file(self, nonce):
|
||||
"""
|
||||
Returns standardized name for challenge certificate
|
||||
nonce: string - hex
|
||||
result: returns certificate file name
|
||||
"""
|
||||
return WORK_DIR + nonce + ".crt"
|
||||
|
||||
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.dvsni_get_cert_file(nonce) + " \n \
|
||||
SSLCertificateKeyFile " + key + " \n \
|
||||
\n \
|
||||
DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \
|
||||
</VirtualHost> \n\n "
|
||||
|
||||
return configText
|
||||
|
||||
def dvsni_mod_config(self, mainConfig, listSNITuple, 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(listSNITuple[idx][2], lis, self.key)
|
||||
configText += "</IfModule> \n"
|
||||
|
||||
self.dvsni_conf_include_check(mainConfig)
|
||||
self.register_file_creation(True, APACHE_CHALLENGE_CONF)
|
||||
newConf = open(APACHE_CHALLENGE_CONF, 'w')
|
||||
newConf.write(configText)
|
||||
newConf.close()
|
||||
|
||||
|
||||
|
||||
def dvsni_conf_include_check(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.find_directive(self.case_i("Include"), APACHE_CHALLENGE_CONF)) == 0:
|
||||
#print "Including challenge virtual host(s)"
|
||||
self.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 dvsni_get_cert_file(nonce)
|
||||
"""
|
||||
|
||||
self.register_file_creation(True, self.dvsni_get_cert_file(nonce))
|
||||
cert_pem = crypto_util.make_ss_cert(key, [nonce + INVALID_EXT, name, ext])
|
||||
with open(self.dvsni_get_cert_file(nonce), 'w') as f:
|
||||
f.write(cert_pem)
|
||||
|
||||
def dvsni_gen_ext(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 main():
|
||||
config = Configurator()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import abc
|
||||
from abc_base import Configurator
|
||||
|
||||
import augeas
|
||||
|
||||
from letsencrypt.client.CONFIG import TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR
|
||||
|
||||
class AugeasConfigurator(Configurator):
|
||||
|
|
|
|||
|
|
@ -1,289 +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 apache_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.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)
|
||||
|
||||
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()
|
||||
Loading…
Reference in a new issue