From 90f4b4daebccf1ea5cfefc745fd5cd63c61658bf Mon Sep 17 00:00:00 2001 From: Seth Schoen Date: Thu, 19 Jul 2012 23:19:39 -0700 Subject: [PATCH] move configuratoin parameters into config file; add extra sanity checks --- server-ca/CONFIG.py | 28 ++++++++++++++++++++++++++++ server-ca/CSR.py | 4 +++- server-ca/chocolate.py | 33 +++++++++++++++++++-------------- 3 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 server-ca/CONFIG.py diff --git a/server-ca/CONFIG.py b/server-ca/CONFIG.py new file mode 100644 index 000000000..c28e97626 --- /dev/null +++ b/server-ca/CONFIG.py @@ -0,0 +1,28 @@ +# The name that the server expects to be referred to by. +chocolate_server_name = "ca.theobroma.info" + +# The shortest length in bits of an acceptable RSA modulus. +min_keysize = 2048 + +# The number of bits of hashcash that a client must provide with +# a new request. +difficulty = 23 + +# The number of seconds that the server asks the client to wait, at +# a time, when a request is still being processed. +polldelay = 4 + +# The maximum number of subject names in a request. +max_names = 20 + +# The maximum size in bytes of a CSR. +max_csr_size = 20480 + +# The expiry times of sessions, challenges, and hashcash, in seconds. +maximum_session_age = 100 +maximum_challenge_age = 600 +hashcash_expiry = 60*60 + +# Extra names that the CA refuses to issue for, apart from those in +# the blacklist table in the database. +extra_name_blacklist = ["eff.org", "www.eff.org"] diff --git a/server-ca/CSR.py b/server-ca/CSR.py index adea1d18f..d3542daca 100644 --- a/server-ca/CSR.py +++ b/server-ca/CSR.py @@ -14,6 +14,8 @@ import hashlib import blacklists # we can use temp() to get tempfiles to pass to OpenSSL subprocesses. +from CONFIG import min_key_size + forbidden_moduli = blacklists.forbidden_moduli() forbidden_names = blacklists.forbidden_names() @@ -45,7 +47,7 @@ def goodkey(key): """Does this public key comply with our CA policy?""" key = str(key) bits = modulusbits(key) - if bits and bits >= 2000 and not blacklisted(key): + if bits and bits >= min_key_size and not blacklisted(key): return True else: return False diff --git a/server-ca/chocolate.py b/server-ca/chocolate.py index 4a65e99e9..18eb4ab16 100755 --- a/server-ca/chocolate.py +++ b/server-ca/chocolate.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import web, redis, time, binascii +import web, redis, time, binascii, re import CSR import hashcash from CSR import M2Crypto @@ -8,11 +8,9 @@ from Crypto import Random from chocolate_protocol_pb2 import chocolatemessage from google.protobuf.message import DecodeError -MaximumSessionAge = 100 # seconds, to demonstrate session timeout -MaximumChallengeAge = 600 # to demonstrate challenge timeout -HashcashExpiry = 60*60 - -difficulty = 23 # bits of hashcash required with new requests +from CONFIG import chocolate_server_name, min_keysize, difficulty, polldelay +from CONFIG import max_names, max_csr_size, maximum_session_age +from CONFIG import maximum_challenge_age, hashcash_expiry, extra_name_blacklist try: chocolate_server_name = open("SERVERNAME").read().rstrip() @@ -36,10 +34,11 @@ def safe(what, s): return False base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" csr_ok = base64 + " =-" -# if what == "nonce": -# return s.isalnum() if what == "recipient" or what == "hostname": - return all(c.isalnum() or c in "-." for c in s) + # This rejects domain names which don't contain ".". Although there + # are some of these which are valid Internet FQDNs, none of them + # should be subjects or recipients of Chocolate signing requests. + return re.match("^[A-Za-z0-9][A-Za-z0-9-]*(\.[A-Za-z0-9][A-Za-z0-9-]*)+$", s) is not None elif what == "csr": return all(all(c in csr_ok for c in line) for line in s.split("\n")) # Note that this implies CSRs must have LF for end-of-line, not CRLF @@ -136,7 +135,7 @@ class session(object): def check_hashcash(self, h): """Is the hashcash string h valid for a request to this server?""" if hashcash.check(stamp=h, resource=chocolate_server_name, \ - bits=difficulty, check_expiration=HashcashExpiry): + 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) @@ -186,7 +185,7 @@ class session(object): if not (self.exists() and self.live()): # Don't need to, or can't, kill nonexistent/already dead session r.failure.cause = r.StaleRequest - elif self.age() > MaximumSessionAge: + elif self.age() > maximum_session_age: # TODO: Sessions in state "done" should probably not be killed by timeout # because they have already resulted in issuance of a cert and no further # issuance can occur. At least, their timeout should probably be extended @@ -236,6 +235,9 @@ class session(object): if time.time() - timestamp > 100: self.die(r, r.BadRequest, uri="https://ca.example.com/failures/past") return + if len(csr) > max_csr_size: + self.die(r, r.BadCSR, uri="https://ca.example.com/failures/longcsr") + return if not CSR.parse(csr): self.die(r, r.BadCSR) return @@ -250,8 +252,11 @@ class session(object): if len(names) == 0: self.die(r, r.BadCSR) return + if len(names) > max_names: + self.die(r, r.BadCSR, uri="https://ca.example.com/failures/toomanynames") + return for san in names: # includes CN as well as SANs - if not safe("hostname", san) or not CSR.can_sign(san): + if not safe("hostname", san) or not CSR.can_sign(san) or san in extra_name_blacklist: # TODO: Is there a problem including client-supplied data in the URL? self.die(r, r.CannotIssueThatName, uri="https://ca.example.com/failures/name?%s" % san) return @@ -262,7 +267,7 @@ class session(object): # do what the daemon does, and then return the challenges instead # of returning proceed. r.proceed.timestamp = int(time.time()) - r.proceed.polldelay = 4 + r.proceed.polldelay = polldelay def handleexistingsession(self, m, r): if m.request.IsInitialized(): @@ -278,7 +283,7 @@ class session(object): # If we're in makechallenge or issue, tell the client to come back later. if state == "makechallenge" or state == "issue": r.proceed.timestamp = int(time.time()) - r.proceed.polldelay = 4 + r.proceed.polldelay = polldelay return # If we're in testchallenge, tell the client about the challenges and their # current status.