[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[minion-cvs] Implement and test server descriptors.
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv27794/lib/mixminion
Modified Files:
Config.py Crypto.py ServerInfo.py test.py
Log Message:
Implement and test server descriptors.
Config:
- Add functionality for allow/deny rules.
- Add 'restricted' format for descriptors
- Add fast path for assumed-valid files
- Make 'Host' sections of config optional
- Add more key-management and descriptor-generation fields to
server config.
Crypto:
- Add wrappers for PEM
ServerInfo:
- Implement and debug server descriptors
test:
- Tests for above functionality
- Tests for logs
crypt.c:
- Change generate_cert to take a time range instead of a number
of days.
tls.c:
- Remove stale XXXX comment.
Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -u -d -r1.5 -r1.6
--- Config.py 26 Jul 2002 20:52:17 -0000 1.5
+++ Config.py 28 Jul 2002 22:42:33 -0000 1.6
@@ -32,6 +32,19 @@
[Section2]
Key5 value5
value5 value5 value5
+
+ We also specify a 'restricted' format in which blank lines,
+ comments, line continuations, and entry formats other than 'key: value'
+ are forbidden. Example:
+
+ [Section1]
+ Key1: Value1
+ Key2: Value2
+ Key3: Value3
+ [Section2]
+ Key4: Value4
+
+ The restricted format is used for server descriptors.
"""
__all__ = [ 'getConfig', 'loadConfig', 'addHook' ]
@@ -84,7 +97,7 @@
# This isn't a method of _Config, since we want to be able to call
# it before we read the configuration file.
_CONFIG_HOOKS.append(hook)
-
+
#----------------------------------------------------------------------
class ConfigError(MixError):
@@ -104,7 +117,7 @@
def _parseSeverity(severity):
"""Validation function. Converts a config value to a log severity.
- Raises ConfigError on failure."""
+ Raises ConfigError on failure."""
s = severity.strip().upper()
if not mixminion.Common._SEVERITIES.has_key(s):
raise ConfigError("Invalid log level %r" % (severity))
@@ -112,7 +125,7 @@
def _parseServerMode(mode):
"""Validation function. Converts a config value to a server mode
- (one of 'relay' or 'local'). Raises ConfigError on failure."""
+ (one of 'relay' or 'local'). Raises ConfigError on failure."""
s = mode.strip().lower()
if s not in ('relay', 'local'):
raise ConfigError("Server mode must be 'Relay' or 'Local'")
@@ -131,14 +144,14 @@
'day': 60*60*24,
'week': 60*60*24*7,
'mon': 60*60*24*30,
- 'month': 60*60*24*30, # These last two aren't quite right, but we
+ 'month': 60*60*24*30, # These last two aren't quite right, but we
'year': 60*60*24*365, # don't need exactness.
}
_abbrev_units = { 'sec' : 'second', 'min': 'minute', 'mon': 'month' }
def _parseInterval(interval):
"""Validation function. Converts a config value to an interval of time,
in the format (number of units, name of unit, total number of seconds).
- Raises ConfigError on failure."""
+ Raises ConfigError on failure."""
inter = interval.strip().lower()
m = _interval_re.match(inter)
if not m:
@@ -150,7 +163,7 @@
def _parseInt(integer):
"""Validation function. Converts a config value to an int.
- Raises ConfigError on failure."""
+ Raises ConfigError on failure."""
i = integer.strip().lower()
try:
return int(i)
@@ -159,7 +172,7 @@
def _parseIP(ip):
"""Validation function. Converts a config value to an IP address.
- Raises ConfigError on failure."""
+ Raises ConfigError on failure."""
i = ip.strip().lower()
try:
f = mixminion.Packet._packIP(i)
@@ -168,9 +181,52 @@
return i
+_address_set_re = re.compile(r'''(\d+\.\d+\.\d+\.\d+|\*)
+ \s*
+ (?:/\s*(\d+\.\d+\.\d+\.\d+))?\s*
+ (?:(\d+)\s*
+ (?:-\s*(\d+))?
+ )?''',re.X)
+def _parseAddressSet_allow(s, allowMode=1):
+ """Validation function. Converts an address set string of the form
+ IP/mask port-port into a tuple of (IP, Mask, Portmin, Portmax).
+ Raises ConfigError on failure."""
+ s = s.strip()
+ m = _address_set_re.match(s)
+ if not m:
+ raise ConfigError("Misformatted address rule %r", s)
+ ip, mask, port, porthi = m.groups()
+ if ip == '*':
+ if mask != None:
+ raise ConfigError("Misformatted address rule %r", s)
+ ip,mask = '0.0.0.0','0.0.0.0'
+ else:
+ ip = _parseIP(ip)
+ if mask:
+ mask = _parseIP(mask)
+ else:
+ mask = "255.255.255.255"
+ if port:
+ port = _parseInt(port)
+ if porthi:
+ porthi = _parseInt(porthi)
+ else:
+ porthi = port
+ if not 1 <= port <= porthi <= 65535:
+ raise ConfigError("Invalid port range %s-%s" %(port,porthi))
+ elif allowMode:
+ port = porthi = 48099
+ else:
+ port, porthi = 0, 65535
+
+ return (ip, mask, port, porthi)
+
+def _parseAddressSet_deny(s):
+ return _parseAddressSet_allow(s,0)
+
def _parseCommand(command):
"""Validation function. Converts a config value to a shell command of
- the form (fname, optionslist). Raises ConfigError on failure."""
+ the form (fname, optionslist). Raises ConfigError on failure."""
c = command.strip().lower().split()
if not c:
raise ConfigError("Invalid command %r" %command)
@@ -187,13 +243,13 @@
c = os.path.join(p, cmd)
if os.path.exists(c):
return c, opts
-
+
raise ConfigError("No match found for command %r" %cmd)
_allChars = "".join(map(chr, range(256)))
def _parseBase64(s,_hexmode=0):
"""Validation function. Converts a base-64 encoded config value into
- its original. Raises ConfigError on failure."""
+ its original. Raises ConfigError on failure."""
s = s.translate(_allChars, " \t\v\n")
try:
if _hexmode:
@@ -205,7 +261,7 @@
def _parseHex(s):
"""Validation function. Converts a hex-64 encoded config value into
- its original. Raises ConfigError on failure."""
+ its original. Raises ConfigError on failure."""
return _parseBase64(s,1)
def _parsePublicKey(s):
@@ -244,7 +300,7 @@
(0 <= mm < 60) and (0 <= ss <= 61)):
raise ConfigError("Invalid %s %r" % (("date","time")[_timeMode],s))
-
+
# we set the DST flag to zero so that subtracting time.timezone always
# gives us gmt.
return time.mktime((yyyy,MM,dd,hh,mm,ss,0,0,0))-time.timezone
@@ -272,7 +328,7 @@
'MORE': The line is a continuation line of an entry. VALUE is the
contents of the line.
"""
-
+
if line == '':
return None, None
@@ -298,9 +354,8 @@
def _readConfigFile(contents, restrict=0):
"""Helper function. Given the string contents of a configuration
-
- Returns a list of (SECTION-NAME, SECTION) tuples, where each
- SECTION is a list of (KEY, VALUE, LINENO) tuples.
+ file, returns a list of (SECTION-NAME, SECTION) tuples, where
+ each SECTION is a list of (KEY, VALUE, LINENO) tuples.
Throws ConfigError if the file is malformatted.
"""
@@ -308,7 +363,7 @@
curSection = None
lineno = 0
lastKey = None
-
+
fileLines = contents.split("\n")
if fileLines[-1] == '':
del fileLines[-1]
@@ -358,7 +413,7 @@
lines.append(ind+v)
lines.append("") # so the last line ends with \n
return "\n".join(lines)
-
+
class _ConfigFile:
"""Base class to parse, validate, and represent configuration files.
"""
@@ -368,14 +423,14 @@
# _sectionEntries: A map from secname->[ (key, value) ] inorder.
# _sectionNames: An inorder list of secnames.
#
- # Set by a subclass:
+ # Fields to be set by a subclass:
# _syntax is map from sec->{key:
# (ALLOW/REQUIRE/ALLOW*/REQUIRE*,
# parseFn,
# default, ) }
# _restrictFormat is 1/0: do we allow full RFC822ness, or do
# we insist on a tight data format?
-
+
## Validation rules:
# A key without a corresponding entry in _syntax gives an error.
# A section without a corresponding entry is ignored.
@@ -388,12 +443,17 @@
# the entry's value will be set to default. Otherwise, the value
# will be set to None.
- def __init__(self, fname=None, string=None):
- """Create a new _ConfigFile. If fname is set, read from
- fname. If string is set, parse string. """
- assert fname is None or string is None
- self.fname = fname
- if fname:
+ def __init__(self, filename=None, string=None, assumeValid=0):
+ """Create a new _ConfigFile. If <filename> is set, read from
+ a corresponding file. If <string> is set, parse its contents.
+
+ If <assumeValid> is true, skip all unnecessary validation
+ steps. (Use this to load a file that's already been checked as
+ valid.)"""
+ assert filename is None or string is None
+ self.assumeValid = assumeValid
+ self.fname = filename
+ if filename:
self.reload()
elif string:
cs = StringIO(string)
@@ -401,7 +461,7 @@
self.__reload(cs)
finally:
cs.close()
- else:
+ else:
self.clear()
def clear(self):
@@ -409,7 +469,7 @@
self._sections = {}
self._sectionEntries = {}
self._sectionNames = []
-
+
def reload(self):
"""Reload this _ConfigFile object from disk. If the object is no
longer present and correctly formatted, raise an error, but leave
@@ -441,10 +501,10 @@
if self_sections.has_key(secName):
raise ConfigError("Duplicate section [%s]" %secName)
-
+
section = {}
sectionEntries = []
- entryLines = []
+ entryLines = []
self_sections[secName] = section
self_sectionEntries[secName] = sectionEntries
sectionEntryLines[secName] = entryLines
@@ -508,7 +568,8 @@
assert rule == 'ALLOW*'
section[k] = map(parseFn,default)
- # Check for missing required sections.
+ # Check for missing required sections, setting any missing
+ # allowed sections to {}.
for secName, secConfig in self._syntax.items():
secRule = secConfig.get('__SECTION__', ('ALLOW',None,None))
if (secRule[0] == 'REQUIRE'
@@ -517,15 +578,18 @@
elif not self_sections.has_key(secName):
self_sections[secName] = {}
self_sectionEntries[secName] = {}
-
- # Make sure that sectionEntries is correct (sanity check)
- for s in self_sectionNames:
- for k,v in self_sectionEntries[s]:
- assert v == self_sections[s][k] or v in self_sections[s][k]
-
- # Call our validation hook.
- self.validate(self_sections, self_sectionEntries, sectionEntryLines,
- fileContents)
+
+ if not self.assumeValid:
+ # Make sure that sectionEntries is correct (sanity check)
+ #XXXX remove this
+ for s in self_sectionNames:
+ for k,v in self_sectionEntries[s]:
+ assert (v==self_sections[s][k] or
+ v in self_sections[s][k])
+
+ # Call our validation hook.
+ self.validate(self_sections, self_sectionEntries,
+ sectionEntryLines, fileContents)
self._sections = self_sections
self._sectionEntries = self_sectionEntries
@@ -562,13 +626,13 @@
for k,v in self._sectionEntries[s]:
lines.append(_formatEntry(k,v))
lines.append("\n")
-
+
return "".join(lines)
class ClientConfig(_ConfigFile):
_restrictFormat = 0
_syntax = {
- 'Host' : { '__SECTION__' : ('REQUIRE', None, None),
+ 'Host' : { '__SECTION__' : ('ALLOW', None, None),
'ShredCommand': ('ALLOW', _parseCommand, None),
'EntropySource': ('ALLOW', None, "/dev/urandom"),
},
@@ -591,7 +655,7 @@
class ServerConfig(_ConfigFile):
_restrictFormat = 0
_syntax = {
- 'Host' : ClientConfig._syntax['Host'],
+ 'Host' : ClientConfig._syntax['Host'],
'Server' : { '__SECTION__' : ('REQUIRE', None, None),
'Homedir' : ('ALLOW', None, "/var/spool/minion"),
'LogFile' : ('ALLOW', None, None),
@@ -600,23 +664,28 @@
'EncryptIdentityKey' : ('REQUIRE', _parseBoolean, "yes"),
'PublicKeyLifetime' : ('REQUIRE', _parseInterval,
"30 days"),
+ 'PublicKeySloppiness': ('ALLOW', _parseInterval,
+ "5 minutes"),
'EncryptPublicKey' : ('REQUIRE', _parseBoolean, "no"),
'Mode' : ('REQUIRE', _parseServerMode, "local"),
+ 'Nickname': ('ALLOW', None, None),
+ 'Contact-Email': ('ALLOW', None, None),
+ 'Comments': ('ALLOW', None, None),
},
'DirectoryServers' : { 'ServerURL' : ('ALLOW*', None, None),
'Publish' : ('ALLOW', _parseBoolean, "no"),
'MaxSkew' : ('ALLOW', _parseInterval,
- "10 minutes",) },
+ "10 minutes",) },
'Incoming/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
'IP' : ('ALLOW', _parseIP, None),
'Port' : ('ALLOW', _parseInt, "48099"),
- 'Allow' : ('ALLOW*', None, None),
- 'Deny' : ('ALLOW*', None, None) },
+ 'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
+ 'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },
'Outgoing/MMTP' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
- 'Allow' : ('ALLOW*', None, None),
- 'Deny' : ('ALLOW', None, None) },
- 'Delivery/MBox' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
- 'AddressFile' : ('REQUIRE', None, None),
+ 'Allow' : ('ALLOW*', _parseAddressSet_allow, None),
+ 'Deny' : ('ALLOW*', _parseAddressSet_deny, None) },
+ 'Delivery/MBOX' : { 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
+ 'AddressFile' : ('ALLOW', None, None),
'Command' : ('ALLOW', _parseCommand, "sendmail") },
}
# XXXX Missing: Queue-Size / Queue config options
@@ -628,3 +697,10 @@
#XXXX write this.
pass
+## if sections['Server']['PublicKeyLifeTime'][2] < 24*60*60:
+## raise ConfigError("PublicKeyLifetime must be at least 1 day.")
+## elif sections['Server']['PublicKeyLifeTime'][2] % (24*60*60) > 30:
+## getLog().warn("PublicKeyLifetime rounded to the nearest day")
+## nDays = sections[60*60*24]
+
+
Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- Crypto.py 26 Jul 2002 20:52:17 -0000 1.10
+++ Crypto.py 28 Jul 2002 22:42:33 -0000 1.11
@@ -29,7 +29,6 @@
CryptoError = _ml.CryptoError
generate_cert = _ml.generate_cert
-PEM_read_key = _ml.rsa_PEM_read_key
# Number of bytes in an AES key.
AES_KEY_LEN = 128 >> 3
@@ -188,6 +187,27 @@
"""Reads an ASN1 representation of a public key from external storage."""
return _ml.rsa_decode_key(s,1)
+def pk_PEM_save(rsa, filename, password=None):
+ """Save a PEM-encoded private key to a file. If <password> is provided,
+ encrypt the key using the password."""
+ f = open(filename, 'w')
+ if password:
+ rsa.PEM_write_key(f, 0, password)
+ else:
+ rsa.PEM_write_key(f, 0)
+ f.close()
+
+def pk_PEM_load(filename, password=None):
+ """Load a PEM-encoded private key from a file. If <password> is provided,
+ decrypt the key using the password."""
+ f = open(filename, 'r')
+ if password:
+ rsa = _ml.rsa_PEM_read_key(f, 0, password)
+ else:
+ rsa = _ml.rsa_PEM_read_key(f, 0)
+ f.close()
+ return rsa
+
#----------------------------------------------------------------------
# OAEP Functionality
#
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.6
retrieving revision 1.7
diff -u -d -r1.6 -r1.7
--- ServerInfo.py 26 Jul 2002 20:52:17 -0000 1.6
+++ ServerInfo.py 28 Jul 2002 22:42:33 -0000 1.7
@@ -3,14 +3,16 @@
"""mixminion.ServerInfo
- Data structures to represent a server's information, and functions to
- martial and unmarshal it.
-
+ Implementation of server descriptors (as described in the mixminion
+ spec). Includes logic to parse, validate, and generate server
+ descriptors.
"""
__all__ = [ 'ServerInfo' ]
import time
+import os
+import binascii
from mixminion.Modules import SWAP_FWD_TYPE, FWD_TYPE
from mixminion.Packet import IPV4Info
@@ -19,20 +21,27 @@
ConfigError = mixminion.Config.ConfigError
-# tmp variable to make this easier to spell.
-C = mixminion.Config
-
+# Longest allowed Nickname
MAX_NICKNAME = 128
+# Longest allowed Contact email
MAX_CONTACT = 256
-MAX_COMMENT = 1024
+# Longest allowed Comments field
+MAX_COMMENTS = 1024
+# Shortest permissible identity key
MIN_IDENTITY_BYTES = 2048 >> 3
+# Longest permissible identity key
MAX_IDENTITY_BYTES = 4096 >> 3
+# Length of packet key
PACKET_KEY_BYTES = 1024 >> 3
+# tmp alias to make this easier to spell.
+C = mixminion.Config
class ServerInfo(mixminion.Config._ConfigFile):
+ """A ServerInfo object holds a parsed server descriptor."""
_restrictFormat = 1
_syntax = {
"Server" : { "__SECTION__": ("REQUIRE", None, None),
+ "Descriptor-Version": ("REQUIRE", None, None),
"IP": ("REQUIRE", C._parseIP, None),
"Nickname": ("REQUIRE", None, None),
"Identity": ("REQUIRE", C._parsePublicKey, None),
@@ -50,10 +59,14 @@
"Port": ("REQUIRE", C._parseInt, None),
"Key-Digest": ("REQUIRE", C._parseBase64, None),
"Protocols": ("REQUIRE", None, None),
+ "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
+ "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
},
"Modules/MMTP" : {
"Version": ("REQUIRE", None, None),
"Protocols": ("REQUIRE", None, None),
+ "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
+ "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
},
"Modules/MBOX" : {
"Version": ("REQUIRE", None, None),
@@ -63,27 +76,26 @@
}
}
- def __init__(self, fname, string):
- mixminion.Config._ConfigFile.__init__(self, fname, string)
+ def __init__(self, fname=None, string=None, assumeValid=0):
+ mixminion.Config._ConfigFile.__init__(self, fname, string, assumeValid)
def validate(self, sections, entries, lines, contents):
####
# Check 'Server' section.
-
server = sections['Server']
if server['Descriptor-Version'] != '1.0':
raise ConfigError("Unrecognized descriptor version")
if len(server['Nickname']) > MAX_NICKNAME:
raise ConfigError("Nickname too long")
- identityKey = server['Identity-Key']
+ identityKey = server['Identity']
identityBytes = identityKey.get_modulus_bytes()
if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):
raise ConfigError("Invalid length on identity key")
if server['Valid-Until'] <= server['Valid-After']:
raise ConfigError("Server is never valid")
- if len(server['Contact']) > MAX_CONTACT:
+ if server['Contact'] and len(server['Contact']) > MAX_CONTACT:
raise ConfigError("Contact too long")
- if len(sever['Comments']) > MAX_COMMENTS:
+ if server['Comments'] and len(server['Comments']) > MAX_COMMENTS:
raise ConfigError("Comments too long")
packetKeyBytes = server['Packet-Key'].get_modulus_bytes()
if packetKeyBytes != PACKET_KEY_BYTES:
@@ -95,7 +107,6 @@
if digest != server['Digest']:
raise ConfigError("Invalid digest")
- signature = server['']
if digest != mixminion.Crypto.pk_check_signature(server['Signature'],
identityKey):
raise ConfigError("Invalid signature")
@@ -104,10 +115,10 @@
def getAddr(self):
return self['Server']['IP']
-
+
def getPort(self):
return self['Incoming/MMTP']['Port']
-
+
def getPacketKey(self):
return self['Server']['Packet-Key']
@@ -122,86 +133,141 @@
#----------------------------------------------------------------------
# This should go in a different file.
class ServerKeys:
- "XXXX"
+ """A set of expirable keys for use by a server.
+
+ A server has one long-lived identity key, and two short-lived
+ temporary keys: one for subheader encryption and one for MMTP. The
+ subheader (or 'packet') key has an associated hashlog, and the
+ MMTP key has an associated self-signed certificate.
+
+ Whether we publish or not, we always generate a server descriptor
+ to store the keys' lifetimes.
+
+ When we create a new ServerKeys object, the associated keys are not
+ read from disk unil the object's load method is called."""
def __init__(self, keyroot, keyname, hashroot):
- self.keydir = os.path.join(keyroot, "key_"+keyname)
+ keydir = self.keydir = os.path.join(keyroot, "key_"+keyname)
self.hashlogFile = os.path.join(hashroot, "hash_"+keyname)
self.packetKeyFile = os.path.join(keydir, "mix.key")
self.mmtpKeyFile = os.path.join(keydir, "mmtp.key")
self.certFile = os.path.join(keydir, "mmtp.cert")
-
+ self.descFile = os.path.join(keydir, "ServerDesc")
+ if not os.path.exists(keydir):
+ os.mkdir(keydir, 0700)
+
def load(self, password=None):
- r = mixminion.Crypto.PEM_read_key
- if password:
- self.packetKey = r(self.packetKeyFile,0,password)
- self.mmtpKey = r(self.mmtpKeyFile,0,password)
- else:
- self.packetKey = r(self.packetKeyFile,0)
- self.mmtpKey = r(self.mmtpKeyFile,0)
-
- def save(self, pasword=None):
- if password:
- self.packetKey.PEM_write_key(self.packetKeyFile,0,password)
- self.mmtpKey.PEM_write_key(self.mmtpKeyFile,0,password)
- else:
- self.packetKey.PEM_write_key(self.packetKeyFile,0)
- self.mmtpKey.PEM_write_key(self.mmtpKeyFile,0)
-
+ "Read this set of keys from disk."
+ self.packetKey = mixminion.Crypto.pk_PEM_load(self.packetKeyFile,
+ password)
+ self.mmtpKey = mixminion.Crypto.pk_PEM_load(self.mmtpKeyFile,
+ password)
+ def save(self, password=None):
+ "Save this set of keys to disk."
+ mixminion.Crypto.pk_PEM_save(self.packetKey, self.packetKeyFile,
+ password)
+ mixminion.Crypto.pk_PEM_save(self.mmtpKey, self.mmtpKeyFile,
+ password)
def getCertFileName(self): return self.certFile
def getHashLogFileName(self): return self.hashlogFile
+ def getDescriptorFileName(self): return self.descFile
def getPacketKey(self): return self.packetKey
def getMMTPKey(self): return self.mmtpKey
- def getMMTPKeyID(self):
- return sha1(self.mmtpKey.encode_key(1))
+ def getMMTPKeyID(self):
+ "Return the sha1 hash of the asn1 encoding of the MMTP public key"
+ return mixminion.Crypto.sha1(self.mmtpKey.encode_key(1))
def _base64(s):
+ "Helper function: returns a one-line base64 encoding of a given string."
return binascii.b2a_base64(s).replace("\n","")
def _time(t):
+ """Helper function: turns a time (in seconds) into the format used by
+ Server descriptors"""
gmt = time.gmtime(t)
- return "%02d/%02s/%04d %02d:%02d:%02d" % (
+ return "%02d/%02d/%04d %02d:%02d:%02d" % (
gmt[2],gmt[1],gmt[0], gmt[3],gmt[4],gmt[5])
def _date(t):
- gmt = time.gmtime(t+1)
- return "%02d/%02s/%04d" % (gmt[2],gmt[1],gmt[0])
+ """Helper function: turns a time (in seconds) into a date in the format
+ used by server descriptors"""
+ gmt = time.gmtime(t+1) # Add 1 to make sure we round down.
+ return "%02d/%02d/%04d" % (gmt[2],gmt[1],gmt[0])
+
+def _rule(allow, (ip, mask, portmin, portmax)):
+ if mask == '0.0.0.0':
+ ip="*"
+ mask=""
+ elif mask == "255.255.255.255":
+ mask = ""
+ else:
+ mask = "/%s" % mask
+
+ if portmin==portmax==48099 and allow:
+ ports = ""
+ elif portmin == 0 and portmax == 65535 and not allow:
+ ports = ""
+ elif portmin == portmax:
+ ports = " %s" % portmin
+ else:
+ ports = " %s-%s" % (portmin, portmax)
+
+ return "%s%s%s\n" % (ip,mask,ports)
+
+# We have our X509 certificate set to expire a bit after public key does,
+# so that slightly-skewed clients don't incorrectly give up while trying to
+# connect to us.
+CERTIFICATE_EXPIRY_SLOPPINESS = 5*60
+
+def generateServerDescriptorAndKeys(config, identityKey, keydir, keyname,
+ hashdir,
+ validAt=None):
+ """Generate and sign a new server descriptor, and generate all the keys to
+ go with it.
+
+ identityKey -- This server's private identity key
+ keydir -- The root directory for storing key sets.
+ keyname -- The name of this new key set within keydir
+ validAt -- The starting time (in seconds) for this key's lifetime."""
-def generateNewServerInfoAndKeys(config, identityKey, keydir, keyname):
packetKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
mmtpKey = mixminion.Crypto.pk_generate(PACKET_KEY_BYTES*8)
-
- serverKeys = ServerKeys(keydir, keyname)
+
+ serverKeys = ServerKeys(keydir, keyname, hashdir)
serverKeys.packetKey = packetKey
serverKeys.mmtpKey = mmtpKey
serverKeys.save()
- nickname = "XXXX" #XXXX"
- contact = "XXXX"
- comment = "XXXX"
- validAt = time.time() #XXXX
- validUntil = time.time()+365*24*60*60 #XXXX
- lifespan = ceilDiv(validUntil-validAt , 24*60*60)#XXXX
-
+ allowIncoming = config['Incoming/MMTP'].get('Enabled', 0)
+
+ nickname = config['Server']['Nickname']
+ if not nickname:
+ nickname = config['Incoming/MMTP'].get('IP', "<Unnamed server>")
+ contact = config['Server']['Contact-Email']
+ comments = config['Server']['Comments']
+ if not validAt:
+ validAt = time.time()
+
+ validUntil = validAt + config['Server']['PublicKeyLifetime'][2]
+ certStarts = validAt - CERTIFICATE_EXPIRY_SLOPPINESS
+ certEnds = validUntil + CERTIFICATE_EXPIRY_SLOPPINESS + \
+ config['Server']['PublicKeySloppiness'][2]
+
mixminion.Crypto.generate_cert(serverKeys.getCertFileName(),
mmtpKey,
- lifespan,
- "MMTP certificate for %s" %nickname)
-
- if not config['Server']['Incoming/MMTP']:
- # Don't generate a serverInfo if we don't allow connections in.
- return
+ "MMTP certificate for %s" %nickname,
+ certStarts, certEnds)
fields = {
- "IP": config['Incoming/MMTP']['IP'],
- "Port": config['Incoming/MMTP']['Port'],
+ "IP": config['Incoming/MMTP'].get('IP', "0.0.0.0"),
+ "Port": config['Incoming/MMTP'].get('Port', 0),
"Nickname": nickname,
- "Identity":
+ "Identity":
_base64(mixminion.Crypto.pk_encode_public_key(identityKey)),
"Published": _time(time.time()),
"ValidAfter": _date(validAt),
"ValidUntil": _date(validUntil),
"PacketKey":
- _base64(mixminion.Crypto.pk_encode_public_key(publicKey)),
+ _base64(mixminion.Crypto.pk_encode_public_key(packetKey)),
"KeyID":
_base64(serverKeys.getMMTPKeyID()),
}
@@ -210,7 +276,7 @@
[Server]
Descriptor-Version: 1.0
IP: %(IP)s
- Port: %(Port)s
+ Nickname: %(Nickname)s
Identity: %(Identity)s
Digest:
Signature:
@@ -218,69 +284,93 @@
Valid-After: %(ValidAfter)s
Valid-Until: %(ValidUntil)s
Packet-Key: %(PacketKey)s
- """ %(fields)
+ """ % fields
if contact:
- info += "Contact %s\n"%contact
- if comment:
- info += "Contact %s\n"%comment
-
- if ALLOW_INCOMING_MMTP: #XXXX
+ info += "Contact: %s\n"%contact
+ if comments:
+ info += "Comments: %s\n"%comments
+
+ if config["Incoming/MMTP"].get("Enabled", 0):
info += """\
[Incoming/MMTP]
Version: 1.0
Port: %(Port)s
Key-Digest: %(KeyID)s
Protocols: 1.0
- """
- if ALLOW_OUTGOING_MMTP: #XXXX
+ """ % fields
+ for k,v in config.getSectionItems("Incoming/MMTP"):
+ if k not in ("Allow", "Deny"):
+ continue
+ info += "%s: %s" % (k, _rule(k=='Allow',v))
+
+ if config["Outgoing/MMTP"].get("Enabled", 0):
info += """\
[Modules/MMTP]
Version: 1.0
Protocols: 1.0
"""
for k,v in config.getSectionItems("Outgoing/MMTP"):
- # XXXX write the rule
- pass
- if ALLOW_DELIVERY_MBOX: #XXXX
+ if k not in ("Allow", "Deny"):
+ continue
+ info += "%s: %s" % (k, _rule(k=='Allow',v))
+
+ if config["Delivery/MBOX"].get("Enabled", 0):
info += """\
[Modules/MBOX]
Version: 1.0
"""
-
+
# Remove extra (leading) whitespace.
lines = [ line.strip() for line in info.split("\n") ]
# Remove empty lines
lines = filter(None, lines)
+ # Force a newline at the end of the file, rejoin, and sign.
+ lines.append("")
info = "\n".join(lines)
info = signServerInfo(info, identityKey)
-
- # debug XXXX
+
+ f = open(serverKeys.getDescriptorFileName(), 'w')
+ try:
+ f.write(info)
+ finally:
+ f.close()
+
+ # for debugging XXXX ### Remove this once we're more confident.
ServerInfo(string=info)
return info
-
-#----------------------------------------------------------------------
+#-----------------------b-----------------------------------------------
def getServerInfoDigest(info):
- return _getServerInfoDiggestImpl(info, None)
+ """Calculate the digest of a server descriptor"""
+ return _getServerInfoDigestImpl(info, None)
def signServerInfo(info, rsa):
- return _getServerInfoDiggestImpl(info, rsa)
+ """Sign a server descriptor. <info> should be a well-formed server
+ descriptor, with Digest: and Signature: lines present but with
+ no values."""
+ return _getServerInfoDigestImpl(info, rsa)
def _getServerInfoDigestImpl(info, rsa=None):
+ """Helper method. Calculates the correct digest of a server descriptor
+ (as provided in a string). If rsa is provided, signs the digest and
+ creates a new descriptor. Otherwise just returns the digest."""
+
+ # The algorithm's pretty easy. We just find the Digest and Signature
+ # lines, replace each with an 'Empty' version, and calculate the digest.
infoLines = info.split("\n")
if not infoLines[0] == "[Server]":
- raise ConfigError("Must begin with server section")
+ raise ConfigError("Must begin with server section")
digestLine = None
signatureLine = None
infoLines = info.split("\n")
- for lineno in range(len(infoLines)):
+ for lineNo in range(len(infoLines)):
line = infoLines[lineNo]
if line.startswith("Digest:") and digestLine is None:
digestLine = lineNo
elif line.startswith("Signature:") and signatureLine is None:
signatureLine = lineNo
-
+
assert digestLine is not None and signatureLine is not None
infoLines[digestLine] = 'Digest:'
@@ -288,10 +378,11 @@
info = "\n".join(infoLines)
digest = mixminion.Crypto.sha1(info)
- if pk is None:
+
+ if rsa is None:
return digest
+ # If we got an RSA key, we need to add the digest and signature.
- #### Signature case.
signature = mixminion.Crypto.pk_sign(digest,rsa)
digest = _base64(digest)
signature = binascii.b2a_base64(signature).replace("\n","")
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- test.py 26 Jul 2002 20:52:17 -0000 1.14
+++ test.py 28 Jul 2002 22:42:33 -0000 1.15
@@ -20,6 +20,8 @@
import atexit
import tempfile
import types
+import re
+import binascii
from mixminion.Common import MixError, MixFatalError, MixProtocolError, getLog
@@ -45,7 +47,11 @@
fnames = [fnames]
for fname in fnames:
try:
- os.unlink(fname)
+ if os.path.isdir(fname):
+ try_unlink([os.path.join(fname,f) for f in os.listdir(fname)])
+ os.rmdir(fname)
+ else:
+ os.unlink(fname)
except OSError:
pass
@@ -1342,6 +1348,41 @@
queue2.cleanQueue()
#----------------------------------------------------------------------
+# LOGGING
+class LogTests(unittest.TestCase):
+ def testLogging(self):
+ import cStringIO
+ from mixminion.Common import Log, FileLogTarget, ConsoleLogTarget
+ log = Log("INFO")
+ self.assertEquals(log.getMinSeverity(), "INFO")
+ log.log("WARN", "This message should not appear")
+ buf = cStringIO.StringIO()
+ log.addHandler(ConsoleLogTarget(buf))
+ log.trace("Foo")
+ self.assertEquals(buf.getvalue(), "")
+ log.log("WARN", "Hello%sworld", ", ")
+ self.failUnless(buf.getvalue().endswith(
+ "[WARN] Hello, world\n"))
+ self.failUnless(buf.getvalue().index('\n') == len(buf.getvalue())-1)
+ log.error("All your anonymity are belong to us")
+ self.failUnless(buf.getvalue().endswith(
+ "[ERROR] All your anonymity are belong to us\n"))
+
+ t = tempfile.mktemp("log")
+ t1 = t+"1"
+ unlink_on_exit(t, t1)
+ log.addHandler(FileLogTarget(t))
+ log.info("Abc")
+ log.info("Def")
+ os.rename(t,t1)
+ log.info("Ghi")
+ log.reset()
+ log.info("Klm")
+ log.close()
+ self.assertEquals(open(t).read().count("\n") , 1)
+ self.assertEquals(open(t1).read().count("\n"), 3)
+
+#----------------------------------------------------------------------
# SIGHANDLERS
# XXXX
@@ -1382,7 +1423,8 @@
print "done.]"
pk = _ml.rsa_generate(1024, 65535)
pk.PEM_write_key(open(pkfile, 'w'), 0)
- _ml.generate_cert(certfile, pk, 365, "Testing certificate")
+ _ml.generate_cert(certfile, pk, "Testing certificate",
+ time.time(), time.time()+365*24*60*60)
unlink_on_exit(certfile, pkfile)
pk = _ml.rsa_PEM_read_key(open(pkfile, 'r'), 0)
@@ -1521,7 +1563,6 @@
from mixminion.Config import _ConfigFile, ConfigError, _parseInt
class TestConfigFile(_ConfigFile):
- _restrictFormat = 0
_syntax = { 'Sec1' : {'__SECTION__': ('REQUIRE', None, None),
'Foo': ('REQUIRE', None, None),
'Bar': ('ALLOW', None, "default"),
@@ -1538,7 +1579,8 @@
'IntAMD2': ('ALLOW*', _parseInt, ["5", "2"]),
'IntRS': ('REQUIRE', _parseInt, None) }
}
- def __init__(self, fname=None, string=None):
+ def __init__(self, fname=None, string=None, restrict=0):
+ self._restrictFormat = restrict
_ConfigFile.__init__(self,fname,string)
class ConfigFileTests(unittest.TestCase):
@@ -1627,7 +1669,6 @@
self.assertEquals(f['Sec1']['Bar'], 'bar')
self.assertEquals(f['Sec2']['Quz'], ['99 99', '88 88'])
-
# Test 'reload' operation
file = open(fn, 'w')
file.write(shorterString)
@@ -1637,6 +1678,11 @@
self.assertEquals(f['Sec1']['Bar'], "default")
self.assertEquals(f['Sec2'], {})
+ # Test restricted mode
+ s = "[Sec1]\nFoo: Bar\nBaz: Quux\n[Sec3]\nIntRS: 9\n"
+ f = TCF(string=s, restrict=1)
+ self.assertEquals(f['Sec1']['Foo'], "Bar")
+ self.assertEquals(f['Sec3']['IntRS'], 9)
def testBadFiles(self):
TCF = TestConfigFile
@@ -1654,6 +1700,15 @@
fails("[Sec1]\nFoo 1\n[Sec2]\nBap = 9\n") # Missing require*
fails("[Sec1]\nFoo: Bar\n[Sec3]\nIntRS=Z\n") # Failed validation
+ # now test the restricted format
+ def fails(string, self=self):
+ self.failUnlessRaises(ConfigError, TestConfigFile, None, string, 1)
+ fails("[Sec1]\nFoo=Bar\n")
+ fails("[Sec1]\nFoo Bar\n")
+ fails("[Sec1]\n\nFoo: Bar\n")
+ fails("\n[Sec1]\nFoo: Bar\n")
+ fails("\n[Sec1]\nFoo: Bar\n\n")
+
def testValidationFns(self):
import mixminion.Config as C
@@ -1669,6 +1724,19 @@
self.assertEquals(C._parseInterval("2 houRS"), (2,"hour",7200))
self.assertEquals(C._parseInt("99"), 99)
self.assertEquals(C._parseIP("192.168.0.1"), "192.168.0.1")
+ pa = C._parseAddressSet_allow
+ self.assertEquals(pa("*"), ("0.0.0.0", "0.0.0.0", 48099, 48099))
+ self.assertEquals(pa("192.168.0.1/255.255.0.0"),
+ ("192.168.0.1", "255.255.0.0", 48099, 48099))
+ self.assertEquals(pa("192.168.0.1 / 255.255.0.0 23-99"),
+ ("192.168.0.1", "255.255.0.0", 23, 99))
+ self.assertEquals(pa("192.168.0.1 / 255.255.0.0 23"),
+ ("192.168.0.1", "255.255.0.0", 23, 23))
+ self.assertEquals(pa("192.168.0.1"),
+ ("192.168.0.1", "255.255.255.255", 48099, 48099))
+ self.assertEquals(pa("192.168.0.1",0),
+ ("192.168.0.1", "255.255.255.255", 0, 65535))
+
# XXXX Won't work on Windows.
self.assertEquals(C._parseCommand("ls -l"), ("/bin/ls", ['-l']))
self.assertEquals(C._parseCommand("rm"), ("/bin/rm", []))
@@ -1700,6 +1768,9 @@
fails(C._parseIP, "192.0.0")
fails(C._parseIP, "192.0.0.0.0")
fails(C._parseIP, "A.0.0.0")
+ fails(pa, "1/1")
+ fails(pa, "192.168.0.1 50-40")
+ fails(pa, "192.168.0.1 50-9999999")
fails(C._parseBase64, "Y")
fails(C._parseHex, "Z")
fails(C._parseHex, "A")
@@ -1728,7 +1799,138 @@
except ConfigError, e:
# This is what we expect
pass
-
+
+
+#----------------------------------------------------------------------
+# Server descriptors
+SERVER_CONFIG = """
+[Server]
+EncryptIdentityKey: no
+PublicKeyLifetime: 10 days
+EncryptPublicKey: no
+Mode: relay
+Nickname: The Server
+Contact-Email: a@b.c
+Comments: This is a test of the emergency
+ broadcast system
+
+[Incoming/MMTP]
+Enabled = yes
+IP: 192.168.0.1
+Allow: 192.168.0.16 1-1024
+Deny: 192.168.0.16
+Allow: *
+
+[Outgoing/MMTP]
+Enabled = yes
+Allow: *
+
+[Delivery/MBOX]
+Enabled: yes
+"""
+
+SERVER_CONFIG_SHORT = """
+[Server]
+EncryptIdentityKey: no
+PublicKeyLifetime: 10 days
+EncryptPublicKey: no
+Mode: relay
+"""
+
+
+import mixminion.Config
+import mixminion.ServerInfo
+class ServerInfoTests(unittest.TestCase):
+ def testServerInfoGen(self):
+ d = tempfile.mktemp()
+ conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG)
+ identity = mixminion.Crypto.pk_generate(2048)
+ if not os.path.exists(d):
+ os.mkdir(d, 0700)
+ unlink_on_exit(d)
+ inf = mixminion.ServerInfo.generateServerDescriptorAndKeys(conf,
+ identity,
+ d,
+ "key1",
+ d)
+ info = mixminion.ServerInfo.ServerInfo(string=inf)
+ eq = self.assertEquals
+ eq(info['Server']['Descriptor-Version'], "1.0")
+ eq(info['Server']['IP'], "192.168.0.1")
+ eq(info['Server']['Nickname'], "The Server")
+ self.failUnless(0 <= time.time()-info['Server']['Published'] <= 120)
+ self.failUnless(0 <= time.time()-info['Server']['Valid-After']
+ <= 24*60*60)
+ eq(info['Server']['Valid-Until']-info['Server']['Valid-After'],
+ 10*24*60*60)
+ eq(info['Server']['Contact'], "a@b.c")
+ eq(info['Server']['Comments'],
+ "This is a test of the emergency broadcast system")
+
+ eq(info['Incoming/MMTP']['Version'], "1.0")
+ eq(info['Incoming/MMTP']['Port'], 48099)
+ eq(info['Incoming/MMTP']['Protocols'], "1.0")
+ eq(info['Modules/MMTP']['Version'], "1.0")
+ eq(info['Modules/MMTP']['Protocols'], "1.0")
+ eq(info['Incoming/MMTP']['Allow'], [("192.168.0.16", "255.255.255.255",
+ 1,1024),
+ ("0.0.0.0", "0.0.0.0",
+ 48099, 48099)] )
+ eq(info['Incoming/MMTP']['Deny'], [("192.168.0.16", "255.255.255.255",
+ 0,65535),
+ ])
+ eq(info['Modules/MBOX']['Version'], "1.0")
+
+ # Now make sure everything was saved properly
+ keydir = os.path.join(d, "key_key1")
+ eq(inf, open(os.path.join(keydir, "ServerDesc")).read())
+ keys = mixminion.ServerInfo.ServerKeys(d, "key1", d)
+ packetKey = mixminion.Crypto.pk_PEM_load(
+ os.path.join(keydir, "mix.key"))
+ eq(packetKey.get_public_key(),
+ info['Server']['Packet-Key'].get_public_key())
+ mmtpKey = mixminion.Crypto.pk_PEM_load(
+ os.path.join(keydir, "mmtp.key"))
+ eq(mixminion.Crypto.sha1(mmtpKey.encode_key(1)),
+ info['Incoming/MMTP']['Key-Digest'])
+
+ # Now check the digest and signature
+ identityPK = info['Server']['Identity']
+ pat = re.compile(r'^(Digest:|Signature:).*$', re.M)
+ x = sha1(pat.sub(r'\1', inf))
+
+ eq(info['Server']['Digest'], x)
+ eq(x, mixminion.Crypto.pk_check_signature(info['Server']['Signature'],
+ identityPK))
+
+ # Now with a shorter configuration
+ conf = mixminion.Config.ServerConfig(string=SERVER_CONFIG_SHORT)
+ inf2 = mixminion.ServerInfo.generateServerDescriptorAndKeys(conf,
+ identity,
+ d,
+ "key2",
+ d)
+
+ # Now with a bad signature
+ sig2 = mixminion.Crypto.pk_sign(sha1("Hello"), identity)
+ sig2 = binascii.b2a_base64(sig2).replace("\n", "")
+ sigpat = re.compile('^Signature:.*$', re.M)
+ badSig = sigpat.sub("Signature: %s" % sig2, inf)
+ self.failUnlessRaises(ConfigError,
+ mixminion.ServerInfo.ServerInfo,
+ None, badSig)
+
+ # But make sure we don't check the sig on assumeValid
+ mixminion.ServerInfo.ServerInfo(None, badSig, assumeValid=1)
+
+ # Now with a bad digest
+ badDig = inf.replace("a@b.c", "---")
+ self.failUnlessRaises(ConfigError,
+ mixminion.ServerInfo.ServerInfo,
+ None, badSig)
+
+
+
def testSuite():
suite = unittest.TestSuite()
loader = unittest.TestLoader()
@@ -1736,7 +1938,9 @@
suite.addTest(tc(MinionlibCryptoTests))
suite.addTest(tc(CryptoTests))
suite.addTest(tc(FormatTests))
+ suite.addTest(tc(LogTests))
suite.addTest(tc(ConfigFileTests))
+ suite.addTest(tc(ServerInfoTests))
suite.addTest(tc(HashLogTests))
suite.addTest(tc(BuildMessageTests))
suite.addTest(tc(PacketHandlerTests))