1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-26 07:41:33 +03:00
Files
certbot/server-ca/daemon.py
2012-07-12 14:30:56 -07:00

219 lines
9.5 KiB
Python

#!/usr/bin/env python
# This daemon runs on the CA side to look for requests in
# the database that are waiting for actions to be taken:
# generating challenges, testing whether challenges have
# been met, and issuing certs when the challenges have been
# met. The daemon does not communicate with the client at
# all; it just notes changes to request state in the database,
# which the server will inform the client about when the
# client subsequently checks in.
# 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 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
# happen if something other than the daemon notices them,
# which currently can only happen when the client checks in.
# If the client never checks in, the daemon can keep advancing
# the request's state, which may not be the right behavior.
import redis, time, CSR, sys, signal
r = redis.Redis()
from sni_challenge.verify import verify_challenge
from Crypto.Hash import SHA256, HMAC
from Crypto import Random
debug = "debug" in sys.argv
clean_shutdown = False
def signal_handler(a, b):
global clean_shutdown
clean_shutdown = True
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def sha256(m):
return SHA256.new(m).hexdigest()
def hmac(k, m):
return HMAC.new(k, m, SHA256).hexdigest()
def random():
"""Return 64 hex digits representing a new 32-byte random number."""
return sha256(Random.get_random_bytes(32))
def random_raw():
"""Return 32 random bytes."""
return SHA256.new(Random.get_random_bytes(32)).digest()
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", 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!)
for i, name in enumerate(r.lrange("%s:names" % session, 0, -1)):
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", challenge
if True: # challenges have been created
r.hset(session, "state", "testchallenge")
r.lpush("pending-testchallenge", session)
else:
r.lpush("pending-makechallenge", session)
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", session
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
# session too often.
# Conceivably, this could wait until the client announces
# 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.
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", 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", challenge, "is not satisfied and not failed"
if challtype == 0: # DomainValidateSNI
if debug: print "\tbeginning dvsni test"
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:
if debug: print "\tsucceeded"
r.hset(challenge, "satisfied", True)
else:
if debug: print "\tfailed"
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).
print "\tall satisfied, going to issue", session
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)
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 session", session
r.lrem("pending-requests", session)
return
csr = r.hget(session, "csr")
cert = CSR.issue(csr)
r.hset(session, "cert", cert)
if cert: # once issuing cert succeeded
if debug: print "issued for", session
r.hset(session, "state", "done")
r.lpush("pending-done", session)
else: # should not be reached in deployed version
if debug: print "issuing for", session, "failed"
r.lpush("pending-issue", session)
while True:
if clean_shutdown: break
session = r.rpop("pending-makechallenge")
if session:
if debug: print "going to makechallenge for", session
makechallenge(session)
session = None
else: session = r.rpop("pending-testchallenge")
if session:
if debug: print "going to testchallenge for", session
testchallenge(session)
session = None
else: session = r.rpop("pending-issue")
if session:
if debug: print "going to issue for", session
issue(session)
session = None
else: time.sleep(2)
# This daemon doesn't currently act on pending-done sessions.