som eminor changes, fixes, and reorganization

This commit is contained in:
Seth Schoen 2012-11-05 17:32:35 -08:00
parent 12d8d34672
commit 5fb6a5b07d
9 changed files with 9 additions and 333 deletions

View file

@ -28,4 +28,5 @@ hashcash_expiry = 60*60
extra_name_blacklist = ["eff.org", "www.eff.org"]
# Name of file containing cert chain
cert_chain_file = "chain.pem"
cert_chain_file = "demoCA/cacert.pem"
debug = True

View file

@ -2,6 +2,7 @@
import web, redis, time, binascii, re, urllib2
import CSR
from redis_lock import redis_lock
from trustify.protocol import hashcash
from CSR import M2Crypto
from Crypto import Random
@ -162,7 +163,7 @@ class session(object):
"""Is the hashcash string h valid for a request to this server for
signing n names?"""
if hashcash.check(stamp=h, resource=chocolate_server_name, \
bits=difficulty*n, check_expiration=hashcash_expiry):
bits=difficulty, check_expiration=hashcash_expiry):
# sessions.sadd returns True upon adding to a set and
# False if the item was already in the set.
return sessions.sadd("spent-hashcash", h)

View file

@ -1,44 +0,0 @@
#!/usr/bin/env python
# functions common to the various kinds of daemon
# TODO: define a log function that sends a pubsub message to the
# logger daemon
import time, binascii
from Crypto import Random
def signal_handler(a, b):
global clean_shutdown
clean_shutdown = True
r.publish("exit", "clean-exit")
r.lpush("exit", "clean-exit")
def short(session):
"""Return the first 12 bytes of a session ID, or, for a
challenge ID, the challenge ID with the session ID truncated."""
tmp = session.partition(":")
return tmp[0][:12] + "..." + tmp[1] + tmp[2]
def ancient(session, state):
"""Given that this session is in the specified named state,
decide whether the daemon should forcibly expire it for being too
old, even if no client request has caused the serve to mark the
session as expired. This is most relevant to truly abandoned
sessions that no client ever asks about."""
age = int(time.time()) - int(r.hget(session, "created"))
if state == "makechallenge" and age > 120:
if debug: print "considered", short(session), "ancient"
return True
if state == "testchallenge" and age > 600:
if debug: print "considered", short(session), "ancient"
return True
return False
def random():
"""Return 64 hex digits representing a new 32-byte random number."""
return binascii.hexlify(Random.get_random_bytes(32))
def random_raw():
"""Return 32 random bytes."""
return Random.get_random_bytes(32)

View file

@ -1,72 +0,0 @@
#!/usr/bin/env python
# This daemon runs on the CA side to look for requests in
# the database that are waiting for a cert to be issued.
import redis, redis_lock, CSR, sys, signal
from sni_challenge.verify import verify_challenge
from Crypto import Random
r = redis.Redis()
ps = r.pubsub()
issue_lock = redis_lock.redis_lock(r, "issue_lock")
# This lock guards the ability to issue certificates with "openssl ca",
# which has no locking of its own. We don't need locking for the updates
# that the daemon performs on the sessions in the database because the
# queues pending-makechallenge, pending-testchallenge, and pending-issue
# are updated atomically and the daemon only ever acts on sessions that it
# has removed from a queue.
debug = "debug" in sys.argv
clean_shutdown = False
from daemon_common import signal_handler, short, ancient, random, random_raw
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def issue(session):
if r.hget(session, "live") != "True":
# This session has died due to some other reason, like an
# illegal request or timeout, since it entered testchallenge
# state. Consequently, we're not allowed to advance its
# state any further, and it should be removed from the
# pending-requests queue and not pushed into any other queue.
# We don't have to remove it from pending-testchallenge
# because the caller has already done so.
#
# Having a session in pending-issue die is a very weird case
# that probably suggests that timeouts are set incorrectly
# or that the client is misbehaving very badly. This means
# that a request passed all of its challenges but the
# session nonetheless died for some reason unrelated to failing
# challenges before the cert could be issued. Normally, this
# should never happen.
if debug: print "removing expired (issue-state!?) session", short(session)
r.lrem("pending-requests", session)
return
if r.hget(session, "state") != "issue":
return
csr = r.hget(session, "csr")
names = r.lrange("%s:names" % session, 0, -1)
with issue_lock:
cert = CSR.issue(csr, names)
r.hset(session, "cert", cert)
if cert: # once issuing cert succeeded
if debug: print "%s: issued certificate for names: %s" % (short(session), ", ".join(names))
r.hset(session, "state", "done")
# r.lpush("pending-done", session)
else: # should not be reached in deployed version
if debug: print "issuing for", short(session), "failed"
r.lpush("pending-issue", session)
while True:
(where, what) = r.brpop(["exit", "pending-issue"])
if where == "exit":
r.lpush("exit", "exit")
break
elif where == "pending-issue":
issue(what)
if clean_shutdown:
print "daemon exiting cleanly"
break

