diff --git a/trustify.py b/trustify.py index d76aac87b..d5afa91cd 100644 --- a/trustify.py +++ b/trustify.py @@ -56,11 +56,16 @@ def main(): continue if not server: - if "CHOCOLATESERVER" in os.environ: - server = os.environ["CHOCOLATESERVER"] + print os.environ + if "ACMESERVER" in os.environ: + server = os.environ["ACMESERVER"] else: - server = "ca.theobroma.info" - + from trustify.client import logger + logger.setLogger(logger.FileLogger(sys.stdout)) + logger.setLogLevel(logger.INFO) + logger.warn("No ACME server specified. Please specify the ACMESERVER enviornment variable or the --server option") + server = "54.183.196.250" + c = client.Client(server, args, csr, privkey, curses) c.authenticate() diff --git a/trustify/client/CONFIG.py b/trustify/client/CONFIG.py index 6e25f61eb..992f9fbf2 100644 --- a/trustify/client/CONFIG.py +++ b/trustify/client/CONFIG.py @@ -24,7 +24,7 @@ APACHE_CHALLENGE_CONF = CONFIG_DIR + "choc_sni_cert_challenge.conf" # Byte size of S and Nonce S_SIZE = 32 -NONCE_SIZE = 32 +NONCE_SIZE = 16 # bits of hashcash to generate difficulty = 23 @@ -38,3 +38,9 @@ INVALID_EXT = ".acme.invalid" # Rewrite rule arguments used for redirections to https vhost REWRITE_HTTPS_ARGS = ["^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"] + +# Challenge Preferences Dict for currently supported challenges +CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"] + +# Mutually Exclusive Challenges - only solve 1 +EXCLUSIVE_CHALLENGES = [set(["dvsni", "simpleHttps"])] diff --git a/trustify/client/ca_offerings b/trustify/client/ca_offerings new file mode 100644 index 000000000..85735c235 --- /dev/null +++ b/trustify/client/ca_offerings @@ -0,0 +1,3 @@ +EFF (DV);EFF Example CA, Domain Validated - FREE +UMich (DV);Michigan Example CA, Domain Validated - FREE +Let's Encrypt (DV);Let's Encrypt ISRG CA, Domain Validated - FREE diff --git a/trustify/client/client.py b/trustify/client/client.py index 0f57a46d9..60d240740 100755 --- a/trustify/client/client.py +++ b/trustify/client/client.py @@ -7,22 +7,25 @@ import M2Crypto # It is OK to use the upstream M2Crypto here instead of our modified # version. -import urllib2 +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 -import hashlib +import hashlib, binascii, jose 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 -from trustify.protocol.chocolate_pb2 import chocolatemessage from trustify.client.sni_challenge import SNI_Challenge from trustify.client.payment_challenge import Payment_Challenge from trustify.client import configurator from trustify.client import logger from trustify.client import trustify_util -from trustify.client.CONFIG import difficulty, cert_file, chain_file +from trustify.client.CONFIG import NONCE_SIZE, cert_file, chain_file from trustify.client.CONFIG import SERVER_ROOT, KEY_DIR, CERT_DIR - +from trustify.client.CONFIG import CHALLENGE_PREFERENCES, EXCLUSIVE_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 @@ -32,7 +35,7 @@ RSA_KEY_SIZE = 2048 class Client(object): # In case of import, dialog needs scope over the class dialog = None - + def __init__(self, ca_server, domains=[], cert_signing_request=None, private_key=None, use_curses=True): global dialog @@ -63,7 +66,7 @@ class Client(object): self.sanity_check_names([ca_server] + domains) - self.upstream = "https://%s/chocolate.py" % self.server + self.server_url = "https://%s/acme/" % self.server @@ -83,34 +86,232 @@ class Client(object): if not self.config.check_ssl_loaded(): logger.info("Loading mod_ssl into Apache Server") self.config.enable_mod("ssl") - + key_pem, csr_pem = self.get_key_csr_pem() - r, k = self.send_request(key_pem, csr_pem, self.names) + #r, k = self.send_request(key_pem, csr_pem, self.names) + challenge_dict = self.send(self.challenge_request(self.names)) + + challenge_dict = self.is_expected_msg(challenge_dict, "challenge") - challenges = self.challenge_factory(r) + if not challenge_dict: + logger.fatal("Unable to retreive Challenge message from server") + sys.exit(1) - # Find set of virtual hosts to deploy certificates to - vhost = self.get_virtual_hosts(self.names) + print challenge_dict + #assert self.is_challenge(challenge_dict) - # Perform all "client knows first" challenges - for challenge in challenges: - if not challenge.perform(quiet=self.curses): - # TODO: In this case the client should probably send a failure - # to the server. - logger.fatal("challenge failed") - sys.exit(1) - logger.info("Configured Apache for challenges; waiting for verification...") + #Perform Challenges - r = self.notify_server_of_completion(r, k) - r = self.check_payment(r, k) + responses, challenge_objs = self.verify_identity(challenge_dict) - self.handle_verification_response(r, challenges, vhost) + authorization_dict = self.send(self.authorization_request(challenge_dict["sessionID"], self.names[0], challenge_dict["nonce"], responses)) + + authorization_dict = self.is_expected_msg(authorization_dict, "authorization") + if not authorization_dict: + self.cleanup_challenges(challenge_objs) + logger.fatal("Failed Authorization procedure - cleaning up challenges") + sys.exit(1) + + certificate_dict = self.send(self.certificate_request(self.names, self.key_file, self.key_file)) + + certifcate_dict = self.is_expected_msg(certificate_dict, "certificate") + + sys.exit() + # Install Certificate + + # Perform optimal config changes + + + # challenges = self.challenge_factory(r) + + # # Find set of virtual hosts to deploy certificates to + # vhost = self.get_virtual_hosts(self.names) + + # # Perform all "client knows first" challenges + # for challenge in challenges: + # if not challenge.perform(quiet=self.curses): + # # TODO: In this case the client should probably send a failure + # # to the server. + # logger.fatal("challenge failed") + # sys.exit(1) + # logger.info("Configured Apache for challenges; waiting for verification...") + + # r = self.notify_server_of_completion(r, k) + # r = self.check_payment(r, k) + + # self.handle_verification_response(r, challenges, vhost) self.config.save("Completed Augeas Authentication") return + def certificate_request(self, names, auth_key, cert_key): + logger.info("Preparing and sending CSR for %s" % names[0]) + + logger.info(csr_pem) + + def cleanup_challenges(self, challenge_objs): + for c in challenge_objs: + c.cleanup() + + def is_expected_msg(self, msg_dict, expected, delay=5, rounds = 20): + for i in range(rounds): + if msg_dict["type"] == expected: + return msg_dict + elif msg_dict["type"] == "error": + logger.error("%s: %s - More Info: %s" % (msg_dict["error"], msg_dict.get("message", ""), msg_dict.get("moreInfo", ""))) + return None + elif msg_dict["type"] == "defer": + logger.info("Waiting for %d seconds..." % delay) + time.sleep(delay) + msg_dict = self.send(self.status_request(msg_dict["token"])) + + logger.error("Server has deferred past the max of %d seconds" % (rounds * delay)) + return None + + + def authorization_request(self, id, name, server_nonce, responses): + auth_req = {"type":"authorizationRequest", "sessionID":id, "nonce":server_nonce} + auth_req["signature"] = self.create_authorization_sig(name + jose.b64decode_url(server_nonce)) + auth_req["responses"] = responses + print auth_req + return auth_req + + def status_request(self, token): + return {"type":"statusRequest", "token":token} + + def __leading_zeros(self, s): + if len(s) % 2: + return "0" + s + return s + + def create_authorization_sig(self, msg, signer_nonce = None, signer_nonce_len = NONCE_SIZE): + # DOES prepend signer_nonce to message + # TODO: Change this over to M2Crypto... PKey + # Protect against crypto unicode errors... is this sufficient? Do I need to escape? + msg = str(msg) + key = RSA.importKey(open(self.key_file).read()) + if signer_nonce is None: + signer_nonce = get_random_bytes(signer_nonce_len) + h = SHA256.new(signer_nonce + msg) + signer = PKCS1_v1_5.new(key) + signature = signer.sign(h) + print "signing:", signer_nonce + msg + print "signature:", signature + n, e = key.n, key.e + n_bytes = binascii.unhexlify(self.__leading_zeros(hex(n)[2:].replace("L", ""))) + e_bytes = binascii.unhexlify(self.__leading_zeros(hex(e)[2:].replace("L", ""))) + n_encoded = jose.b64encode_url(n_bytes) + e_encoded = jose.b64encode_url(e_bytes) + signer_nonce_encoded = jose.b64encode_url(signer_nonce) + sig_encoded = jose.b64encode_url(signature) + jwk = { "kty": "RSA", "n": n_encoded, "e": e_encoded } + signature = { "nonce": signer_nonce_encoded, "alg": "RS256", "jwk": jwk, "sig": sig_encoded } + # return json.dumps(signature) + return (signature) + + def challenge_request(self, names): + logger.info("Temporarily only enabling one name") + return {"type":"challengeRequest", "identifier": names[0]} + + def verify_identity(self, c): + path = self.gen_challenge_path(c["challenges"], c.get("combinations", None)) + logger.info("Peforming the following challenges:") + print c["challenges"] + print c + for chall in path: + logger.info(c["challenges"][chall]) + + # Every indicies element is a list of integers referring to which challenges in the master list + # the challenge object satisfies + # Single Challenge objects that can satisfy multiple server challenges + # mess up the order of the challenges, thus requiring the indicies + challenge_objs, indicies = self.challenge_factory(self.names[0], c["challenges"], path) + + + responses = [None] * len(c["challenges"]) + + # Perform challenges and populate responses + for i, c_obj in enumerate(challenge_objs): + if not c_obj.perform(): + logger.fatal("Challenge Failed") + sys.exit(1) + for index in indicies[i]: + responses[index] = c_obj.generate_response() + + logger.info("Configured Apache for challenges; waiting for verification...") + + return responses, challenge_objs + + def gen_challenge_path(self, challenges, combos): + if combos: + return self.__find_smart_path(challenges, combos) + + return self.__find_dumb_path(challenges) + + def __find_smart_path(self, challenges, combos): + """ + Can be called if combinations is included + Function uses a simple ranking system to choose the combo with the lowest cost + """ + chall_cost = {} + max_cost = 0 + for i, chall in enumerate(CHALLENGE_PREFERENCES): + chall_cost[chall] = i + max_cost += i + + best_combo = [] + # Set above completing all of the available challenges + best_combo_cost = max_cost + 1 + + combo_total = 0 + for combo in combos: + for c in combo: + combo_total += chall_cost.get(challenges[c]["type"], max_cost) + if combo_total < best_combo_total: + best_combo = combo + combo_total = 0 + + if not best_combo: + logger.fatal("Client does not support any combination of challenges to satisfy ACME server") + sys.exit(22) + + return best_combo + + def __find_dumb_path(self, challenges): + """ + Should be called if the combinations hint is not included by the server + This function returns the best path that does not contain multiple mutually exclusive + challenges + """ + # Add logic for a crappy server + # Choose a DV + path = [] + for pref_c in CHALLENGE_PREFERENCES: + for i, offered_c in enumerate(challenges): + if pref_c == offered_c["type"] and self.is_preferred(offered_c["type"], path): + path.append((i, offered_c["type"])) + + return [tup[0] for tup in path] + + + def is_preferred(self, offered_c_type, path): + for tup in path: + for s in EXCLUSIVE_CHALLENGES: + # Second part is in case we eventually allow multiple names to be challenged + # at the same time + if (tup[1] in s and offered_c_type in s) and tup[1] != offered_c_type: + return False + + return True + + def send(self, json_obj): + #validate(json.dumps(json_obj)) + response = urllib2.urlopen(self.server_url, json.dumps(json_obj)).read() + #validate(response) + return json.loads(response) + def handle_verification_response(self, r, challenges, vhost): if r.success.IsInitialized(): # Allow Challenges to cleanup @@ -238,32 +439,38 @@ class Client(object): vhost.add(host) return vhost - def challenge_factory(self, r): + def challenge_factory(self, name, challenges, path): sni_todo = [] - challenges = [] - logger.info("Received %s challenges from server." % len(r.challenge)) - for chall in r.challenge: - logger.debug(chall) - if chall.type == r.DomainValidateSNI: - logger.info("\tDomainValidateSNI challenge for name %s." % chall.name) - dvsni_nonce, dvsni_y, dvsni_ext = chall.data - sni_todo.append( (chall.name, dvsni_y, dvsni_nonce, dvsni_ext) ) - - if chall.type == r.Payment: - url = chall.data[0] - challenges.append(Payment_Challenge(url, "Alexa Top 10k Domain")) + # Since a single invocation of SNI challenge can satsify multiple challenges + # We must keep track of all the challenges it satisfies + sni_satisfies = [] - #if chall.type == r.Interactive: - # message = chall.data - # challenges.append(Interactive_Challenge(message) + challenge_objs = [] + challenge_obj_indicies = [] + for c in path: + if challenges[c]["type"] == "dvsni": + logger.info("\tDomainValidateSNI challenge for name %s." % name) + sni_satisfies.append(c) + sni_todo.append( (str(name), str(challenges[c]["r"]), str(challenges[c]["nonce"])) ) + + elif challenges[c]["type"] == "recoveryToken": + logger.fatal("RecoveryToken Challenge type not currently supported") + sys.exit(82) + + else: + logger.fatal("Challenge not currently supported") + sys.exit(82) if sni_todo: # SNI_Challenge can satisfy many sni challenges at once so only # one "challenge object" is issued for all sni_challenges - challenges.append(SNI_Challenge(sni_todo, os.path.abspath(self.csr_file), os.path.abspath(self.key_file), self.config)) + challenge_objs.append(SNI_Challenge(sni_todo, os.path.abspath(self.key_file), self.config)) + challenge_obj_indicies.append(sni_satisfies) logger.debug(sni_todo) - return challenges + return challenge_objs, challenge_obj_indicies + + def send_request(self, key_pem, csr_pem, names): k = chocolatemessage() @@ -306,7 +513,7 @@ class Client(object): def get_key_csr_pem(self): """ - Returns key and CSR in pem form, using provided files or generating a new files if + Returns key and CSR in pem form, using provided files or generating new files if necessary """ key_pem = None @@ -505,87 +712,6 @@ def sha256(m): return hashlib.sha256(m).hexdigest() -# based on M2Crypto unit test written by Toby Allsopp -#from M2Crypto import EVP, X509, RSA - -# def make_key_and_csr(names, bits=2048): -# """Return a tuple (key, csr) containing a PEM-formatted private key -# of the specified number of bits and a CSR requesting a certificate for -# the specified DNS names.""" -# assert names, "Must provide one or more hostnames." -# pk = EVP.PKey() -# x = X509.Request() -# rsa = RSA.gen_key(bits, 65537) -# pk.assign_rsa(rsa) -# key_pem = rsa.as_pem(cipher=None) -# rsa = None # should not be freed here -# x.set_pubkey(pk) -# name = x.get_subject() -# name.CN = names[0] -# extstack = X509.X509_Extension_Stack() -# for n in names: -# ext = X509.new_extension('subjectAltName', 'DNS:%s' % n) -# extstack.push(ext) -# x.add_extensions(extstack) -# x.sign(pk,'sha1') -# assert x.verify(pk) -# pk2 = x.get_pubkey() -# assert x.verify(pk2) -# return key_pem, x.as_pem() - -# def by_default(): -# d = dialog.Dialog() -# choices = [("Easy", "Allow both HTTP and HTTPS access to these sites"), ("Secure", "Make all requests redirect to secure HTTPS access")] -# result = d.menu("Please choose whether HTTPS access is required or optional.", width=70, choices=choices) -# if result[0] != 0: -# sys.exit(1) -# return result[1] == "Secure" - - -# def rsa_sign(key, data): -# """ -# Sign this data with this private key. For client-side use. - -# @type key: str -# @param key: PEM-encoded string of the private key. - -# @type data: str -# @param data: The data to be signed. Will be hashed (sha256) prior to -# signing. - -# @return: binary string of the signature -# """ -# key = str(key) -# data = str(data) -# privkey = M2Crypto.RSA.load_key_string(key) -# return privkey.sign(hashlib.sha256(data).digest(), 'sha256') - -# def do(upstream, m): -# u = urllib2.urlopen(upstream, m.SerializeToString()) -# return u.read() - -# def decode(m): -# return (chocolatemessage.FromString(m)) - -# def init(m): -# m.chocolateversion = 1 -# m.session = "" - -# def make_request(server, m, csr, names, quiet=False): -# m.request.recipient = server -# m.request.timestamp = int(time.time()) -# m.request.csr = csr -# hashcash_cmd = ["hashcash", "-P", "-m", "-z", "12", "-b", `difficulty`, "-r", server] -# if quiet: -# hashcash = subprocess.Popen(hashcash_cmd, preexec_fn=drop_privs, shell= False, stdout=subprocess.PIPE, stderr=open("/dev/null", "w")).communicate()[0].rstrip() -# else: -# hashcash = subprocess.Popen(hashcash_cmd, preexec_fn=drop_privs, shell= False, stdout=subprocess.PIPE).communicate()[0].rstrip() - -# if hashcash: m.request.clientpuzzle = hashcash - -# def sign(key, m): -# m.request.sig = rsa_sign(key, ("(%d) (%s) (%s)" % (m.request.timestamp, m.request.recipient, m.request.csr))) - def old_cert(cert_filename, days_left): cert = M2Crypto.X509.load_cert(cert_filename) exp_time = cert.get_not_before().get_datetime() @@ -601,33 +727,6 @@ def old_cert(cert_filename, days_left): return True return False -# def save_key_csr(key, csr): -# """ -# This function saves the newly generated key and csr to new files -# in the ssl and certs directories respectively -# This function sets the appropriate permissions for the key and its -# directory. -# """ -# # Create directories if they do not exist -# # This should probably go in the installation script -# # Make sure directories exist & make sure directories are set with the -# # correct permissions if they do exist. -# # Note: Appears I forgot to check existing directories permissions -# if not os.path.isdir(CERT_DIR): -# os.makedirs(CERT_DIR, 0755) -# if not os.path.isdir(KEY_DIR): -# os.makedirs(KEY_DIR, 0700) - -# # Write key to new file and change permissions -# key_f, key_fn = unique_file(KEY_DIR + "key-trustify.pem", 0600) -# key_f.write(key) -# key_f.close() -# # Write CSR to new file -# csr_f, csr_fn = unique_file(CERT_DIR + "csr-trustify.pem", 0644) -# csr_f.write(csr) -# csr_f.close() - -# return key_fn, csr_fn def recognized_ca(issuer): pass @@ -635,145 +734,6 @@ def recognized_ca(issuer): def gen_req_from_cert(): return -# def gen_https_names(domains): -# """ -# Returns a string of the domains formatted nicely with https:// prepended -# to each -# """ -# result = "" -# if len(domains) > 2: -# for i in range(len(domains)-1): -# result = result + "https://" + domains[i] + ", " -# result = result + "and " -# if len(domains) == 2: -# return "https://" + domains[0] + " and https://" + domains[1] - -# if domains: -# result = result + "https://" + domains[len(domains)-1] -# return result - -# def challenge_factory(r, req_filepath, key_filepath, config): -# sni_todo = [] -# dn = [] -# challenges = [] -# logger.info("Received %s challenges from server." % len(r.challenge)) -# for chall in r.challenge: -# logger.debug(chall) -# if chall.type == r.DomainValidateSNI: -# logger.info("\tDomainValidateSNI challenge for name %s." % chall.name) -# dvsni_nonce, dvsni_y, dvsni_ext = chall.data -# sni_todo.append( (chall.name, dvsni_y, dvsni_nonce, dvsni_ext) ) - -# # TODO: This domain name list is inelegant and the info should be -# # gathered from the challenge list itself -# dn.append(chall.name) - -# if chall.type == r.Payment: -# url = chall.data[0] -# challenges.append(Payment_Challenge(url, "Alexa Top 10k Domain")) - -# #if chall.type == r.Interactive: -# # message = chall.data -# # challenges.append(Interactive_Challenge(message) - -# if sni_todo: -# # SNI_Challenge can satisfy many sni challenges at once so only -# # one "challenge object" is issued for all sni_challenges -# challenges.append(SNI_Challenge(sni_todo, req_filepath, key_filepath, config)) -# logger.debug(sni_todo) - -# return challenges, dn - - -# def send_request(key_pem, csr_pem, names, quiet=curses): -# ''' -# Sends the request to the CA and returns a response -# ''' -# global server -# upstream = "https://%s/chocolate.py" % server -# k=chocolatemessage() -# m=chocolatemessage() -# init(k) -# init(m) -# logger.info("Creating request; generating hashcash...") -# make_request(server, m, csr_pem, names, quiet=curses) -# sign(key_pem, m) -# logger.info("Created request; sending to server...") -# logger.debug(m) - -# r=decode(do(upstream, m)) -# logger.debug(r) -# while r.proceed.IsInitialized(): -# if r.proceed.polldelay > 60: r.proceed.polldelay = 60 -# logger.info("Waiting %d seconds..." % r.proceed.polldelay) -# time.sleep(r.proceed.polldelay) -# k.session = r.session -# r = decode(do(upstream, k)) -# logger.debug(r) - -# if r.failure.IsInitialized(): -# logger.fatal("Chocolate Server reported failure.") -# sys.exit(1) - -# return r, k - - -# def handle_verification_response(r, dn, challenges, vhost, key_file, config): -# if r.success.IsInitialized(): -# for chall in challenges: -# chall.cleanup() -# cert_chain_abspath = None -# cert_fd, cert_fn = unique_file(cert_file, 644) -# cert_fd.write(r.success.certificate) -# cert_fd.close() -# logger.info("Server issued certificate; certificate written to %s" % cert_fn) -# if r.success.chain: -# chain_fd, chain_fn = unique_file(chain_file, 644) -# chain_fd.write(r.success.chain) -# chain_fd.close() - -# logger.info("Cert chain written to %s" % chain_fn) - -# # This expects a valid chain file -# cert_chain_abspath = os.path.abspath(chain_fn) - -# for host in vhost: -# config.deploy_cert(host, os.path.abspath(cert_fn), os.path.abspath(key_file), cert_chain_abspath) -# # Enable any vhost that was issued to, but not enabled -# if not host.enabled: -# logger.info("Enabling Site " + host.file) -# config.enable_site(host) - -# # sites may have been enabled / final cleanup -# config.restart(quiet=curses) - -# if curses: -# dialog.Dialog().msgbox("\nCongratulations! You have successfully enabled " + gen_https_names(dn) + "!", width=70) -# config.enable_mod("rewrite") -# if by_default(): -# redirect_to_ssl(vhost, config) -# config.restart(quiet=curses) -# else: -# logger.info("Congratulations! You have successfully enabled " + gen_https_names(dn) + "!") - -# elif r.failure.IsInitialized(): -# logger.fatal("Server reported failure.") -# sys.exit(1) - -# else: -# logger.fatal("Unexpected server verification response!") -# sys.exit(43) - - -# def redirect_to_ssl(vhost, config): -# for ssl_vh in vhost: -# success, redirect_vhost = config.redirect_all_ssl(ssl_vh) -# logger.info("\nRedirect vhost: " + redirect_vhost.file + " - " + str(success)) -# # If successful, make sure redirect site is enabled -# if success: -# if not config.is_site_enabled(redirect_vhost.file): -# config.enable_site(redirect_vhost) -# logger.info("Enabling available site: " + redirect_vhost.file) def renew(config): cert_key_pairs = config.get_all_certs_keys() @@ -786,163 +746,6 @@ def renew(config): # Wait for response, act accordingly gen_req_from_cert() -# def all_payment_challenge(r): -# if not r.challenge: -# return False -# for chall in r.challenge: -# if chall.type != r.Payment: -# return False - -# return True - -# def authenticate(): -# """ -# Main call to do DV_SNI validation and deploy the trustify certificate -# TODO: This should be turned into a class... -# """ -# global server, names, csr, privkey - -# # Check if root -# if not os.geteuid()==0: -# sys.exit("\nOnly root can run trustify\n") - -# if "CHOCOLATESERVER" in os.environ: -# server = os.environ["CHOCOLATESERVER"] -# if not server: -# # Global default value for Chocolate server! -# server = "ca.theobroma.info" - -# assert is_hostname_sane(server), `server` + " is an impossible hostname" - -# upstream = "https://%s/chocolate.py" % server - - -# if curses: -# logger.setLogger(logger.NcursesLogger()) -# logger.setLogLevel(logger.INFO) -# else: -# logger.setLogger(sys.stdout) -# logger.setLogLevel(logger.INFO) - -# # Logger should be init before config -# config = configurator.Configurator() - -# if not names: -# names = config.get_all_names() - -# if curses: -# if not names: -# logger.fatal("No domain names were found in your apache config") -# logger.fatal("Either specify which names you would like trustify to validate or add server names to your virtual hosts") -# sys.exit(1) - -# names = filter_names(names) -# choice = choice_of_ca() -# if choice[0] != 0: -# sys.exit(1) - - -# # Check first if mod_ssl is loaded -# if not config.check_ssl_loaded(): -# logger.info("Loading mod_ssl into Apache Server") -# config.enable_mod("ssl") - -# req_file = csr -# key_file = privkey -# if csr and privkey: -# csr_pem = open(req_file).read().replace("\r", "") -# key_pem = open(key_file).read().replace("\r", "") -# if not csr or not privkey: -# # Generate new private key and corresponding csr! -# key_pem, csr_pem = make_key_and_csr(names, 2048) -# key_file, req_file = save_key_csr(key_pem, csr_pem) -# logger.info("Generating key: " + key_file) -# logger.info("Creating CSR: " + req_file) - -# r, k = send_request(key_pem, csr_pem, names) - - -# challenges, dn = challenge_factory(r, os.path.abspath(req_file), os.path.abspath(key_file), config) - -# # Find set of virtual hosts to deploy certificates to -# vhost = set() -# for name in dn: -# host = config.choose_virtual_host(name) -# if host is not None: -# vhost.add(host) - -# for challenge in challenges: -# if not challenge.perform(quiet=curses): -# # TODO: In this case the client should probably send a failure -# # to the server. -# logger.fatal("challenge failed") -# sys.exit(1) -# logger.info("Configured Apache for challenge; waiting for verification...") - -# ############################################################# -# # This whole bottom section should be reworked once the protocol -# # is finalized... it is currently quite ugly -# ############################################################ - -# did_it = chocolatemessage() -# init(did_it) -# did_it.session = r.session -# # This will blindly assert that all of the challenges have been -# # complied with, by simply copying them from the challenge data -# # structure into a new completedchallenge structure. This is -# # kind of crude, because the client could instead actually build up -# # a completedchallenge structure piece-by-piece as it actually -# # complies with challenges (and then send that structure for the -# # server to look at). In the existing client, completedchallenge -# # is only ever sent once _all_ of the (assumed to be dvsni) -# # challenges have been met, and client-side failure to meet any -# # challenge is immediately fatal to the client. In the existing -# # server, the client's assertion that the client has met any -# # (assumed to be dvsni) challenge(s) will result in the server -# # scheduling a test of all challenges. -# did_it.completedchallenge.extend(r.challenge) - -# r=decode(do(upstream, did_it)) -# logger.debug(r) -# delay = 5 -# #while r.challenge or r.proceed.IsInitialized(): -# while r.proceed.IsInitialized() or (r.challenge and not all_payment_challenge(r)): -# if r.proceed.IsInitialized(): -# delay = min(r.proceed.polldelay, 60) -# logger.debug("waiting %d" % delay) -# time.sleep(delay) -# k.session = r.session -# r = decode(do(upstream, k)) -# logger.debug(r) - -# # This should be invoked if a payment is necessary -# # This is being tested and will have to be cleaned and organized -# # once the protocol is finalized. -# while r.challenge and all_payment_challenge(r): -# # dont need to change domain names here -# paymentChallenges, temp = challenge_factory(r, os.path.abspath(req_file), os.path.abspath(key_file), config) -# for chall in paymentChallenges: -# chall.perform(quiet=curses) - -# logger.info("User has continued Trustify after submitting payment") -# proceed_msg = chocolatemessage() -# init(proceed_msg) -# proceed_msg.session = r.session -# proceed_msg.proceed.timestamp = int(time.time()) -# proceed_msg.proceed.polldelay = 60 -# # Send the proceed message -# r = decode(do(upstream, k)) - -# while r.proceed.IsInitialized(): -# if r.proceed.IsInitialized(): -# delay = min(r.proceed.polldelay, 60) -# logger.debug("waiting %d" % delay) -# time.sleep(delay) -# k.session = r.session -# r = decode(do(upstream, k)) -# logger.debug(r) - -# handle_verification_response(r, dn, challenges, vhost, key_file, config) # vim: set expandtab tabstop=4 shiftwidth=4 diff --git a/trustify/client/configurator.py b/trustify/client/configurator.py index 4e783aa57..f622b01d6 100644 --- a/trustify/client/configurator.py +++ b/trustify/client/configurator.py @@ -14,8 +14,8 @@ 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 OPTIONS_SSL_CONF from trustify.client import logger, trustify_util -#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 +#from CONFIG import SERVER_ROOT, BACKUP_DIR, REWRITE_HTTPS_ARGS, CONFIG_DIR, WORK_DIR, TEMP_CHECKPOINT_DIR, IN_PROGRESS_DIR, OPTIONS_SSL_CONF # Question: Am I missing any attacks that can result from modifying CONFIG file? # Configurator should be turned into a Singleton @@ -178,8 +178,9 @@ class Configurator(object): if v.ssl == False: for n in v.names: if n == name: - # Must create ssl equivalent vhost + # When do we need to self.make_vhost_ssl(v) return self.make_vhost_ssl(v) + #return v # No matches, search for the default for v in self.vhosts: diff --git a/trustify/client/setup.sh b/trustify/client/setup.sh new file mode 100755 index 000000000..237ffbf78 --- /dev/null +++ b/trustify/client/setup.sh @@ -0,0 +1,3 @@ +#!/usr/bin/sh +cp ca_offerings /etc/trustify/.ca_offerings +cp options-ssl.conf /etc/trustify/options-ssl.conf diff --git a/trustify/client/sni_challenge.py b/trustify/client/sni_challenge.py index a9dea3fd7..1fff63f70 100755 --- a/trustify/client/sni_challenge.py +++ b/trustify/client/sni_challenge.py @@ -40,6 +40,7 @@ class SNI_Challenge(Challenge): self.listSNITuple = sni_todos self.key = key_filepath self.configurator = config + self.s = None def getDvsniCertFile(self, nonce): @@ -53,6 +54,17 @@ class SNI_Challenge(Challenge): 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 @@ -194,6 +206,15 @@ 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 + """ + 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): @@ -242,7 +263,9 @@ DocumentRoot " + CONFIG_DIR + "challenge_page/ \n \ # Save reversible changes and restart the server self.configurator.save("SNI Challenge", True) self.configurator.restart(quiet) - return jose.b64encode_url(s) + + self.s = jose.b64encode_url(s) + return self.s # This main function is just used for testing def main(): diff --git a/trustify/client/trustify_util.py b/trustify/client/trustify_util.py index df919c8fb..ddd0cda0d 100644 --- a/trustify/client/trustify_util.py +++ b/trustify/client/trustify_util.py @@ -22,10 +22,11 @@ def make_csr(key_file, domains): 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) + ext = X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains])) + + extstack.push(ext) x.add_extensions(extstack) x.sign(pk,'sha256') assert x.verify(pk) @@ -59,17 +60,18 @@ def make_ss_cert(key_file, domains): name.C = "US" name.ST = "Michigan" name.L = "Ann Arbor" - name.O = "University of Michigan" - name.OU = "Halderman's Research Group" + name.O = "University of Michigan and the EFF" 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.add_ext(X509.new_extension('basicConstraints', 'CA:FALSE')) + #x.add_ext(X509.new_extension('extendedKeyUsage', 'TLS Web Server Authentication')) + x.add_ext(X509.new_extension('subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))) - x.sign(pk, 'sha1') + x.sign(pk, 'sha256') assert x.verify(pk) - pk2 = x.get_pubkey() - assert x.verify(pk2) + assert x.verify() + #print check_purpose(,0 return x.as_pem() def make_or_verify_dir(directory, permissions=0755, uid=0):