[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[minion-cvs] Big pile of changes -- 0.0.4rc1 draws ever closer.
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv21169/src/minion/lib/mixminion
Modified Files:
BuildMessage.py ClientMain.py Common.py Config.py Crypto.py
MMTPClient.py Main.py Packet.py ServerInfo.py __init__.py
benchmark.py test.py
Log Message:
Big pile of changes -- 0.0.4rc1 draws ever closer.
Main:
- Add a big warning banner to say we aren't compatible with anything
right now.
BuildMessage, Packet, PacketHandler:
- Change to new packet format (2048-bit RSA keys, overlapping
encryption)
setup.py:
- Bump version to 0.0.4alpha2
ClientMain:
- Add a handy 'ping' command -- too handy to stay in the codebase, but
useful for testing.
Common:
- Add a checkPrivateFile function to enforce file permissions.
Config, ServerConfig, ServerInfo, Modules:
- Refactor away a lot of useless code. I once thought the
configuration files would be self-reloading, and everybody would use
some kind of publish/subscribe mechanism to update themselves -- but
that's kinda silly in practise.
- Add a prevalidate function so that we can freak out sooner if the
version number doesn't match.
ServerInfo:
- Move IP to Incoming/MMTP section
- Bump Descriptor-Version to 0.2
Crypto:
- Make sure that private keys are stored mode 0600.
MMTPClient:
- Make 'bad authentication' a separate exception
- Check to make sure our certs aren't expired.
EventStats:
- Begin testing, debugging, refactoring.
- Fancier rule for rotation: don't rotate until we've accumulated data
for a sufficiently long time, even if a long time has passed. (That
is, if we've been offline for 23 hours, don't trigger a daily
rotation.)
MMTPServer:
- Refactor client connection cache
- Use PeerCertificateCache to remember which certificates we've
already verified.
Index: BuildMessage.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/BuildMessage.py,v
retrieving revision 1.42
retrieving revision 1.43
diff -u -d -r1.42 -r1.43
--- BuildMessage.py 27 Mar 2003 10:30:59 -0000 1.42
+++ BuildMessage.py 26 Apr 2003 14:39:58 -0000 1.43
@@ -227,10 +227,6 @@
# message with 99.6% probability. (Otherwise, we'd need to repeatedly
# lioness-decrypt the payload in order to see whether the message was
# a reply.)
-
- # XXXX D'oh! This enables an offline password guessing attack for
- # XXXX anybody who sees multiple tags. We need to make sure that userKey
- # XXXX is stored on disk, and isn't a password. This needs more thought.
while 1:
seed = _getRandomTag(secretRNG)
if Crypto.sha1(seed+userKey+"Validate")[-1] == '\x00':
@@ -464,15 +460,23 @@
paddingPRNG: A pseudo-random number generator to generate padding
"""
assert len(path) == len(secrets)
- if len(path) * ENC_SUBHEADER_LEN > HEADER_LEN:
- raise MixError("Too many nodes in path")
routing, sizes, totalSize = _getRouting(path, exitType, exitInfo)
+ if totalSize > HEADER_LEN:
+ raise MixError("Path cannot fit in header")
# headerKey[i]==the AES key object node i will use to decrypt the header
headerKeys = [ Crypto.Keyset(secret).get(Crypto.HEADER_SECRET_MODE)
for secret in secrets ]
+## # junkKeys[i]==the AES key object that node i will use to re-pad the
+## # header.
+## junkKeys = [ Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
+## for secret in secrets ]
+
+ # Length of padding needed for the header
+ paddingLen = HEADER_LEN - totalSize
+
# Calculate junk.
# junkSeen[i]==the junk that node i will see, before it does any
# encryption. Note that junkSeen[0]=="", because node 0
@@ -483,25 +487,28 @@
#
# Node i+1 sees the junk that node i saw, plus the junk that i appends,
# all encrypted by i.
- prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
- # NewJunk is the junk that node i will append. (It's as long as
- # the subheaders that i removes.)
- newJunk = Crypto.prng(prngKey,size*128)
+
+ prngKey = Crypto.Keyset(secret).get(Crypto.RANDOM_JUNK_MODE)
+
+ # newJunk is the junk that node i will append. (It's as long as
+ # the data that i removes.)
+ newJunk = Crypto.prng(prngKey,size)
lastJunk = junkSeen[-1]
nextJunk = lastJunk + newJunk
- # Node i encrypts starting with its first extended subheader. By
- # the time it reaches the junk, it's traversed:
- # All of its extended subheaders [(size-1)*128]
- # Non-junk parts of the header [HEADER_LEN-len(nextJunk)]
- #
- # Simplifying, we find that the PRNG index for the junk is
- # HEADER_LEN-len(lastJunk)-128.
- startIdx = HEADER_LEN-len(lastJunk)-128
+
+ # Before we encrypt the junk, we'll encrypt all the data, and
+ # all the initial padding, but not the RSA-encrypted part.
+ # This is equal to - 256
+ # + sum(size[current]....size[last])
+ # + paddingLen
+ # This simplifies to:
+ #startIdx = paddingLen - 256 + totalSize - len(lastJunk)
+ startIdx = HEADER_LEN - ENC_SUBHEADER_LEN - len(lastJunk)
nextJunk = Crypto.ctr_crypt(nextJunk, headerKey, startIdx)
junkSeen.append(nextJunk)
# We start with the padding.
- header = paddingPRNG.getBytes(HEADER_LEN - totalSize*128)
+ header = paddingPRNG.getBytes(paddingLen)
# Now, we build the subheaders, iterating through the nodes backwards.
for i in range(len(path)-1, -1, -1):
@@ -514,12 +521,32 @@
None, #placeholder for as-yet-uncalculated digest
rt, ri)
- extHeaders = "".join(subhead.getExtraBlocks())
- rest = Crypto.ctr_crypt(extHeaders+header, headerKeys[i])
- subhead.digest = Crypto.sha1(rest+junkSeen[i])
+ # Do we need to include some of the remaining header in the
+ # RSA-encrypted portion?
+ underflowLength = subhead.getUnderflowLength()
+ if underflowLength > 0:
+ underflow = header[:underflowLength]
+ header = header[underflowLength:]
+ else:
+ underflow = ""
+
+ # Do we need to spill some of the routing info out from the
+ # RSA-encrypted portion?
+ #XXXX004 most of these asserts are silly.
+ assert not subhead.getOverflow() or not subhead.getUnderflowLength()
+ header = subhead.getOverflow() + header
+
+ header = Crypto.ctr_crypt(header, headerKeys[i])
+
+ assert len(header)+len(junkSeen[i])+ENC_SUBHEADER_LEN == HEADER_LEN
+ subhead.digest = Crypto.sha1(header+junkSeen[i])
pubkey = path[i].getPacketKey()
- esh = Crypto.pk_encrypt(subhead.pack(), pubkey)
- header = esh + rest
+ rsaPart = subhead.pack() + underflow
+ assert len(rsaPart) + OAEP_OVERHEAD == ENC_SUBHEADER_LEN
+ assert pubkey.get_modulus_bytes() == ENC_SUBHEADER_LEN
+ esh = Crypto.pk_encrypt(rsaPart, pubkey)
+ assert len(esh) == ENC_SUBHEADER_LEN == 256
+ header = esh + header
return header
@@ -636,12 +663,12 @@
node in path[1:] ]
routing.append((exitType, exitInfo))
- # sizes[i] is size, in blocks, of subheaders for i.
- sizes =[ getTotalBlocksForRoutingInfoLen(len(ri)) for _, ri in routing]
+ # sizes[i] is number of bytes added to header for subheader i.
+ sizes = [ len(ri)+OAEP_OVERHEAD+MIN_SUBHEADER_LEN for _, ri in routing]
- # totalSize is the total number of blocks.
+ # totalSize is the total number of bytes needed for header
totalSize = reduce(operator.add, sizes)
- if totalSize * ENC_SUBHEADER_LEN > HEADER_LEN:
+ if totalSize > HEADER_LEN:
raise MixError("Routing info won't fit in header")
return routing, sizes, totalSize
Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.70
retrieving revision 1.71
diff -u -d -r1.70 -r1.71
--- ClientMain.py 4 Apr 2003 20:28:55 -0000 1.70
+++ ClientMain.py 26 Apr 2003 14:39:58 -0000 1.71
@@ -25,7 +25,8 @@
import mixminion.Crypto
import mixminion.MMTPClient
from mixminion.Common import IntervalSet, LOG, floorDiv, MixError, \
- MixFatalError, MixProtocolError, UIError, UsageError, ceilDiv, \
+ MixFatalError, MixProtocolError, MixProtocolBadAuth, \
+ UIError, UsageError, ceilDiv, \
createPrivateDir, isPrintingAscii, \
isSMTPMailbox, formatDate, formatFnameTime, formatTime,\
Lockfile, openUnique, previousMidnight, readPossiblyGzippedFile, \
@@ -483,7 +484,7 @@
def getServerInfo(self, name, startAt=None, endAt=None, strict=0):
"""Return the most-recently-published ServerInfo for a given
- 'name' valid over a given time range. If strict, and no
+ 'name' valid over a given time range. If not strict, and no
such server is found, return None.
name -- A ServerInfo object, a nickname, or a filename.
@@ -493,7 +494,6 @@
startAt = time.time()
if endAt is None:
endAt = startAt + self.DEFAULT_REQUIRED_LIFETIME
-
if isinstance(name, ServerInfo):
# If it's a valid ServerInfo, we're done.
@@ -1478,6 +1478,23 @@
"""
return SURBLog(self.surbLogFilename)
+ def pingServer(self, routingInfo):
+ """Given an IPV4Info, try to connect to a server and find out if
+ it's up. Returns a boolean and a status message."""
+ timeout = self.config['Network'].get('ConnectionTimeout')
+ if timeout:
+ timeout = timeout[2]
+ else:
+ timeout = 60
+
+ try:
+ mixminion.MMTPClient.pingServer(routingInfo, timeout)
+ return 1, "Server seems to be running"
+ except MixProtocolBadAuth:
+ return 0, "Server seems to be running, but its key is wrong!"
+ except MixProtocolError, e:
+ return 0, "Couldn't connect to server: %s" % e
+
def sendMessages(self, msgList, routingInfo, noQueue=0, lazyQueue=0,
warnIfLost=1):
"""Given a list of packets and an IPV4Info object, sends the
@@ -2200,6 +2217,55 @@
else:
client.sendForwardMessage(address, payload, path1, path2,
forceQueue, forceNoQueue)
+
+_PING_USAGE = """\
+Usage: mixminion ping [options] serverName
+Options
+ -h, --help: Print this usage message and exit.
+ -v, --verbose Display extra debugging messages.
+ -f FILE, --config=FILE Use a configuration file other than ~/.mixminionrc
+ -D <yes|no>, --download-directory=<yes|no>
+ Force the client to download/not to download a
+ fresh directory.
+"""
+def runPing(cmd, args):
+ #DOCDOC comment me
+
+ if len(args) == 1 and args[0] in ('-h', '--help'):
+ print _PING_USAGE
+ sys.exit(0)
+
+ options, args = getopt.getopt(args, "hvf:D:",
+ ["help", "verbose", "config=", "download-directory=", ])
+
+ if len(args) != 1:
+ raise UsageError("mixminion ping requires a single server name")
+
+
+ print "==========================================================="
+ print "WARNING: Pinging a server is potentially dangerous, since"
+ print " it might alert people that you plan to use the server"
+ print " for your messages. This command is for testing only,"
+ print " and will go away before Mixminion 1.0. By then, all"
+ print " listed servers will be reliable anyway. <wink>"
+ print "==========================================================="
+
+ parser = CLIArgumentParser(options, wantConfig=1,
+ wantClientDirectory=1, wantClient=1,
+ wantLog=1, wantDownload=1)
+
+ parser.init()
+
+ directory = parser.directory
+ client = parser.client
+
+ info = directory.getServerInfo(args[0],
+ startAt=time.time(), endAt = time.time(),
+ strict=1)
+
+ ok, status = client.pingServer(info.getRoutingInfo())
+ print ">>>", status
+ print info.getNickname(), (ok and "is up" or "is down")
_IMPORT_SERVER_USAGE = """\
Usage: %(cmd)s [options] <filename> ...
Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.69
retrieving revision 1.70
diff -u -d -r1.69 -r1.70
--- Common.py 22 Apr 2003 01:45:22 -0000 1.69
+++ Common.py 26 Apr 2003 14:39:58 -0000 1.70
@@ -7,14 +7,14 @@
__all__ = [ 'IntervalSet', 'Lockfile', 'LOG', 'LogStream', 'MixError',
'MixFatalError', 'MixProtocolError', 'UIError', 'UsageError',
- 'ceilDiv', 'checkPrivateDir', 'createPrivateDir',
+ 'ceilDiv', 'checkPrivateDir', 'checkPrivateFile',
+ 'createPrivateDir',
'encodeBase64', 'floorDiv',
'formatBase64', 'formatDate', 'formatFnameTime', 'formatTime',
'installSIGCHLDHandler', 'isSMTPMailbox', 'openUnique',
'previousMidnight', 'readPossiblyGzippedFile', 'secureDelete',
'stringContains', 'succeedingMidnight', 'waitForChildren' ]
-import base64
import binascii
import bisect
import calendar
@@ -53,6 +53,10 @@
"""Exception class for server-rejected packets."""
pass
+class MixProtocolBadAuth(MixProtocolError):
+ """Exception class for failed authentication to a server."""
+ pass
+
class UIError(MixError):
"""Exception raised for an error that should be reported to the user,
not dumped as a stack trace."""
@@ -152,6 +156,30 @@
return "".join(pieces)
#----------------------------------------------------------------------
+def checkPrivateFile(fn, fix=1):
+ """Checks whether f is a file owned by this uid, set to mode 0600 or
+ 0700, and all its parents pass checkPrivateDir. Raises MixFatalError
+ if the assumtions are not met; else return None. If 'fix' is true,
+ repair permissions on the file rather than raising MixFatalError."""
+ parent, _ = os.path.split(fn)
+ if not checkPrivateDir(parent):
+ return None
+ st = os.stat(fn)
+ if not st:
+ raise MixFatalError("Nonexistant file %s" % fn)
+ if not os.path.isfile(fn):
+ raise MixFatalError("%s is not a regular file" % fn)
+ me = os.getuid()
+ if st[stat.ST_UID] != me:
+ raise MixFatalError("File %s must have owner %s" % (fn, me))
+ mode = st[stat.ST_MODE] & 0777
+ if mode not in (0700, 0600):
+ if not fix:
+ raise MixFatalError("Bad mode %o on file %s" % mode)
+ newmode = {0:0600,0100:0700}[(newmode & 0100)]
+ LOG.warn("Repairing permissions on file %s" % fn)
+ os.chmod(fn, newmode)
+
def createPrivateDir(d, nocreate=0):
"""Create a directory, and all parent directories, checking permissions
as we go along. All superdirectories must be owned by root or us."""
@@ -168,7 +196,7 @@
_WARNED_DIRECTORIES = {}
def checkPrivateDir(d, recurse=1):
- """Return true iff d is a directory owned by this uid, set to mode
+ """Check whether d is a directory owned by this uid, set to mode
0700. All of d's parents must not be writable or owned by anybody but
this uid and uid 0. If any of these conditions are unmet, raise
MixFatalErrror. Otherwise, return None."""
Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- Config.py 20 Feb 2003 16:57:39 -0000 1.40
+++ Config.py 26 Apr 2003 14:39:58 -0000 1.41
@@ -511,40 +511,22 @@
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
+ assert (filename is None) != (string is None)
+
if not hasattr(self, '_callbacks'):
self._callbacks = {}
self.assumeValid = assumeValid
- self.fname = filename
+
if filename:
- self.reload()
- elif string:
- self.__reload(None, string)
+ contents = mixminion.Common.readPossiblyGzippedFile(filename)
+ self.__load(contents)
else:
- self.clear()
-
- def clear(self):
- """Remove all sections from this _ConfigFile object."""
- 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
- the contents of this object unchanged."""
- if not self.fname:
- return
-
- contents = mixminion.Common.readPossiblyGzippedFile(self.fname)
- self.__reload(None, contents)
+ assert string is not None
+ self.__load(string)
- def __reload(self, file, fileContents):
+ def __load(self, fileContents):
"""As in .reload(), but takes an open file object _or_ a string."""
- if fileContents is None:
- fileContents = file.read()
- file.close()
fileContents = _abnormal_line_ending_re.sub("\n", fileContents)
@@ -553,24 +535,24 @@
else:
sections = _readConfigFile(fileContents)
- # These will become self.(_sections,_sectionEntries,_sectionNames)
- # if we are successful.
- self_sections = {}
- self_sectionEntries = {}
- self_sectionNames = []
+ sections = self.prevalidate(sections)
+
+ self._sections = {}
+ self._sectionEntries = {}
+ self._sectionNames = []
sectionEntryLines = {}
for secName, secEntries in sections:
- self_sectionNames.append(secName)
+ self._sectionNames.append(secName)
- if self_sections.has_key(secName):
+ if self._sections.has_key(secName):
raise ConfigError("Duplicate section [%s]" %secName)
section = {}
sectionEntries = []
entryLines = []
- self_sections[secName] = section
- self_sectionEntries[secName] = sectionEntries
+ self._sections[secName] = section
+ self._sectionEntries[secName] = sectionEntries
sectionEntryLines[secName] = entryLines
secConfig = self._syntax.get(secName, None)
@@ -647,20 +629,15 @@
for secName, secConfig in self._syntax.items():
secRule = secConfig.get('__SECTION__', ('ALLOW',None,None))
if (secRule[0] == 'REQUIRE'
- and not self_sections.has_key(secName)):
+ and not self._sections.has_key(secName)):
raise ConfigError("Section [%s] not found." %secName)
- elif not self_sections.has_key(secName):
- self_sections[secName] = {}
- self_sectionEntries[secName] = []
+ elif not self._sections.has_key(secName):
+ self._sections[secName] = {}
+ self._sectionEntries[secName] = []
if not self.assumeValid:
# Call our validation hook.
- self.validate(self_sections, self_sectionEntries,
- sectionEntryLines, fileContents)
-
- self._sections = self_sections
- self._sectionEntries = self_sectionEntries
- self._sectionNames = self_sectionNames
+ self.validate(sectionEntryLines, fileContents)
def _addCallback(self, section, cb):
"""For use by subclasses. Adds a callback for a section"""
@@ -668,8 +645,14 @@
self._callbacks = {}
self._callbacks[section] = cb
- def validate(self, sections, sectionEntries, entryLines,
- fileContents):
+ def prevalidate(self, contents):
+ """Given a list of (SECTION-NAME, [(KEY, VAL, LINENO)]), makes
+ decision on whether to parse sections. Subclasses should
+ override. Returns a revised version of its input.
+ """
+ return contents
+
+ def validate(self, entryLines, fileContents):
"""Check additional semantic properties of a set of configuration
data before overwriting old data. Subclasses should override."""
pass
@@ -723,10 +706,10 @@
def __init__(self, fname=None, string=None):
_ConfigFile.__init__(self, fname, string)
- def validate(self, sections, entries, lines, contents):
- _validateHostSection(sections.get('Host', {}))
+ def validate(self, lines, contents):
+ _validateHostSection(self['Host'])
- security = sections.get('Security', {})
+ security = self['Security']
p = security.get('PathLength', 8)
if not 0 < p <= 16:
raise ConfigError("Path length must be between 1 and 16")
@@ -734,7 +717,7 @@
LOG.warn("Your default path length is frighteningly low."
" I'll trust that you know what you're doing.")
- t = sections['Network'].get('ConnectionTimeout')
+ t = self['Network'].get('ConnectionTimeout')
if t:
if t[2] < 5:
LOG.warn("Very short connection timeout")
Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.41
retrieving revision 1.42
diff -u -d -r1.41 -r1.42
--- Crypto.py 16 Feb 2003 04:50:55 -0000 1.41
+++ Crypto.py 26 Apr 2003 14:39:58 -0000 1.42
@@ -272,7 +272,8 @@
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')
+ fd = os.open(filename, os.O_WRONLY|os.O_CREAT, 0600)
+ f = os.fdopen(fd, 'wb')
if password:
rsa.PEM_write_key(f, 0, password)
else:
Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.27
retrieving revision 1.28
diff -u -d -r1.27 -r1.28
--- MMTPClient.py 22 Apr 2003 01:45:22 -0000 1.27
+++ MMTPClient.py 26 Apr 2003 14:39:58 -0000 1.28
@@ -22,8 +22,8 @@
import socket
import mixminion._minionlib as _ml
from mixminion.Crypto import sha1, getCommonPRNG
-from mixminion.Common import MixProtocolError, MixProtocolReject, LOG, \
- MixError, formatBase64
+from mixminion.Common import MixProtocolError, MixProtocolReject, \
+ MixProtocolBadAuth, LOG, MixError, formatBase64
class TimeoutError(MixProtocolError):
"""Exception raised for protocol timeout."""
@@ -256,6 +256,13 @@
finally:
con.shutdown()
+def pingServer(routing, connectTimeout=5):
+ """Try to connect to a server and send a junk packet.
+
+ May raise MixProtocolBadAuth, or other MixProtocolError if server
+ isn't up."""
+ sendMessages(routing, ["JUNK"], connectTimeout=connectTimeout)
+
class PeerCertificateCache:
#XXXX004 use this properly; flush it to disk.
"DOCDOC"
@@ -267,9 +274,14 @@
if targetKeyID is None:
return
+ try:
+ tls.check_cert_alive()
+ except _ml.TLSError, e:
+ raise MixProtocolBadAuth("Invalid certificate: %s", str(e))
+
peer_pk = tls.get_peer_cert_pk()
hashed_peer_pk = sha1(peer_pk.encode_key(public=1))
- #XXXX005 Remove this option
+ #XXXX004 Remove this option
if targetKeyID == hashed_peer_pk:
LOG.warn("Non-rotatable keyid from server at %s", address)
return # raise MixProtocolError
@@ -277,7 +289,8 @@
if self.cache[hashed_peer_pk] == targetKeyID:
return # All is well.
else:
- raise MixProtocolError("Mismatch between expected and actual key id")
+ raise MixProtocolBadAuth(
+ "Mismatch between expected and actual key id")
except KeyError:
# We haven't found an identity for this pk yet.
pass
@@ -285,11 +298,11 @@
try:
identity = tls.verify_cert_and_get_identity_pk()
except _ml.TLSError, e:
- raise MixProtocolError("Invalid KeyID from server at %s: %s"
+ raise MixProtocolBadAuth("Invalid KeyID from server at %s: %s"
%(address, e))
hashed_identity = sha1(identity.encode_key(public=1))
self.cache[hashed_peer_pk] = hashed_identity
if hashed_identity != targetKeyID:
- raise MixProtocolError("Invalid KeyID for server at %s" % address)
+ raise MixProtocolBadAuth("Invalid KeyID for server at %s" %address)
Index: Main.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Main.py,v
retrieving revision 1.39
retrieving revision 1.40
diff -u -d -r1.39 -r1.40
--- Main.py 4 Apr 2003 20:59:53 -0000 1.39
+++ Main.py 26 Apr 2003 14:39:58 -0000 1.40
@@ -131,7 +131,8 @@
"flush" : ( 'mixminion.ClientMain', 'flushQueue' ),
"inspect-queue" : ( 'mixminion.ClientMain', 'listQueue' ),
# XXXX Obsolete; use "inspect-queue"; remove in 0.0.5
- "inspect-pool" : ( 'mixminion.ClientMain', 'listQueue' ),
+ "inspect-pool" : ( 'mixminion.ClientMain', 'listQueue' ),
+ "ping" : ( 'mixminion.ClientMain', 'runPing' ),
# XXXX Obsolete; use "server-start"; remove in 0.0.5
"server" : ( 'mixminion.server.ServerMain', 'runServer' ),
"server-start" : ( 'mixminion.server.ServerMain', 'runServer' ),
@@ -158,6 +159,7 @@
" decode [Decode or decrypt a received message]\n"+
" generate-surb [Generate a single-use reply block]\n"+
" inspect-surbs [Describe a single-use reply block]\n"+
+ " ping [Quick and dirty check whether a server is running]\n"
" (For Servers)\n"+
" server-start [Begin running a Mixminion server]\n"+
" server-stop [Halt a running Mixminion server]\n"+
@@ -201,6 +203,14 @@
if len(args) == 1 or not _COMMANDS.has_key(args[1]):
printUsage()
sys.exit(1)
+
+ if args[1] not in ('unittests', 'benchmarks'):
+ print "==========================================================="
+ print " TURN BACK NOW !!!"
+ print "This version of Mixminion (0.0.4alpha2) is compatible with no"
+ print "other version. Go check out the maintenance branch if you"
+ print "want to use this software to run a server or send messages."
+ print "==========================================================="
# Read the 'common' module to get the UIError class. To simplify
# command implementation code, we catch all UIError exceptions here.
Index: Packet.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Packet.py,v
retrieving revision 1.38
retrieving revision 1.39
diff -u -d -r1.38 -r1.39
--- Packet.py 20 Feb 2003 16:57:39 -0000 1.38
+++ Packet.py 26 Apr 2003 14:39:58 -0000 1.39
@@ -12,13 +12,14 @@
__all__ = [ 'compressData', 'CompressedDataTooLong', 'DROP_TYPE',
'ENC_FWD_OVERHEAD', 'ENC_SUBHEADER_LEN',
'FRAGMENT_PAYLOAD_OVERHEAD', 'FWD_TYPE', 'FragmentPayload',
- 'HEADER_LEN', 'Header', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
- 'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE', 'Message',
+ 'HEADER_LEN', 'IPV4Info', 'MAJOR_NO', 'MBOXInfo',
+ 'MBOX_TYPE', 'MINOR_NO', 'MIN_EXIT_TYPE',
+ 'MIN_SUBHEADER_LEN', 'Message',
'OAEP_OVERHEAD', 'PAYLOAD_LEN', 'ParseError', 'ReplyBlock',
'ReplyBlock', 'SECRET_LEN', 'SINGLETON_PAYLOAD_OVERHEAD',
'SMTPInfo', 'SMTP_TYPE', 'SWAP_FWD_TYPE', 'SingletonPayload',
'Subheader', 'TAG_LEN', 'TextEncodedMessage',
- 'getTotalBlocksForRoutingInfoLen', 'parseHeader', 'parseIPV4Info',
+ 'parseHeader', 'parseIPV4Info',
'parseMBOXInfo', 'parseMessage', 'parsePayload', 'parseReplyBlock',
'parseReplyBlocks', 'parseSMTPInfo', 'parseSubheader',
'parseTextEncodedMessage', 'parseTextReplyBlocks', 'uncompressData'
@@ -38,7 +39,7 @@
import mixminion._zlibutil as zlibutil
# Major and minor number for the understood packet format.
-MAJOR_NO, MINOR_NO = 0,2
+MAJOR_NO, MINOR_NO = 0,3
# Length of a Mixminion message
MESSAGE_LEN = 1 << 15
@@ -51,12 +52,13 @@
OAEP_OVERHEAD = 42
# Length of a subheader, once RSA-encoded.
-ENC_SUBHEADER_LEN = 128
+ENC_SUBHEADER_LEN = 256
# Smallest possible size for a subheader
MIN_SUBHEADER_LEN = 42
# Most information we can fit into a subheader before padding
MAX_SUBHEADER_LEN = ENC_SUBHEADER_LEN - OAEP_OVERHEAD
-# Longest routing info that will fit in the main subheader
+# Longest routing info that will fit into the RSA-encrypted portion of
+# the subheader.
MAX_ROUTING_INFO_LEN = MAX_SUBHEADER_LEN - MIN_SUBHEADER_LEN
# Length of a digest
@@ -66,9 +68,6 @@
# Length of end-to-end message tag
TAG_LEN = 20
-# Most info that fits in a single extened subheader
-ROUTING_INFO_PER_EXTENDED_SUBHEADER = ENC_SUBHEADER_LEN
-
#----------------------------------------------------------------------
# Values for the 'Routing type' subheader field
# Mixminion types
@@ -118,35 +117,7 @@
if len(s) != HEADER_LEN:
raise ParseError("Bad header length")
- return Header(s)
-
-class Header:
- """Represents a 2K Mixminion header, containing up to 16 subheaders."""
- def __init__(self, contents):
- """Initialize a new header from its contents"""
- self.contents = contents
-
- def __getitem__(self, i):
- """header[i] -> str
-
- Returns the i'th encoded subheader of this header, for i in 0..15"""
- if i < 0: i = 16+i
- return self.contents[i*ENC_SUBHEADER_LEN:
- (i+1)*ENC_SUBHEADER_LEN]
-
- def __getslice__(self, i, j):
- """header[i:j] -> str
-
- Returns a slice of the i-j'th subheaders of this header."""
- if j > 16: j = 16
- if i < 0: i += 16
- if j < 0: j += 16
- return self.contents[i*ENC_SUBHEADER_LEN:
- j*ENC_SUBHEADER_LEN]
-
- def __len__(self):
- """Return the number of subheaders in this header (always 16)"""
- return 16
+ return s
# A subheader begins with: a major byte, a minor byte, SECRET_LEN secret
# bytes, DIGEST_LEN digest bytes, a routing_len short, and a routing_type
@@ -166,20 +137,13 @@
except struct.error:
raise ParseError("Misformatted subheader")
ri = s[MIN_SUBHEADER_LEN:]
+ underflow = ""
if rlen < len(ri):
- ri = ri[:rlen]
+ ri, underflow = ri[:rlen], ri[rlen:]
if rt >= MIN_EXIT_TYPE and rlen < 20:
raise ParseError("Subheader missing tag")
- return Subheader(major,minor,secret,digest,rt,ri,rlen)
-
-def getTotalBlocksForRoutingInfoLen(bytes):
- """Return the number of subheaders that will be needed for a hop
- whose routinginfo is (bytes) long."""
- if bytes <= MAX_ROUTING_INFO_LEN:
- return 1
- else:
- extraBytes = bytes - MAX_ROUTING_INFO_LEN
- return 2 + floorDiv(extraBytes,ROUTING_INFO_PER_EXTENDED_SUBHEADER)
+ #XXXX004 test underflow
+ return Subheader(major,minor,secret,digest,rt,ri,rlen,underflow)
class Subheader:
"""Represents a decoded Mixminion subheader
@@ -190,10 +154,11 @@
A Subheader can exist in a half-initialized state where routing
info has been read from the first header, but not from the
extened headers. If this is so, routinglen will be > len(routinginfo).
+ DOCDOC underflow
"""
def __init__(self, major, minor, secret, digest, routingtype,
- routinginfo, routinglen=None):
+ routinginfo, routinglen=None, underflow=""):
"""Initialize a new subheader"""
self.major = major
self.minor = minor
@@ -205,6 +170,7 @@
self.routinglen = routinglen
self.routingtype = routingtype
self.routinginfo = routinginfo
+ self.underflow = underflow
def __repr__(self):
return ("Subheader(major=%(major)r, minor=%(minor)r, "+
@@ -231,33 +197,33 @@
self.routinginfo = info
self.routinglen = len(info)
- def isExtended(self):
- """Return true iff the routinginfo is too long to fit in a single
- subheader."""
- return self.routinglen > MAX_ROUTING_INFO_LEN
-
- def getNExtraBlocks(self):
- """Return the number of extra blocks that will be needed to fit
- the routinginfo."""
- return getTotalBlocksForRoutingInfoLen(self.routinglen)-1
-
- def appendExtraBlocks(self, data):
- """Given a string containing additional (decoded) blocks of
- routing info, add them to the routinginfo of this
+ def appendOverflow(self, data):
+ """Given a string containing additional
+ routing info, add it to the routinginfo of this
object.
+ DOCDOC
"""
- nBlocks = self.getNExtraBlocks()
- assert len(data) == nBlocks * ENC_SUBHEADER_LEN
- raw = [self.routinginfo]
- for i in range(nBlocks):
- block = data[i*ENC_SUBHEADER_LEN:(i+1)*ENC_SUBHEADER_LEN]
- raw.append(block)
- self.routinginfo = ("".join(raw))[:self.routinglen]
+ #XXXX004 test
+ self.routinginfo += data
+ assert len(self.routinginfo) <= self.routinglen
+
+ def getUnderflowLength(self):
+ return max(0, MAX_ROUTING_INFO_LEN - self.routinglen)
+
+ def getOverflowLength(self):
+ """DOCDOC"""
+ #XXXX004 test
+ return max(0, self.routinglen - MAX_ROUTING_INFO_LEN)
+
+ def getOverflow(self):
+ """DOCDOC"""
+ #XXXX004 test
+ return self.routinginfo[MAX_ROUTING_INFO_LEN:]
def pack(self):
"""Return the (unencrypted) string representation of this Subhead.
- Does not include extra blocks"""
+ Does not include overflow or underflow"""
assert self.routinglen == len(self.routinginfo)
assert len(self.digest) == DIGEST_LEN
assert len(self.secret) == SECRET_LEN
@@ -267,22 +233,6 @@
self.major,self.minor,self.secret,self.digest,
self.routinglen, self.routingtype)+info
- def getExtraBlocks(self):
- """Return a list of (unencrypted) blocks of extra routing info."""
- if not self.isExtended():
- return []
- else:
- info = self.routinginfo[MAX_ROUTING_INFO_LEN:]
- result = []
- for i in range(self.getNExtraBlocks()):
- content = info[i*ROUTING_INFO_PER_EXTENDED_SUBHEADER:
- (i+1)*ROUTING_INFO_PER_EXTENDED_SUBHEADER]
- missing = ROUTING_INFO_PER_EXTENDED_SUBHEADER-len(content)
- if missing > 0:
- content += '\000'*missing
- result.append(content)
- return result
-
#----------------------------------------------------------------------
# UNENCRYPTED PAYLOADS
@@ -535,7 +485,7 @@
"""Converts routing info for an IPV4 address into an IPV4Info object,
suitable for use by FWD or SWAP_FWD modules."""
if len(s) != 4+2+DIGEST_LEN:
- raise ParseError("IPV4 information with wrong length")
+ raise ParseError("IPV4 information with wrong length (%d)" % len(s))
try:
ip, port, keyinfo = struct.unpack(IPV4_PAT, s)
except struct.error:
@@ -579,7 +529,7 @@
r = cmp(self.ip, other.ip)
if r: return r
r = cmp(self.port, other.port)
- if r: return n
+ if r: return r
return cmp(self.keyinfo, other.keyinfo)
def parseSMTPInfo(s):
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- ServerInfo.py 18 Apr 2003 17:41:38 -0000 1.40
+++ ServerInfo.py 26 Apr 2003 14:39:58 -0000 1.41
@@ -26,12 +26,16 @@
MAX_CONTACT = 256
# Longest allowed Comments field
MAX_COMMENTS = 1024
+# Longest allowed Contact-Fingerprint field
+MAX_FINGERPRINT = 128
# 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
+PACKET_KEY_BYTES = 2048 >> 3
+# Length of MMTP key
+MMTP_KEY_BYTES = 1024 >> 3
# tmp alias to make this easier to spell.
C = mixminion.Config
@@ -47,7 +51,6 @@
_syntax = {
"Server" : { "__SECTION__": ("REQUIRE", None, None),
"Descriptor-Version": ("REQUIRE", None, None),
- "IP": ("REQUIRE", C._parseIP, None),
"Nickname": ("REQUIRE", C._parseNickname, None),
"Identity": ("REQUIRE", C._parsePublicKey, None),
"Digest": ("REQUIRE", C._parseBase64, None),
@@ -58,7 +61,7 @@
"Contact": ("ALLOW", None, None),
"Comments": ("ALLOW", None, None),
"Packet-Key": ("REQUIRE", C._parsePublicKey, None),
- "Contact-Fingerptint": ("ALLOW", None, None),
+ "Contact-Fingerprint": ("ALLOW", None, None),
# XXXX010 change these next few to "REQUIRE".
"Packet-Formats": ("ALLOW", None, None),
"Software": ("ALLOW", None, None),
@@ -66,6 +69,7 @@
},
"Incoming/MMTP" : {
"Version": ("REQUIRE", None, None),
+ "IP": ("REQUIRE", C._parseIP, None),
"Port": ("REQUIRE", C._parseInt, None),
"Key-Digest": ("REQUIRE", C._parseBase64, None),
"Protocols": ("REQUIRE", None, None),
@@ -105,16 +109,28 @@
self['Server']['Nickname'],
fname or "<string>")
- def validate(self, sections, entries, lines, contents):
+ def prevalidate(self, contents):
+ for name, ents in contents:
+ if name == 'Server':
+ for k,v,_ in ents:
+ if k == 'Descriptor-Version' and v.strip() != '0.2':
+ raise ConfigError("Unrecognized descriptor version %s"
+ % v.strip())
+ #XXXX Remove sections with unrecognized versions.
+
+ return contents
+
+
+ def validate(self, lines, contents):
####
# Check 'Server' section.
- server = sections['Server']
- if server['Descriptor-Version'] != '0.1':
+ server = self['Server']
+ if server['Descriptor-Version'] != '0.2':
raise ConfigError("Unrecognized descriptor version %r",
server['Descriptor-Version'])
####
- # Check the igest of file
+ # Check the digest of file
digest = getServerInfoDigest(contents)
if digest != server['Digest']:
raise ConfigError("Invalid digest")
@@ -138,11 +154,14 @@
raise ConfigError("Contact too long")
if server['Comments'] and len(server['Comments']) > MAX_COMMENTS:
raise ConfigError("Comments too long")
+ if server['Contact-Fingerprint'] and \
+ len(server['Contact-Fingerprint']) > MAX_FINGERPRINT:
+ raise ConfigError("Contact-Fingerprint too long")
+
packetKeyBytes = server['Packet-Key'].get_modulus_bytes()
if packetKeyBytes != PACKET_KEY_BYTES:
raise ConfigError("Invalid length on packet key")
-
####
# Check signature
try:
@@ -154,7 +173,7 @@
raise ConfigError("Signed digest is incorrect")
## Incoming/MMTP section
- inMMTP = sections['Incoming/MMTP']
+ inMMTP = self['Incoming/MMTP']
if inMMTP:
if inMMTP['Version'] != '0.1':
raise ConfigError("Unrecognized MMTP descriptor version %s"%
@@ -164,7 +183,7 @@
formatBase64(inMMTP['Key-Digest']))
## Outgoing/MMTP section
- outMMTP = sections['Outgoing/MMTP']
+ outMMTP = self['Outgoing/MMTP']
if outMMTP:
if outMMTP['Version'] != '0.1':
raise ConfigError("Unrecognized MMTP descriptor version %s"%
@@ -186,7 +205,7 @@
def getAddr(self):
"""Returns this server's IP address"""
- return self['Server']['IP']
+ return self['Incoming/MMTP']['IP']
def getPort(self):
"""Returns this server's IP port"""
@@ -369,8 +388,17 @@
self.expectedDigest = expectedDigest
mixminion.Config._ConfigFile.__init__(self, string=contents)
- def validate(self, sections, entries, lines, contents):
- direc = sections['Directory']
+ def prevalidate(self, contents):
+ for name, ents in contents:
+ if name == 'Directory':
+ for k,v,_ in ents:
+ if k == 'Version' and v.strip() != '0.1':
+ raise ConfigError("Unrecognized directory version")
+
+ return contents
+
+ def validate(self, lines, contents):
+ direc = self['Directory']
if direc['Version'] != "0.1":
raise ConfigError("Unrecognized directory version")
if direc['Published'] > time.time() + 600:
@@ -378,7 +406,7 @@
if direc['Valid-Until'] <= direc['Valid-After']:
raise ConfigError("Directory is never valid")
- sig = sections['Signature']
+ sig = self['Signature']
identityKey = sig['DirectoryIdentity']
identityBytes = identityKey.get_modulus_bytes()
if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):
Index: __init__.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/__init__.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- __init__.py 5 Mar 2003 21:34:04 -0000 1.30
+++ __init__.py 26 Apr 2003 14:39:59 -0000 1.31
@@ -7,7 +7,7 @@
"""
# This version string is generated from setup.py; don't edit it.
-__version__ = "0.0.4alpha"
+__version__ = "0.0.4alpha2"
# This 5-tuple encodes the version number for comparison. Don't edit it.
# The first 3 numbers are the version number; the 4th is:
# 0 for alpha
@@ -18,7 +18,7 @@
# The 4th or 5th number may be a string. If so, it is not meant to
# succeed or preceed any other sub-version with the same a.b.c version
# number.
-version_info = (0, 0, 4, 0, -1)
+version_info = (0, 0, 4, 0, 2)
__all__ = [ 'server', 'directory' ]
def version_tuple_to_string(t):
Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.32
retrieving revision 1.33
diff -u -d -r1.32 -r1.33
--- benchmark.py 10 Apr 2003 03:03:16 -0000 1.32
+++ benchmark.py 26 Apr 2003 14:39:59 -0000 1.33
@@ -406,7 +406,7 @@
def buildMessageTiming():
print "#================= BUILD MESSAGE ====================="
- pk = pk_generate()
+ pk = pk_generate(2048)
for payload in "Hello!!!"*128, "Hello!!!"*(128*28):
print "Compress %sK" % (len(payload)/1024), \
@@ -470,7 +470,7 @@
def serverProcessTiming():
print "#================= SERVER PROCESS ====================="
- pk = pk_generate()
+ pk = pk_generate(2048)
server = FakeServerInfo("127.0.0.1", 1, pk, "X"*20)
sp = PacketHandler(pk, DummyLog())
@@ -550,7 +550,7 @@
print " short xor: %3.1f%%" % (100*2*strxor_20b/lioness_e)
##### SERVER PROCESS
- pk = pk_generate(1024)
+ pk = pk_generate(2048)
# Typical (no swap) server process is:
# pk_decrypt (128b)
@@ -825,8 +825,6 @@
sock.close()
#----------------------------------------------------------------------
-
-
def timeAll(name, args):
cryptoTiming()
buildMessageTiming()
@@ -838,6 +836,3 @@
timeEfficiency()
#import profile
#profile.run("import mixminion.benchmark; mixminion.benchmark.directoryTiming()")
-
-def timeAll(name,args):
- testLeaks6_2()
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.99
retrieving revision 1.100
diff -u -d -r1.99 -r1.100
--- test.py 18 Apr 2003 17:41:38 -0000 1.99
+++ test.py 26 Apr 2003 14:39:59 -0000 1.100
@@ -992,21 +992,23 @@
"ABCDEFGHIJABCDEFGHIJ\000\005\000\001Hello"
# test packing
self.assertEquals(s.pack(), expected)
- self.failUnless(not s.isExtended())
- self.assertEquals(s.getNExtraBlocks(), 0)
- self.assertEquals(s.getExtraBlocks(), [])
+ self.assertEquals(s.getOverflow(), "")
+ self.assertEquals(s.getUnderflowLength(), 256 - 42 - 42 - 5)
+ self.assertEquals(s.getOverflowLength(), 0)
# test unpacking,
- s = parseSubheader(s.pack())
- self.assertEquals(s.major, 3)
- self.assertEquals(s.minor, 0)
- self.assertEquals(s.secret, "abcde"*3+"f")
- self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
- self.assertEquals(s.routingtype, 1)
- self.assertEquals(s.routinglen, 5)
- self.assertEquals(s.routinginfo, "Hello")
- self.failUnless(not s.isExtended())
- self.assertEquals(s.pack(), expected)
+ for more in "", "There is more":
+ s = parseSubheader(s.pack()+more)
+ self.assertEquals(s.major, 3)
+ self.assertEquals(s.minor, 0)
+ self.assertEquals(s.secret, "abcde"*3+"f")
+ self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
+ self.assertEquals(s.routingtype, 1)
+ self.assertEquals(s.routinglen, 5)
+ self.assertEquals(s.routinginfo, "Hello")
+ self.assertEquals(s.pack(), expected)
+ self.assertEquals(s.underflow, more)
+ self.assertEquals(s.getUnderflowLength(), 256-42-42-s.routinglen)
ts_eliot = ("Who is the third who walks always beside you? / "+
"When I count, there are only you and I together / "+
@@ -1022,17 +1024,15 @@
# test extended subeaders
expected = ("\003\011abcdeabcdeabcdefABCDEFGHIJABCDEFGHIJ"+
"\000\272\001\054Who is the third who walks always"+
- " beside you")
+ " beside you? / When I count, there are only you and I"+
+ " together / But when I look ahead up the white road /"+
+ " There is always another one walk")
self.assertEquals(len(expected), mixminion.Packet.MAX_SUBHEADER_LEN)
self.assertEquals(s.pack(), expected)
- extra = s.getExtraBlocks()
- self.assertEquals(len(extra), 2)
- self.assertEquals(extra[0],
- ("? / When I count, there are only you "+
- "and I together / But when I look ahead up the white "+
- "road / There is always another one walk"))
- self.assertEquals(extra[1], "ing beside you"+(114*'\000'))
+ overflow = s.getOverflow()
+ self.assertEquals(len(overflow), 14)
+ self.assertEquals(overflow, "ing beside you")
# test parsing extended subheaders
s = parseSubheader(expected)
@@ -1042,36 +1042,20 @@
self.assertEquals(s.digest, "ABCDEFGHIJ"*2)
self.assertEquals(s.routingtype, 300)
self.assertEquals(s.routinglen, 186)
- self.failUnless(s.isExtended())
- self.assertEquals(s.getNExtraBlocks(), 2)
+ self.assertEquals(s.getOverflowLength(), 14)
- s.appendExtraBlocks("".join(extra))
+ s.appendOverflow("ing beside you")
self.assertEquals(s.routinginfo, ts_eliot)
self.assertEquals(s.getExitAddress(), ts_eliot[20:])
self.assertEquals(s.getTag(), ts_eliot[:20])
self.assertEquals(s.pack(), expected)
- self.assertEquals(s.getExtraBlocks(), extra)
# Underlong subheaders must fail
self.failUnlessRaises(ParseError,
parseSubheader, "a"*(41))
# overlong subheaders must fail
self.failUnlessRaises(ParseError,
- parseSubheader, "a"*(99))
-
- def test_headers(self):
- # Make sure we extract the subheaders from a header correctly.
- # (Generate a nice random string to make sure we're slicing right.)
- header = Crypto.prng("onefish, twofish", 2048)
- h = parseHeader(header)
- self.failUnless(h[0] == header[:128])
- self.failUnless(h[4] == header[128*4:128*5])
- self.failUnless(h[:1] == h[0])
- self.failUnless(h[1:] == header[128:])
- self.failUnless(h[1:4] == header[128:128*4])
- self.failUnless(h[15] == header[-128:])
- self.failUnless(h[15] == h[-1])
- self.failUnless(h[14:] == h[-2:])
+ parseSubheader, "a"*(256))
def test_message(self):
# Make sure we can pull the headers and payload of a message apart
@@ -1370,8 +1354,9 @@
"""Represents a Mixminion server, and the information needed to send
messages to it."""
def __init__(self, addr, port, key, keyid):
+ assert key.get_modulus_bytes() == 256
self.addr = addr
- self.port = port
+ self.port = port
self.key = key
self.keyid = keyid
@@ -1388,9 +1373,9 @@
class BuildMessageTests(unittest.TestCase):
def setUp(self):
- self.pk1 = getRSAKey(0,1024)
- self.pk2 = getRSAKey(1,1024)
- self.pk3 = getRSAKey(2,1024)
+ self.pk1 = getRSAKey(0,2048)
+ self.pk2 = getRSAKey(1,2048)
+ self.pk3 = getRSAKey(2,2048)
self.pk512 = getRSAKey(0,512)
self.server1 = FakeServerInfo("127.0.0.1", 1, self.pk1, "X"*20)
self.server2 = FakeServerInfo("127.0.0.2", 3, self.pk2, "Z"*20)
@@ -1515,7 +1500,7 @@
# Go through the hops one by one, simulating the decoding process.
for pk, secret, rt, ri in zip(pks, secrets,rtypes,rinfo):
# Decrypt the first subheader.
- subh = mixminion.Packet.parseSubheader(pk_decrypt(head[:128], pk))
+ subh = mixminion.Packet.parseSubheader(pk_decrypt(head[:256], pk))
# If we're expecting a given secret in this subheader, check it.
if secret:
self.assertEquals(subh.secret, secret)
@@ -1525,7 +1510,7 @@
# Check the version, the digest, and the routing type.
self.assertEquals(subh.major, mixminion.Packet.MAJOR_NO)
self.assertEquals(subh.minor, mixminion.Packet.MINOR_NO)
- self.assertEquals(subh.digest, sha1(head[128:]))
+ self.assertEquals(subh.digest, sha1(head[256:]))
self.assertEquals(subh.routingtype, rt)
# Key to decrypt the rest of the header
@@ -1543,20 +1528,19 @@
# Check the routinginfo. This is a little different for regular
# and extended subheaders...
- if not subh.isExtended():
+ if not subh.getOverflowLength():
if ri:
self.assertEquals(subh.routinginfo[ext:], ri)
self.assertEquals(subh.routinglen, len(ri)+ext)
else:
retinfo.append(subh.routinginfo)
- size = 128
- n = 0
+ size = 256
else:
- self.assert_(len(ri)+ext>mixminion.Packet.MAX_ROUTING_INFO_LEN)
- n = subh.getNExtraBlocks()
- size = (1+n)*128
- more = ctr_crypt(head[128:128+128*n], key)
- subh.appendExtraBlocks(more)
+ more = ctr_crypt(head[256:256+subh.getOverflowLength()], key)
+ self.assert_(len(ri+more)>
+ mixminion.Packet.MAX_ROUTING_INFO_LEN)
+ subh.appendOverflow(more)
+ size = 256 + subh.getOverflowLength()
if ri:
self.assertEquals(subh.routinginfo[ext:], ri)
self.assertEquals(subh.routinglen, len(ri)+ext)
@@ -1565,15 +1549,18 @@
# Decrypt and pad the rest of the header.
prngkey = ks.get(RANDOM_JUNK_MODE)
- head = ctr_crypt(head[size:]+prng(prngkey,size), key, 128*n)
+ head = ctr_crypt(head[size:]+
+ prng(prngkey,size-len(subh.underflow)), key, size-256)
+ head = subh.underflow + head
+ self.assertEquals(len(head), 2048)
return retsecrets, retinfo, tag
- def test_extended_routinginfo(self):
+ def test_header_overflow(self):
bhead = BuildMessage._buildHeader
secrets = ["9"*16]
- longStr = "Foo"*50
+ longStr = "Foo"*100
head = bhead([self.server1], secrets, 99, longStr, AESCounterPRNG())
pks = (self.pk1,)
rtypes = (99,)
@@ -1584,7 +1571,7 @@
# Now try a header with extended **intermediate** routing info.
# Since this never happens in the wild, we fake it.
tag = "dref"*5
- longStr2 = longStr * 2
+ longStr2 = "BAR"*100
def getLongRoutingInfo(longStr2=longStr2,tag=tag):
return MBOXInfo(tag+longStr2)
@@ -1979,7 +1966,7 @@
# Encoded forward message
efwd = (comp+"RWE/HGW"*4096)[:28*1024-22-38]
efwd = '\x00\x6D'+sha1(efwd)+efwd
- rsa1 = self.pk1
+ rsa1 = getRSAKey(0,1024)
key1 = Keyset("RWE "*4).getLionessKeys("END-TO-END ENCRYPT")
efwd_rsa = pk_encrypt(("RWE "*4)+efwd[:70], rsa1)
efwd_lioness = lioness_encrypt(efwd[70:], key1)
@@ -2032,7 +2019,7 @@
if 1:
for p in (passwd, None):
self.assertEquals(payload,
- decodePayload(efwd_p, efwd_t, self.pk1, p))
+ decodePayload(efwd_p, efwd_t, rsa1, p))
self.assertEquals(None,
decodePayload(efwd_p, efwd_t, None, p))
self.assertEquals(None,
@@ -2080,11 +2067,11 @@
efwd_pbad = efwd_p[:-1] + chr(ord(efwd_p[-1])^0xaa)
self.failUnlessRaises(MixError,
BuildMessage._decodeEncryptedForwardPayload,
- efwd_pbad, efwd_t, self.pk1)
+ efwd_pbad, efwd_t, rsa1)
for p in (passwd, None):
self.failUnlessRaises(MixError, decodePayload,
- efwd_pbad, efwd_t, self.pk1, p)
+ efwd_pbad, efwd_t, rsa1, p)
self.assertEquals(None,
decodePayload(efwd_pbad, efwd_t, self.pk2, p))
@@ -2101,9 +2088,9 @@
class PacketHandlerTests(unittest.TestCase):
def setUp(self):
- self.pk1 = getRSAKey(0,1024)
- self.pk2 = getRSAKey(1,1024)
- self.pk3 = getRSAKey(2,1024)
+ self.pk1 = getRSAKey(0,2048)
+ self.pk2 = getRSAKey(1,2048)
+ self.pk3 = getRSAKey(2,2048)
self.tmpfile = mix_mktemp(".db")
h = self.hlog = HashLog(self.tmpfile, "Z"*20)
@@ -2283,7 +2270,7 @@
m = bfm("Z", MBOX_TYPE, "hello\000bye",
[self.server2, server1X, self.server3],
[server1X, self.server2, self.server3])
- self.failUnlessRaises(ContentError, self.sp2.processMessage, m)
+ self.failUnlessRaises(ParseError, self.sp2.processMessage, m)
# Duplicate messages need to fail.
m = bfm("Z", SMTP_TYPE, "nobody@invalid",
@@ -2344,39 +2331,26 @@
self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
# Subhead we can't parse
- m_x = pk_encrypt("foo", self.pk1)+m[128:]
+ m_x = pk_encrypt("foo", self.pk1)+m[256:]
self.failUnlessRaises(ParseError, self.sp1.processMessage, m_x)
# Bad IPV4 info
- subh_real = pk_decrypt(m[:128], self.pk1)
+ subh_real = pk_decrypt(m[:256], self.pk1)
subh = parseSubheader(subh_real)
subh.setRoutingInfo(subh.routinginfo + "X")
- m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+ m_x = pk_encrypt(subh.pack()+subh.underflow[:-1], self.pk1)+m[256:]
self.failUnlessRaises(ParseError, self.sp1.processMessage, m_x)
- # Subhead that claims to be impossibly long: FWD case
- subh = parseSubheader(subh_real)
- subh.setRoutingInfo("X"*100)
- m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
- self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
-
- # Subhead that claims to be impossibly long: exit case
- subh = parseSubheader(subh_real)
- subh.routingtype = MBOX_TYPE
- subh.setRoutingInfo("X"*10000)
- m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
- self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
-
# Bad Major or Minor
subh = parseSubheader(subh_real)
subh.major = 255
- m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+ m_x = pk_encrypt(subh.pack()+subh.underflow, self.pk1)+m[256:]
self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
# Bad digest
subh = parseSubheader(subh_real)
subh.digest = " "*20
- m_x = pk_encrypt(subh.pack(), self.pk1)+m[128:]
+ m_x = pk_encrypt(subh.pack()+subh.underflow, self.pk1)+m[256:]
self.failUnlessRaises(ContentError, self.sp1.processMessage, m_x)
# Corrupt payload
@@ -2388,8 +2362,6 @@
m_x = self.sp2.processMessage(m_x).getPacket()
self.failUnlessRaises(CryptoError, self.sp3.processMessage, m_x)
-
-
#----------------------------------------------------------------------
# QUEUE
@@ -3261,20 +3233,6 @@
self.assertEquals(f['Sec3']['IntAMD2'], [5,2])
self.assertEquals(f['Sec3']['IntRS'], 5)
- # Test failing reload
- writeFile(fn, "[Sec1]\nFoo=99\nBadEntry 3\n\n")
- self.failUnlessRaises(ConfigError, f.reload)
- self.assertEquals(f['Sec1']['Foo'], 'abcde f')
- self.assertEquals(f['Sec1']['Bar'], 'bar')
- self.assertEquals(f['Sec2']['Quz'], ['99 99', '88 88'])
-
- # Test 'reload' operation
- writeFile(fn, shorterString)
- f.reload()
- self.assertEquals(f['Sec1']['Foo'], 'a')
- 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)
@@ -3546,8 +3504,8 @@
d)
info = mixminion.ServerInfo.ServerInfo(string=inf)
eq = self.assertEquals
- eq(info['Server']['Descriptor-Version'], "0.1")
- eq(info['Server']['IP'], "192.168.0.1")
+ eq(info['Server']['Descriptor-Version'], "0.2")
+ eq(info['Incoming/MMTP']['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']
@@ -3555,8 +3513,8 @@
eq(info['Server']['Valid-Until']-info['Server']['Valid-After'],
10*24*60*60)
eq(info['Server']['Contact'], "a@b.c")
- eq(info['Server']['Software'], "Mixminion 0.0.4alpha")
- eq(info['Server']['Packet-Formats'], "0.2")
+ eq(info['Server']['Software'], "Mixminion %s"%mixminion.__version__)
+ eq(info['Server']['Packet-Formats'], "0.3")
eq(info['Server']['Comments'],
"This is a test of the emergency broadcast system")
@@ -3703,6 +3661,17 @@
self.assert_(not bobs[1].isSupersededBy([bobs[1]]))
self.assert_(not bobs[1].isSupersededBy([freds[2]]))
+ # Test whether we ignore server descriptors with unknown versions.
+ try:
+ inf2 = inf.replace("Descriptor-Version: 0.2\n",
+ "Descriptor-Version: 0.99\n")
+ inf2 = inf2.replace("\nIP: ", "\nIP: x.")
+ inf2 = inf2.replace("\nNickname: ", "\nNickname-XX: ")
+ ServerInfo(string=inf2)
+ self.fail("Missing ConfigError")
+ except ConfigError, p:
+ self.assertEquals(str(p), "Unrecognized descriptor version 0.99")
+
def test_directory(self):
eq = self.assertEquals
examples = getExampleServerDescriptors()
@@ -3857,6 +3826,106 @@
eq(5, len(lst.servers))
eq(2, len(os.listdir(archiveDir)))
+
+#----------------------------------------------------------------------
+# EventStats
+class EventStatsTests(unittest.TestCase):
+ def testNilLog(self):
+ import mixminion.server.EventStats as ES
+ ES.configureLog({'Server': {'LogStats' : 0}})
+ self.failUnless(isinstance(ES.log, ES.NilEventLog))
+ ES.log.receivedPacket()
+ ES.log.attemptedRelay()
+ ES.log.failedRelay()
+ ES.log.unretriableRelay()
+
+ def testEventLog(self):
+ import mixminion.server.EventStats as ES
+ eq = self.assertEquals
+ homedir = mix_mktemp()
+ ES.configureLog({'Server':
+ {'LogStats' : 1,
+ 'Homedir' : homedir,
+ 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+ self.failUnless(isinstance(ES.log, ES.EventLog))
+ # Test all commands
+ ES.log.receivedPacket()
+ ES.log.attemptedRelay()
+ ES.log.failedRelay()
+ ES.log.unretriableRelay()
+ ES.log.attemptedDelivery()
+ ES.log.attemptedDelivery()
+ ES.log.attemptedDelivery("Y")
+ ES.log.attemptedDelivery("Y")
+ ES.log.successfulDelivery()
+ ES.log.failedDelivery()
+ ES.log.failedDelivery("Y")
+ ES.log.unretriableDelivery("Y")
+ ES.log.save()
+ eq(ES.log.count['UnretriableDelivery']['Y'], 1)
+ eq(ES.log.count['AttemptedDelivery'][None], 2)
+ # Test reload.
+ ES.configureLog({'Server':
+ {'LogStats' : 1,
+ 'Homedir' : homedir,
+ 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+ eq(ES.log.count['AttemptedDelivery']['Y'], 2)
+ # Test dump
+ buf = cStringIO.StringIO()
+ ES.log.dump(buf)
+ sOrig = buf.getvalue()
+ s = "\n".join(sOrig.split("\n")[1:])
+ expected = """\
+ ReceivedPacket: 1
+ AttemptedRelay: 1
+ SuccessfulRelay: 0
+ FailedRelay: 1
+ UnretriableRelay: 1
+ AttemptedDelivery:
+ {Unknown}: 2
+ Y: 2
+ Total: 4
+ SuccessfulDelivery: 1
+ FailedDelivery:
+ {Unknown}: 1
+ Y: 1
+ Total: 2
+ UnretriableDelivery:
+ Y: 1
+ Total: 1
+"""
+ eq(s, expected)
+ # Test time accumultion.
+ ES.log.save(now=time.time()+1800)
+ self.assert_(abs(1800-ES.log.accumulatedTime) < 10)
+ ES.log.lastSave = time.time()+1900
+ ES.log.save(now=time.time()+2000)
+ self.assert_(abs(1900-ES.log.accumulatedTime) < 10)
+ # Test rotate
+ ES.log.save(now=time.time()+3600*24)
+ s2 = readFile(os.path.join(homedir, "stats"))
+ eq(s2, sOrig)
+ eq(ES.log.count['UnretriableDelivery'], {})
+ ES.configureLog({'Server':
+ {'LogStats' : 1,
+ 'Homedir' : homedir,
+ 'StatsInterval' : mixminion.Config._parseInterval("1 hour")}})
+ eq(ES.log.count['UnretriableDelivery'], {})
+ # Test time configured properly.
+ # XXXX
+
+ # Test no rotation if not enough accumulated time
+ #XXXX THIS NEXT PART DOESN'T WORK....
+ ES.log.lastSave = time.time()-1000
+ ES.log._setNextRotation()
+ #print ES.log.nextRotation, time.time()+4000
+ r = ES.log._rotate
+ try:
+ ES.log._rotate = self.fail
+ ES.log.save(now=time.time()+9000)
+ finally:
+ ES.log._rotate = r
+
#----------------------------------------------------------------------
# Modules and ModuleManager
@@ -3977,53 +4046,6 @@
self.assert_(exampleMod.processedAll[0].isPlaintext())
-#### All these tests belong as tests of DeliveryPacket
-
-## # Try a real message, to make sure that we really decode stuff properly
-## msg = mixminion.BuildMessage._encodePayload(
-## "A man disguised as an ostrich, actually.",
-## 0, Crypto.getCommonPRNG())
-## manager.queueMessage(msg, "A"*20, 1234, "Hello")
-## exampleMod.processedAll = []
-## manager.sendReadyMessages()
-## # The retriable message got sent again; the other one, we care about.
-## pos = None
-## for i in xrange(len(exampleMod.processedAll)):
-## if not exampleMod.processedAll[i][0].startswith('Hello'):
-## pos = i
-## self.assert_(pos is not None)
-## self.assertEquals(exampleMod.processedAll[i],
-## ("A man disguised as an ostrich, actually.",
-## None, 1234, "Hello" ))
-
-## # Now a non-decodeable message
-## manager.queueMessage("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches")
-## exampleMod.processedAll = []
-## manager.sendReadyMessages()
-## pos = None
-## for i in xrange(len(exampleMod.processedAll)):
-## if not exampleMod.processedAll[i][0].startswith('Hello'):
-## pos = i
-## self.assert_(pos is not None)
-## self.assertEquals(exampleMod.processedAll[i],
-## ("XYZZYZZY"*3584, "Z"*20, 1234, "Buenas noches"))
-
-## # Now a message that compressed too much.
-## # (first, erase the pending message.)
-## manager.queues[exampleMod.getName()].removeAll()
-## manager.queues[exampleMod.getName()]._rescan()
-
-## p = "For whom is the funhouse fun?"*8192
-## msg = mixminion.BuildMessage._encodePayload(
-## p, 0, Crypto.getCommonPRNG())
-## manager.queueMessage(msg, "Z"*20, 1234, "Buenas noches")
-## exampleMod.processedAll = []
-## self.assertEquals(len(exampleMod.processedAll), 0)
-## manager.sendReadyMessages()
-## self.assertEquals(len(exampleMod.processedAll), 1)
-## self.assertEquals(exampleMod.processedAll[0],
-## (compressData(p), 'long', 1234, "Buenas noches"))
-
# Check serverinfo generation.
try:
suspendLog()
@@ -4275,7 +4297,7 @@
"""Check out the SMTP module. (We temporarily relace sendSMTPMessage
with a stub function so that we don't actually send anything.)"""
FDP = FakeDeliveryPacket
-
+
blacklistFile = mix_mktemp()
writeFile(blacklistFile, "Deny onehost wangafu.net\nDeny user fred\n")
@@ -4560,6 +4582,7 @@
msg = resumeLog()
self.failUnless(len(msg))
Crypto.pk_PEM_save(identity, fn)
+ self.assertEquals(os.stat(fn)[stat.ST_MODE] & 0777, 0600)
# Now create a keyset
now = time.time()
@@ -5266,7 +5289,7 @@
keyring = mixminion.ClientMain.ClientKeyring(keydir)
# Check for some nonexistant keys.
self.assertEquals({}, keyring.getSURBKeys(password="pwd"))
-
+
self.assertEquals(None, keyring.getSURBKey(create=0))
# Reload, try again:
kfirst = None
@@ -5299,7 +5322,7 @@
finally:
s = resumeLog()
self.assert_(stringContains(s, "Incorrect password"))
-
+
def testMixminionClient(self):
# Create and configure a MixminionClient object...
parseAddress = mixminion.ClientMain.parseAddress
@@ -5442,7 +5465,9 @@
tc = loader.loadTestsFromTestCase
if 0:
- suite.addTest(tc(MMTPTests))
+ suite.addTest(tc(ServerInfoTests))
+ #suite.addTest(tc(EventStatsTests))
+ #suite.addTest(tc(MMTPTests))
#suite.addTest(tc(MiscTests))
return suite
@@ -5457,6 +5482,7 @@
suite.addTest(tc(BuildMessageTests))
suite.addTest(tc(PacketHandlerTests))
suite.addTest(tc(QueueTests))
+ suite.addTest(tc(EventStatsTests))
suite.addTest(tc(ModuleTests))
suite.addTest(tc(ClientMainTests))