From ac3441a972e8576eb1c4e5ef0b58ec23b83ac14f Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 9 Jul 2012 00:01:19 -0700 Subject: [PATCH 01/15] changes to make CSR.issue() successfully issue certs --- server-ca/CA.sh | 12 +++++++++++- server-ca/CSR.py | 12 +++++++++++- server-ca/demoCA/index.txt.attr | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/server-ca/CA.sh b/server-ca/CA.sh index 7ad6b8c52..488c917af 100755 --- a/server-ca/CA.sh +++ b/server-ca/CA.sh @@ -1,5 +1,10 @@ #!/bin/sh -# + +# A fully automated robo-CA does have to have credentials stored somewhere +# that it can use to issue certs on its own initiative! Though ideally +# the actual signing key would be in an HSM, not a text file. +export PASSWORD=dang + # CA - wrapper around ca to make it easier to use ... basically ca requires # some setup stuff to be done before you can use it and this makes # things easier between now and when Eric is convinced to fix it :-) @@ -158,6 +163,11 @@ case $1 in cat newcert.pem echo "Signed certificate is in newcert.pem" ;; +-chocolate) + /bin/echo -e "y\ny\ny\n" | $CA -passin env:PASSWORD -policy policy_anything -out "$3" -infiles "$2" + RET=$? + exit $RET + ;; -signCA) $CA -policy policy_anything -out newcert.pem -extensions v3_ca -infiles newreq.pem RET=$? diff --git a/server-ca/CSR.py b/server-ca/CSR.py index 3d4225a74..3b54705ec 100644 --- a/server-ca/CSR.py +++ b/server-ca/CSR.py @@ -115,4 +115,14 @@ def issue(csr): # TODO: a real CA should severely restrict the content of the cert, not # just grant what's asked for. (For example, the CA shouldn't trust # all the data in the subject field if it hasn't been validated.) - return "-----BEGIN CERTIFICATE-----\nThanks for the shrubbery!\n-----END CERTIFICATE-----" + # Therefore, we should construct a new CSR from scratch using the + # parsed-out data from the input CSR, and then pass that to OpenSSL. + cert = None + with tempfile.NamedTemporaryFile() as csr_tmp: + csr_tmp.write(csr) + csr_tmp.flush() + with tempfile.NamedTemporaryFile() as cert_tmp: + ret = subprocess.Popen(["./CA.sh", "-chocolate", csr_tmp.name, cert_tmp.name],shell=False,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE).wait() + if ret == 0: + cert = cert_tmp.read() + return cert diff --git a/server-ca/demoCA/index.txt.attr b/server-ca/demoCA/index.txt.attr index 8f7e63a34..3a7e39e6e 100644 --- a/server-ca/demoCA/index.txt.attr +++ b/server-ca/demoCA/index.txt.attr @@ -1 +1 @@ -unique_subject = yes +unique_subject = no From a3435b79829ddd88126cdbdebfa68c9f0004217f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 9 Jul 2012 07:59:30 -0400 Subject: [PATCH 02/15] added ability for concurrent sni challenges, made choc ext critical, allows variable oid --- client-webserver/sni_challenge.py | 89 ++++++++++++------- .../sni_challenge/verify_sni_challenge.py | 15 ++-- 2 files changed, 66 insertions(+), 38 deletions(-) diff --git a/client-webserver/sni_challenge.py b/client-webserver/sni_challenge.py index 1b2666d76..ac0dc1162 100644 --- a/client-webserver/sni_challenge.py +++ b/client-webserver/sni_challenge.py @@ -9,17 +9,19 @@ from shutil import move from os import remove, close CHOC_DIR = "/home/james/Documents/apache_choc/" -#CHOC_KEY = "../ca/sni_challenge/testing.key" -CHOC_KEY = CHOC_DIR + "testing.key" -SERVER_BASE = "/etc/apache2/" -CHOC_CERT = CHOC_DIR + "choc.crt" -CSR = CHOC_DIR + "choc.csr" CHOC_CERT_CONF = "choc_cert_extensions.cnf" OPTIONS_SSL_CONF = CHOC_DIR + "options-ssl.conf" APACHE_CHALLENGE_CONF = CHOC_DIR + "choc_sni_cert_challenge.conf" S_SIZE = 32 NONCE_SIZE = 32 +#class sni_challenge(object): +# def __init__(self, ip_addrs, y, nonce): +# self.address = ip_addrs + +def getChocCertFile(nonce): + return CHOC_DIR + byteToHex(nonce) + ".crt" + def findApacheConfigFile(): #This needs to be fixed to account for multiple httpd.conf files try: @@ -32,7 +34,7 @@ def findApacheConfigFile(): print "Please include .... in the conf file" return None -def modifyApacheConfig(mainConfig, nonce, ip_addr): +def getConfigText(nonce, ip_addr, key): configText = " \n \ \n \ Servername " + nonce + ".chocolate \n \ @@ -41,19 +43,26 @@ UseCanonicalName on \n \ LimitRequestBody 1048576 \n \ \n \ Include " + OPTIONS_SSL_CONF + " \n \ -SSLCertificateFile " + CHOC_CERT + " \n \ -SSLCertificateKeyFile " + CHOC_KEY + " \n \ +SSLCertificateFile " + getChocCertFile(nonce) + " \n \ +SSLCertificateKeyFile " + key + " \n \ \n \ DocumentRoot " + CHOC_DIR + "challenge_page/ \n \ \n \ -" + \n" + + return configText + +def modifyApacheConfig(mainConfig, listSNITuple): + configText = "" + for tup in listSNITuple: + configText += getConfigText(tup[2], tup[0], tup[5]) checkForApacheConfInclude(mainConfig) newConf = open(APACHE_CHALLENGE_CONF, 'w') newConf.write(configText) newConf.close() -# Need to add NameVirtualHost IP_ADDR +# Need to add NameVirtualHost IP_ADDR or does the chocolate install do this? def checkForApacheConfInclude(mainConfig): searchStr = "Include " + APACHE_CHALLENGE_CONF #conf = open(mainConfig, 'r+') @@ -64,19 +73,19 @@ def checkForApacheConfInclude(mainConfig): subprocess.check_output(["sudo", "tee", "-a", mainConfig], stdin=process.stdout) process.stdout.close() - conf.close(); + conf.close() -def createChallengeCert(ext): +def createChallengeCert(oid, ext, nonce, csr, key): #Assume CSR is already generated from original request - updateCertConf(ext) - subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", CHOC_KEY, "-out", CHOC_CERT, "-in", CSR]) + updateCertConf(oid, ext) + subprocess.call(["openssl", "x509", "-req", "-days", "21", "-extfile", CHOC_CERT_CONF, "-extensions", "v3_ca", "-signkey", key, "-out", getChocCertFile(nonce), "-in", csr]) -def generateExtension(challengeValue): - rsaPrivKey = RSA.importKey(open(CHOC_KEY).read()) - r = rsaPrivKey.decrypt(challengeValue) - print r +def generateExtension(key, y): + rsaPrivKey = RSA.importKey(open(key).read()) + r = rsaPrivKey.decrypt(y) + #print r s = Random.get_random_bytes(S_SIZE) #s = "0xDEADBEEF" @@ -86,13 +95,22 @@ def generateExtension(challengeValue): def byteToHex(byteStr): return ''.join(["%02X" % ord(x) for x in byteStr]).strip() -def updateCertConf(value): +#Searches for the first extension specified in binary +def updateCertConf(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') for line in confOld: - if line.startswith("1.3.3.7=DER:"): - confNew.write("1.3.3.7=DER:" + value + "\n") + if "=critical, DER:" in line: + confNew.write(oid + "=critical, DER:" + value + "\n") else: confNew.write(line) confNew.close() @@ -104,28 +122,37 @@ def apache_restart(): subprocess.call(["sudo", "/etc/init.d/apache2", "reload"]) #main call -def perform_sni_cert_challenge(address, r, nonce): - ext = generateExtension(r) - createChallengeCert(ext) +# address, y, nonce, ext, CSR, KEY +def perform_sni_cert_challenge(listSNITuple): + for tup in listSNITuple: + ext = generateExtension(tup[5], tup[1]) + createChallengeCert(tup[3], ext, tup[2], tup[4], tup[5]) - #Need to decide the form of nonce - modifyApacheConfig(findApacheConfigFile(), nonce, address) + modifyApacheConfig(findApacheConfigFile(), listSNITuple) apache_restart() def main(): + key = CHOC_DIR + "testing.key" + key2 = CHOC_DIR + "testing2.key" + csr = CHOC_DIR + "choc.csr" + csr2 = CHOC_DIR + "choc2.csr" - testkey = RSA.importKey(open(CHOC_KEY).read()) - - #the second parameter is ignored - #https://www.dlitz.net/software/pycrypto/api/current/ + testkey = RSA.importKey(open(key).read()) + testkey2 = RSA.importKey(open(key2).read()) r = Random.get_random_bytes(S_SIZE) r = "testValueForR" nonce = Random.get_random_bytes(NONCE_SIZE) nonce = "nonce" + r2 = "testValueForR2" + nonce2 = "nonce2" + #the second parameter is ignored + #https://www.dlitz.net/software/pycrypto/api/current/ y = testkey.encrypt(r, 0) - perform_sni_cert_challenge("127.0.0.1", y, nonce) + y2 = testkey2.encrypt(r2, 0) + + perform_sni_cert_challenge([("127.0.0.1", y, nonce, "1.3.3.7", csr, key), ("localhost",y2, nonce2, "1.3.3.7", csr2, key2)]) if __name__ == "__main__": main() diff --git a/server-ca/sni_challenge/verify_sni_challenge.py b/server-ca/sni_challenge/verify_sni_challenge.py index ced6f388f..7c48c74ce 100644 --- a/server-ca/sni_challenge/verify_sni_challenge.py +++ b/server-ca/sni_challenge/verify_sni_challenge.py @@ -84,15 +84,16 @@ def main(): nonce = Random.get_random_bytes(NONCE_SIZE) nonce = "nonce" - testkey = RSA.importKey(open("testing.key").read()) - - #the second parameter is ignored - #https://www.dlitz.net/software/pycrypto/api/current/ + nonce2 = "nonce2" + r = Random.get_random_bytes(NONCE_SIZE) r = "testValueForR" - encryptedValue = testkey.encrypt(r, 0) - valid, response = verify_challenge("127.0.0.1", r, binascii.hexlify(nonce)) - print response + r2 = "testValueForR2" + #valid, response = verify_challenge("127.0.0.1", r, binascii.hexlify(nonce)) + valid, response = verify_challenge("127.0.0.1", r, nonce) + print response + valid, response = verify_challenge("localhost", r2, nonce2) + print response if __name__ == "__main__": main() From 1f45a4cfe515c790af2e7377668a89be8bddb9f4 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 9 Jul 2012 22:22:20 -0700 Subject: [PATCH 03/15] enforce some protocol sanity checks --- server-ca/chocolate.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/server-ca/chocolate.py b/server-ca/chocolate.py index fc651535f..495280ae8 100755 --- a/server-ca/chocolate.py +++ b/server-ca/chocolate.py @@ -133,12 +133,29 @@ class session(object): def handlesession(self, m, r): if r.failure.IsInitialized(): return - # TODO: perhaps some code belongs here to enforce rules about which - # combinations of protocol messages can occur together. I think the - # rules are: Client must send either nothing (polling for updates) - # or exactly one of request, failure, or completedchallenge. Client - # may not send proceed, challenge, or success. If the rules are - # violated, we should self.die(r, r.BadRequest) and return. + # Note that m.challenge and m.completedchallenge present + # as lists, which are True if they are nonempty. By + # contrast, m.proceed, m.success, m.request, and m.failure + # are always True but have an .IsInitialized() property + # indicating whether they are actually present in m as + # messages from the client. + # + # Check for some ways in which the message from the client + # can be inappropriate. + if m.challenge or m.proceed.IsInitialized() or m.success.IsInitialized(): + self.die(r, r.BadRequest, uri="https://ca.example.com/failures/invalidfromclient") + return + distinct_messages = 0 + if m.request.IsInitialized(): distinct_messages += 1 + if m.failure.IsInitialized(): distinct_messages += 1 + if m.completedchallenge: distinct_messages += 1 + if distinct_messages > 1: + self.die(r, r.BadRequest, uri="https://ca.example.com/failures/mixedmessages") + return + # The rule that a new session must contain a request is enforced + # by handlenewsession. The rule that an existing session must + # not contain a request is enforced by handleexistingsession. + # TODO: check that there are no bad cases that slip through. if m.session == "": # New session r.session = random() @@ -269,8 +286,7 @@ class session(object): def send_challenges(self, m, r): if r.failure.IsInitialized(): return - # TODO: This needs a more sophisticated notion of success/failure, - # and also of the possibility of multiple data strings. + # TODO: This needs a more sophisticated notion of success/failure. for c in self.challenges(): # Currently, we can only handle challenge type 0 (dvsni) # TODO: unify names "succeeded" vs. "satisfied"? From ee9343469f4f0047d7e74e7815ec7995e2548951 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 9 Jul 2012 22:23:44 -0700 Subject: [PATCH 04/15] more explicit clarification about concurrency --- server-ca/daemon.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server-ca/daemon.py b/server-ca/daemon.py index 692072309..ea6df166b 100644 --- a/server-ca/daemon.py +++ b/server-ca/daemon.py @@ -12,12 +12,12 @@ # The queue mechanism with pending-* is supposed to control # concurrency issues properly, but this needs verification # to ensure that there are no possible race conditions. -# Generally, the server is not supposed to change sessions -# very much once they have been added to a queue, except -# for marking them no longer live if the server realizes -# that something bad has happened to them. There may be -# some exceptions, and they should all be analyzed for -# possible races. +# Generally, the server process (as distinct from the daemon) +# is not supposed to change sessions at all once they have +# been added to a queue, except for marking them no longer +# live if the server realizes that something bad has happened +# to them. There may be some exceptions, and they should all +# be analyzed for possible races. # TODO: The daemon should probably check for timeouts before # advancing sessions' state. Currently timeouts can only From cfc4cbaae347743479f9abfff05964b12dba3620 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Mon, 9 Jul 2012 22:33:02 -0700 Subject: [PATCH 05/15] remove disused debug flag in protocol --- server-ca/chocolate.py | 6 +----- server-ca/chocolate_protocol.proto | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/server-ca/chocolate.py b/server-ca/chocolate.py index 495280ae8..6d14f15a0 100755 --- a/server-ca/chocolate.py +++ b/server-ca/chocolate.py @@ -335,11 +335,7 @@ class session(object): # or similar. # Send reply - if m.debug: - web.header("Content-type", "text/plain") - return "SAW MESSAGE: %s\nRESPONSE: %s\n" % (str(m), str(r)) - else: - return r.SerializeToString() + return r.SerializeToString() def GET(self): web.header("Content-type", "text/html") diff --git a/server-ca/chocolate_protocol.proto b/server-ca/chocolate_protocol.proto index a4d1543f2..2bc75bbd2 100644 --- a/server-ca/chocolate_protocol.proto +++ b/server-ca/chocolate_protocol.proto @@ -106,8 +106,4 @@ message chocolatemessage { /* Sent by SERVER to issue the requested certificate */ optional Success success = 8; - - /* For debugging; should be removed in final protocol. */ - optional bool debug = 9; /* Causes server to return text instead of - message! */ } From 3f03dc4e5c6ff9b59c4c235ebf169705578e17b1 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 10 Jul 2012 00:25:11 -0700 Subject: [PATCH 06/15] implement dvsni challenge checking --- server-ca/daemon.py | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/server-ca/daemon.py b/server-ca/daemon.py index ea6df166b..5fa03c168 100644 --- a/server-ca/daemon.py +++ b/server-ca/daemon.py @@ -102,14 +102,47 @@ def testchallenge(session): # that it has completed the challenges. Information about # the client's reporting could be stored in the database. # Then the CA doesn't need to poll prematurely. - if False: # if challenges all succeed + all_satisfied = True + for i, name in enumerate(r.lrange("%s:names" % session, 0, -1)): + challenge = "%s:%d" % (session, i) + challtime = r.hget(challenge, "challtime") + challtype = r.hget(challenge, "type") + name = r.hget(challenge, "name") + satisfied = r.hget(challenge, "satisfied") == "True" + failed = r.hget(challenge, "failed") == "True" + # TODO: check whether this challenge is too old + if not satisfied and not failed: + if challtype == 0: # DomainValidateSNI + dvsni_nonce = r.hget(challenge, "dvsni:nonce") + dvsni_r = r.hget(challenge, "dvsni:r") + dvsni_ext = r.hget(challenge, "dvsni:ext") + if verify_challenge(name, dvsni_r, dvsni_nonce)[0]: + r.hset(challenge, "satisfied", True) + else: + all_satisfied = False + # TODO: distinguish permanent and temporarily failures + # can cause a permanent failure under some conditions, causing + # the session to become dead. TODO: need to articulate what + # those conditions are + else: + # Don't know how to handle this challenge type + all_satisfied = False + elif not satisfied: + all_satisfied = False + if all_satisfied: + # Challenges all succeeded, so we should prepare to issue + # the requested cert. + # TODO: double-check that there were > 0 challenges, + # so that we don't somehow mistakenly issue a cert in + # response to an empty list of challenges (even though + # the daemon that put this session on the queue should + # also have implicitly guaranteed this). r.hset(session, "state", "issue") r.lpush("pending-issue", session) else: + # Some challenges are not verified. + # Put this session back on the stack to try to verify again. r.lpush("pending-testchallenge", session) - # can also cause a failure under some conditions, causing the - # session to become dead. TODO: need to articulate what those - # conditions are def issue(session): if r.hget(session, "live") != "True": @@ -130,9 +163,6 @@ def issue(session): # should never happen. r.lrem("pending-requests", session) return - # Note that we can push this back into the original queue. - # TODO: need to add a way to make sure we don't test the same - # TODO: actually make this call issue the cert csr = r.hget(session, "csr") cert = CSR.issue(csr) r.hset(session, "cert", cert) From b8c3856dfd5bdddb532353c22b4a2c7180887ade Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 15:43:49 -0400 Subject: [PATCH 07/15] Changed extension conf to critical --- client-webserver/choc_cert_extensions.cnf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-webserver/choc_cert_extensions.cnf b/client-webserver/choc_cert_extensions.cnf index 5cba2c49d..91deb35f1 100644 --- a/client-webserver/choc_cert_extensions.cnf +++ b/client-webserver/choc_cert_extensions.cnf @@ -13,4 +13,4 @@ choc=1.3.3.7 basicConstraints = CA:TRUE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer:always -1.3.3.7=DER:C15B2E9D8F49A16C57A66F9E07C18C0B524183C427dae3a76c9c5945ff91c27d28eec5a4cc8e2e42a274f7de42d079bfe413171e +1.3.3.7=critical, DER:9720ED6B02437C2F421CF3B25565551ED253D0270F006186EE1CD4BCF82F4230abc36d4012e5b4842ba0a1011171419ff7d27ba523e24116d7ecefe9253a943f From cd7b849366da2419a4a005634ffb4d1c843a232d Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 16:17:10 -0400 Subject: [PATCH 08/15] Fixed conference call bug, made challenge servers only accessible by SNI name --- client-webserver/sni_challenge.py | 16 ++++++++++------ server-ca/sni_challenge/verify_sni_challenge.py | 5 +++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/client-webserver/sni_challenge.py b/client-webserver/sni_challenge.py index ac0dc1162..3727698fd 100644 --- a/client-webserver/sni_challenge.py +++ b/client-webserver/sni_challenge.py @@ -7,6 +7,7 @@ import hmac import hashlib from shutil import move from os import remove, close +import binascii CHOC_DIR = "/home/james/Documents/apache_choc/" CHOC_CERT_CONF = "choc_cert_extensions.cnf" @@ -20,7 +21,7 @@ NONCE_SIZE = 32 # self.address = ip_addrs def getChocCertFile(nonce): - return CHOC_DIR + byteToHex(nonce) + ".crt" + return CHOC_DIR + nonce + ".crt" def findApacheConfigFile(): #This needs to be fixed to account for multiple httpd.conf files @@ -35,10 +36,10 @@ def findApacheConfigFile(): return None def getConfigText(nonce, ip_addr, key): - configText = " \n \ - \n \ + configText = " \n \ Servername " + nonce + ".chocolate \n \ UseCanonicalName on \n \ +SSLStrictSNIVHostCheck on \n \ \n \ LimitRequestBody 1048576 \n \ \n \ @@ -47,15 +48,15 @@ SSLCertificateFile " + getChocCertFile(nonce) + " \n \ SSLCertificateKeyFile " + key + " \n \ \n \ DocumentRoot " + CHOC_DIR + "challenge_page/ \n \ - \n \ - \n" + \n\n " return configText def modifyApacheConfig(mainConfig, listSNITuple): - configText = "" + configText = " \n" for tup in listSNITuple: configText += getConfigText(tup[2], tup[0], tup[5]) + configText += " \n" checkForApacheConfInclude(mainConfig) newConf = open(APACHE_CHALLENGE_CONF, 'w') @@ -152,6 +153,9 @@ def main(): y = testkey.encrypt(r, 0) y2 = testkey2.encrypt(r2, 0) + nonce = binascii.hexlify(nonce) + nonce2 = binascii.hexlify(nonce2) + perform_sni_cert_challenge([("127.0.0.1", y, nonce, "1.3.3.7", csr, key), ("localhost",y2, nonce2, "1.3.3.7", csr2, key2)]) if __name__ == "__main__": diff --git a/server-ca/sni_challenge/verify_sni_challenge.py b/server-ca/sni_challenge/verify_sni_challenge.py index 7c48c74ce..35b7604a2 100644 --- a/server-ca/sni_challenge/verify_sni_challenge.py +++ b/server-ca/sni_challenge/verify_sni_challenge.py @@ -30,8 +30,6 @@ def check_challenge_value(ext_value, r): #print "s: ", byteToHex(s) #print "mac: ", byteToHex(mac) #print "expected_mac: ", byteToHex(expected_mac) - #print type(mac) - #print type(expected_mac) if mac == expected_mac: return True @@ -90,6 +88,9 @@ def main(): r = "testValueForR" r2 = "testValueForR2" + nonce = binascii.hexlify(nonce) + nonce2 = binascii.hexlify(nonce2) + #valid, response = verify_challenge("127.0.0.1", r, binascii.hexlify(nonce)) valid, response = verify_challenge("127.0.0.1", r, nonce) print response From 662e935bdf88524ff719ee131915513a368a2070 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 16:46:38 -0400 Subject: [PATCH 09/15] removed postConnectionCheck --- server-ca/sni_challenge/verify_sni_challenge.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server-ca/sni_challenge/verify_sni_challenge.py b/server-ca/sni_challenge/verify_sni_challenge.py index 35b7604a2..9d8dd8fdb 100644 --- a/server-ca/sni_challenge/verify_sni_challenge.py +++ b/server-ca/sni_challenge/verify_sni_challenge.py @@ -53,6 +53,10 @@ def verify_challenge(address, r, nonce): context.set_allow_unknown_ca(True) context.set_verify(M2Crypto.SSL.verify_none, 4) + #Consider placing try/catch block around wrong host exception + #or fix M2Crypto to handle SANs appropriately + postConnectionCheck = None + conn = M2Crypto.SSL.Connection(context) sni_support.set_sni_ext(conn.ssl, sni_name) conn.connect((address, 443)) From 6ac9d358aff3c7b4a8b4ae89c24307a06b330d41 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 17:00:59 -0400 Subject: [PATCH 10/15] minor postConnectionCheck fix --- server-ca/sni_challenge/verify_sni_challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-ca/sni_challenge/verify_sni_challenge.py b/server-ca/sni_challenge/verify_sni_challenge.py index 9d8dd8fdb..ad4697aa7 100644 --- a/server-ca/sni_challenge/verify_sni_challenge.py +++ b/server-ca/sni_challenge/verify_sni_challenge.py @@ -55,7 +55,7 @@ def verify_challenge(address, r, nonce): #Consider placing try/catch block around wrong host exception #or fix M2Crypto to handle SANs appropriately - postConnectionCheck = None + M2Crypto.SSL.Connection.postConnectionCheck = None conn = M2Crypto.SSL.Connection(context) sni_support.set_sni_ext(conn.ssl, sni_name) From 87918f6124b92a2d5e6ed51134b0603d1bed4833 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 17:06:26 -0400 Subject: [PATCH 11/15] added in code to catch SSL connection failures --- server-ca/sni_challenge/verify_sni_challenge.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server-ca/sni_challenge/verify_sni_challenge.py b/server-ca/sni_challenge/verify_sni_challenge.py index ad4697aa7..fd33d2afd 100644 --- a/server-ca/sni_challenge/verify_sni_challenge.py +++ b/server-ca/sni_challenge/verify_sni_challenge.py @@ -59,7 +59,10 @@ def verify_challenge(address, r, nonce): conn = M2Crypto.SSL.Connection(context) sni_support.set_sni_ext(conn.ssl, sni_name) - conn.connect((address, 443)) + try: + conn.connect((address, 443)) + except: + return False, "Connection to SSL Server failed" cert_chain = conn.get_peer_cert_chain() @@ -95,7 +98,6 @@ def main(): nonce = binascii.hexlify(nonce) nonce2 = binascii.hexlify(nonce2) - #valid, response = verify_challenge("127.0.0.1", r, binascii.hexlify(nonce)) valid, response = verify_challenge("127.0.0.1", r, nonce) print response valid, response = verify_challenge("localhost", r2, nonce2) From e1bbcd04b7ab24ebeb781d528bd3598a42b513a1 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Tue, 10 Jul 2012 17:27:26 -0400 Subject: [PATCH 12/15] changed format for perform_sni_challenge to [(addr, y, nonce, ext)], csr, key --- client-webserver/sni_challenge.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/client-webserver/sni_challenge.py b/client-webserver/sni_challenge.py index 3727698fd..fbc232805 100644 --- a/client-webserver/sni_challenge.py +++ b/client-webserver/sni_challenge.py @@ -16,10 +16,6 @@ APACHE_CHALLENGE_CONF = CHOC_DIR + "choc_sni_cert_challenge.conf" S_SIZE = 32 NONCE_SIZE = 32 -#class sni_challenge(object): -# def __init__(self, ip_addrs, y, nonce): -# self.address = ip_addrs - def getChocCertFile(nonce): return CHOC_DIR + nonce + ".crt" @@ -52,10 +48,10 @@ DocumentRoot " + CHOC_DIR + "challenge_page/ \n \ return configText -def modifyApacheConfig(mainConfig, listSNITuple): +def modifyApacheConfig(mainConfig, listSNITuple, key): configText = " \n" for tup in listSNITuple: - configText += getConfigText(tup[2], tup[0], tup[5]) + configText += getConfigText(tup[2], tup[0], key) configText += " \n" checkForApacheConfInclude(mainConfig) @@ -108,12 +104,16 @@ def updateCertConf(oid, value): """ 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) @@ -124,23 +124,20 @@ def apache_restart(): #main call # address, y, nonce, ext, CSR, KEY -def perform_sni_cert_challenge(listSNITuple): +def perform_sni_cert_challenge(listSNITuple, csr, key): for tup in listSNITuple: - ext = generateExtension(tup[5], tup[1]) - createChallengeCert(tup[3], ext, tup[2], tup[4], tup[5]) + ext = generateExtension(key, tup[1]) + createChallengeCert(tup[3], ext, tup[2], csr, key) - modifyApacheConfig(findApacheConfigFile(), listSNITuple) + modifyApacheConfig(findApacheConfigFile(), listSNITuple, key) apache_restart() def main(): key = CHOC_DIR + "testing.key" - key2 = CHOC_DIR + "testing2.key" csr = CHOC_DIR + "choc.csr" - csr2 = CHOC_DIR + "choc2.csr" testkey = RSA.importKey(open(key).read()) - testkey2 = RSA.importKey(open(key2).read()) - + r = Random.get_random_bytes(S_SIZE) r = "testValueForR" nonce = Random.get_random_bytes(NONCE_SIZE) @@ -151,12 +148,12 @@ def main(): #the second parameter is ignored #https://www.dlitz.net/software/pycrypto/api/current/ y = testkey.encrypt(r, 0) - y2 = testkey2.encrypt(r2, 0) + y2 = testkey.encrypt(r2, 0) nonce = binascii.hexlify(nonce) nonce2 = binascii.hexlify(nonce2) - perform_sni_cert_challenge([("127.0.0.1", y, nonce, "1.3.3.7", csr, key), ("localhost",y2, nonce2, "1.3.3.7", csr2, key2)]) + perform_sni_cert_challenge([("127.0.0.1", y, nonce, "1.3.3.7"), ("localhost",y2, nonce2, "1.3.3.7")], csr, key) if __name__ == "__main__": main() From 208ded9d0211227f3578868688e507a7272bfa5b Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 10 Jul 2012 16:48:36 -0700 Subject: [PATCH 13/15] make this a Python package that can be imported --- server-ca/sni_challenge/Makefile | 2 +- server-ca/sni_challenge/__init__.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 server-ca/sni_challenge/__init__.py diff --git a/server-ca/sni_challenge/Makefile b/server-ca/sni_challenge/Makefile index 63bea9675..35cfac20d 100644 --- a/server-ca/sni_challenge/Makefile +++ b/server-ca/sni_challenge/Makefile @@ -12,4 +12,4 @@ _sni_support.so: sni_support_wrap.o sni_support.o $(CC) -shared $^ -o $@ clean: - rm -f $(targets) + rm -f $(targets) *.pyc diff --git a/server-ca/sni_challenge/__init__.py b/server-ca/sni_challenge/__init__.py new file mode 100644 index 000000000..e69de29bb From 2a514f5166be9f45bf39fb495705b8b5ea2a354f Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 10 Jul 2012 16:59:04 -0700 Subject: [PATCH 14/15] adjust daemon to use sni_challenge package --- server-ca/daemon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server-ca/daemon.py b/server-ca/daemon.py index 5fa03c168..a74528385 100644 --- a/server-ca/daemon.py +++ b/server-ca/daemon.py @@ -29,6 +29,7 @@ import redis, time, CSR r = redis.Redis() +from sni_challenge.verify import verify_challenge from Crypto.Hash import SHA256, HMAC from Crypto import Random @@ -116,7 +117,8 @@ def testchallenge(session): dvsni_nonce = r.hget(challenge, "dvsni:nonce") dvsni_r = r.hget(challenge, "dvsni:r") dvsni_ext = r.hget(challenge, "dvsni:ext") - if verify_challenge(name, dvsni_r, dvsni_nonce)[0]: + result, reason = verify_challenge(name, dvsni_r, dvsni_nonce) + if result: r.hset(challenge, "satisfied", True) else: all_satisfied = False From 2f35f3979788f1f04287f51fbed81f8df2834996 Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Tue, 10 Jul 2012 17:01:32 -0700 Subject: [PATCH 15/15] deploy and build sni_challenge code on Chocolate server --- server-ca/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server-ca/Makefile b/server-ca/Makefile index 91343eef4..bcedca363 100644 --- a/server-ca/Makefile +++ b/server-ca/Makefile @@ -1,5 +1,6 @@ deploy: chocolate_protocol_pb2.py chocolate.py CSR.py pkcs10.py daemon.py - scp chocolate_protocol.proto chocolate.py CSR.py pkcs10.py daemon.py ${CHOCOLATESERVER}: && ssh ${CHOCOLATESERVER} protoc chocolate_protocol.proto --python_out=. + scp chocolate_protocol.proto chocolate.py CSR.py pkcs10.py daemon.py ${CHOCOLATESERVER}: && ssh ${CHOCOLATESERVER} protoc chocolate_protocol.proto --python_out=. && rsync -av sni_challenge ${CHOCOLATESERVER}: && ssh ${CHOCOLATESERVER} make -C sni_challenge clean all + chocolate_protocol_pb2.py: chocolate_protocol.proto protoc chocolate_protocol.proto --python_out=.