mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 00:24:35 +02:00
Merge branch 'master' of github.com:research/chocolate
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -7,19 +7,18 @@ import hmac
|
||||
import hashlib
|
||||
from shutil import move
|
||||
from os import remove, close
|
||||
import binascii
|
||||
|
||||
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
|
||||
|
||||
def getChocCertFile(nonce):
|
||||
return CHOC_DIR + nonce + ".crt"
|
||||
|
||||
def findApacheConfigFile():
|
||||
#This needs to be fixed to account for multiple httpd.conf files
|
||||
try:
|
||||
@@ -32,28 +31,35 @@ def findApacheConfigFile():
|
||||
print "Please include .... in the conf file"
|
||||
return None
|
||||
|
||||
def modifyApacheConfig(mainConfig, nonce, ip_addr):
|
||||
configText = "<IfModule mod_ssl.c> \n \
|
||||
<VirtualHost " + ip_addr + ":443> \n \
|
||||
def getConfigText(nonce, ip_addr, key):
|
||||
configText = "<VirtualHost " + ip_addr + ":443> \n \
|
||||
Servername " + nonce + ".chocolate \n \
|
||||
UseCanonicalName on \n \
|
||||
SSLStrictSNIVHostCheck on \n \
|
||||
\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 \
|
||||
</VirtualHost> \n \
|
||||
</IfModule>"
|
||||
</VirtualHost> \n\n "
|
||||
|
||||
return configText
|
||||
|
||||
def modifyApacheConfig(mainConfig, listSNITuple, key):
|
||||
configText = "<IfModule mod_ssl.c> \n"
|
||||
for tup in listSNITuple:
|
||||
configText += getConfigText(tup[2], tup[0], key)
|
||||
configText += "</IfModule> \n"
|
||||
|
||||
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 +70,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,15 +92,28 @@ 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')
|
||||
|
||||
flag = False
|
||||
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")
|
||||
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)
|
||||
@@ -104,28 +123,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, csr, key):
|
||||
for tup in listSNITuple:
|
||||
ext = generateExtension(key, tup[1])
|
||||
createChallengeCert(tup[3], ext, tup[2], csr, key)
|
||||
|
||||
#Need to decide the form of nonce
|
||||
modifyApacheConfig(findApacheConfigFile(), nonce, address)
|
||||
modifyApacheConfig(findApacheConfigFile(), listSNITuple, key)
|
||||
apache_restart()
|
||||
|
||||
def main():
|
||||
key = CHOC_DIR + "testing.key"
|
||||
csr = CHOC_DIR + "choc.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())
|
||||
|
||||
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 = 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"), ("localhost",y2, nonce2, "1.3.3.7")], csr, key)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+11
-1
@@ -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=$?
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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=.
|
||||
|
||||
+25
-13
@@ -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"?
|
||||
@@ -319,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")
|
||||
|
||||
@@ -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! */
|
||||
}
|
||||
|
||||
+45
-13
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -102,14 +103,48 @@ 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")
|
||||
result, reason = verify_challenge(name, dvsni_r, dvsni_nonce)
|
||||
if result:
|
||||
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 +165,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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
unique_subject = yes
|
||||
unique_subject = no
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -55,9 +53,16 @@ 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
|
||||
M2Crypto.SSL.Connection.postConnectionCheck = None
|
||||
|
||||
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()
|
||||
|
||||
@@ -84,15 +89,18 @@ 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"
|
||||
|
||||
nonce = binascii.hexlify(nonce)
|
||||
nonce2 = binascii.hexlify(nonce2)
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user