View file

@ -1,29 +0,0 @@
#!/usr/bin/env python
# This daemon runs on the CA side to handle logging.
import redis, signal
r = redis.Redis()
ps = r.pubsub()
debug = "debug" in sys.argv
clean_shutdown = False
from daemon_common import signal_handler
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
ps.subscribe(["logs", "exit"])
for message in ps.listen():
if message["type"] != "message":
continue
if message["channel"] == "logs":
if debug: print message["data"]
continue
if message["channel"] == "exit":
break
if clean_shutdown:
print "daemon exiting cleanly"
break

View file

@ -1,70 +0,0 @@
#!/usr/bin/env python
# This daemon runs on the CA side to look for requests in
# the database that are waiting for challenges to be issued.
import redis, redis_lock, time, sys, signal
r = redis.Redis()
ps = r.pubsub()
debug = "debug" in sys.argv
clean_shutdown = False
from daemon_common import signal_handler, short, ancient, random, random_raw
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def makechallenge(session):
if r.hget(session, "live") != "True":
# This session has died due to some other reason, like an
# illegal request or timeout, since it entered makechallenge
# state. Consequently, we're not allowed to advance its
# state any further, and it should be removed from the
# pending-requests queue and not pushed into any other queue.
# We don't have to remove it from pending-makechallenge
# because the caller has already done so.
if debug: print "removing expired session", short(session)
r.lrem("pending-requests", session)
return
# Currently only makes challenges of type 0 (DomainValidateSNI)
# This challenge type has three internal data parameters:
# dvsni:nonce, dvsni:r, dvsni:ext
# This challenge type sends three data parameters to the client:
# nonce, y = E(r), ext
#
# Make one challenge for each name. (This one-to-one relationship
# is not an inherent protocol requirement!)
names = r.lrange("%s:names" % session, 0, -1)
if debug: print "%s: new valid request" % session
if debug: print "%s: from requesting client at %s" % (short(session), r.hget(session, "client-addr"))
if debug: print "%s: for %d names: %s" % (short(session), len(names), ", ".join(names))
for i, name in enumerate(names):
challenge = "%s:%d" % (session, i)
r.hset(challenge, "challtime", int(time.time()))
r.hset(challenge, "type", 0) # DomainValidateSNI
r.hset(challenge, "name", name)
r.hset(challenge, "satisfied", False)
r.hset(challenge, "failed", False)
r.hset(challenge, "dvsni:nonce", random())
r.hset(challenge, "dvsni:r", random_raw())
r.hset(challenge, "dvsni:ext", "1.3.3.7")
# Keep accurate count of how many challenges exist in this session.
r.hincrby(session, "challenges", 1)
if debug: print "created new challenge", short(challenge)
if True: # challenges have been created
r.hset(session, "state", "testchallenge")
else:
r.lpush("pending-makechallenge", session)
while True:
(where, what) = r.brpop(["exit", "pending-makechallenge"])
if where == "exit":
r.lpush("exit", "exit")
break
elif where == "pending-makechallenge":
makechallenge(what)
if clean_shutdown:
print "daemon exiting cleanly"
break

View file

@ -1,111 +0,0 @@
#!/usr/bin/env python
# This daemon runs on the CA side to look for requests in
# the database that are waiting for the CA to test whether
# challenges have been met, and to perform this test.
import redis, redis_lock, time, sys, signal
from sni_challenge.verify import verify_challenge
r = redis.Redis()
ps = r.pubsub()
debug = "debug" in sys.argv
clean_shutdown = False
from daemon_common import signal_handler, short, ancient, random, random_raw
def signal_handler(a, b):
global clean_shutdown
clean_shutdown = True
r.publish("exit", "clean-exit")
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def testchallenge(session):
if r.hget(session, "live") != "True":
# This session has died due to some other reason, like an
# illegal request or timeout, since it entered testchallenge
# state. Consequently, we're not allowed to advance its
# state any further, and it should be removed from the
# pending-requests queue and not pushed into any other queue.
# We don't have to remove it from pending-testchallenge
# because the caller has already done so.
if debug: print "removing expired session", short(session)
r.lrem("pending-requests", session)
return
if r.hget(session, "state") != "testchallenge":
return
if int(r.hincrby(session, "times-tested", 1)) > 3:
# This session has already been unsuccessfully tested three
# times. Clearly, something has gone wrong or the client is
# just trying to annoy us. Do not allow it to be tested again.
r.hset(session, "live", False)
r.lrem("pending-requests", session)
return
all_satisfied = True
for i, name in enumerate(r.lrange("%s:names" % session, 0, -1)):
challenge = "%s:%d" % (session, i)
if debug: print "testing challenge", short(challenge)
challtime = int(r.hget(challenge, "challtime"))
challtype = int(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 debug: print "challenge", short(challenge), "being tested"
if challtype == 0: # DomainValidateSNI
if debug: print "\tbeginning dvsni test to %s" % name
dvsni_nonce = r.hget(challenge, "dvsni:nonce")
dvsni_r = r.hget(challenge, "dvsni:r")
dvsni_ext = r.hget(challenge, "dvsni:ext")
direct_result, direct_reason = verify_challenge(name, dvsni_r, dvsni_nonce, False)
proxy_result, proxy_reason = verify_challenge(name, dvsni_r, dvsni_nonce, True)
if debug:
print "\t...direct probe: %s (%s)" % (direct_result, direct_reason)
print "\tTor proxy probe: %s (%s)" % (proxy_result, proxy_reason)
if direct_result and proxy_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:
if debug: print "\tchallenge was not attempted"
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).
if debug: print "\t** All challenges satisfied; request %s GRANTED" % short(session)
r.hset(session, "state", "issue")
r.lpush("pending-issue", session)
else:
# Some challenges were not verified. In the current
# design of this daemon, the client must contact
# us again to request that the session be placed back
# in pending-testchallenge!
pass
while True:
(where, what) = r.brpop(["exit", "pending-testchallenge"])
if where == "exit":
r.lpush("exit", "exit")
break
elif where == "pending-testchallenge":
with redis_lock(r, "lock-" + what):
testchallenge(what)
if clean_shutdown:
print "daemon exiting cleanly"
break

View file

@ -68,8 +68,8 @@ def verify_challenge(address, r, nonce, socksify=False):
sni_support.set_sni_ext(conn.ssl, sni_name)
try:
conn.connect((address, 443))
except:
return False, "Connection to SSL Server failed"
except Exception, e:
return False, "Connection to SSL Server failed (%s)" % str(e)
cert_chain = conn.get_peer_cert_chain()

View file

@ -10,13 +10,13 @@ nohup ./logging-daemon.py &
# runs of the daemon should occur here.
echo "Starting issue daemon..."
nohup daemons/issue-daemon.py &
nohup ./issue-daemon.py &
for instance in a b c
do
echo "Starting testchallenge daemon $instance..."
nohup daemons/testchallenge-daemon.py &
nohup ./testchallenge-daemon.py &
done
echo "Starting makechallenge daemon..."
nohup daemons/makechallenge-daemon.py &
nohup ./makechallenge-daemon.py &