Exemple #1
0
def _getRouting(path, exitType, exitInfo):
    """Given a list of ServerInfo, and a final exitType and exitInfo,
       return a 3-tuple of:
           1) A list of routingtype/routinginfo tuples for the header
           2) The size (in bytes) added to the header in order to
              route to each of the nodes
           3) Minimum size (in bytes) needed for the header.

       Raises MixError if the routing info is too big to fit into a single
       header. """
    # Construct a list 'routing' of exitType, exitInfo.
    routing = []
    for i in xrange(len(path)-1):
        routing.append(path[i].getRoutingFor(path[i+1],swap=0))
    routing.append((exitType, exitInfo))

    # 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 bytes needed for header
    totalSize = reduce(operator.add, sizes)
    if totalSize > HEADER_LEN:
        raise MixError("Routing info won't fit in header")

    padding = HEADER_LEN-totalSize
    # We can't underflow from the last header.  That means we *must* have
    # enough space to pad the last routinginfo out to a public key size.
    if padding+sizes[-1] < ENC_SUBHEADER_LEN:
        raise MixError("Routing info won't fit in header")

    return routing, sizes, totalSize
Exemple #2
0
 def __setstate__(self, state):
     if type(state) == types.TupleType:
         if state[0] == 'V0':
             self.__dict__.update(state[1])
         else:
             raise MixError("Unrecognized state version %s" % state[0])
     else:
         raise MixError("Unrecognized state type %s"% type(state))
Exemple #3
0
def _decodeEncryptedForwardPayload(payload, tag, key):
    """Helper function: decode an encrypted forward payload.  Return values
       are the same as decodePayload.
             payload: the payload to decode
             tag: the decoding tag
             key: the RSA key of the payload's recipient."""
    assert len(tag) == TAG_LEN
    assert len(payload) == PAYLOAD_LEN

    # Given an N-byte RSA key, the first N bytes of tag+payload will be
    # encrypted with RSA, and the rest with a lioness key given in the
    # first N.  Try decrypting...
    msg = tag+payload
    try:
        rsaPart = Crypto.pk_decrypt(msg[:key.get_modulus_bytes()], key)
    except Crypto.CryptoError:
        return None
    rest = msg[key.get_modulus_bytes():]

    k = Crypto.Keyset(rsaPart[:SECRET_LEN]).getLionessKeys(
        Crypto.END_TO_END_ENCRYPT_MODE)
    rest = rsaPart[SECRET_LEN:] + Crypto.lioness_decrypt(rest, k)

    # ... and then, check the checksum and continue.
    if not _checkPayload(rest):
        raise MixError("Invalid checksum on encrypted forward payload")

    return parsePayload(rest)
Exemple #4
0
def _decodeForwardPayload(payload):
    """Helper function: decode a non-encrypted forward payload. Return values
       are the same as decodePayload."""
    if not _checkPayload(payload):
        raise MixError("Hash doesn't match")

    return parsePayload(payload)
Exemple #5
0
def _readEncryptedFile(fname, password, magicList):
    """Read encrypted data from the file named 'fname', using the password
       'password' and checking for a magic string contained in 'magicList'.
       Returns the magic string and the plaintext file contents on success.

       If the file is corrupt or the password is wrong, raises BadPassword.
       If the magic is incorrect, raises ValueError.
    """
    assert list(map(len, magicList)) == [8] * len(magicList)

    text = readFile(fname)
    r = unarmorText(text, ["TYPE III KEYRING"])
    if len(r) != 1:
        raise ValueError("Bad ascii armor on keyring")
    tp, headers, s = r[0]
    assert tp == "TYPE III KEYRING"
    vers = [v for k, v in headers if k == 'Version']
    if not vers or vers[0] != '0.1':
        raise ValueError("Unrecognized version on keyring")

    if len(s) < MAGIC_LEN + 1 or s[MAGIC_LEN] != '\x00':
        raise ValueError("Unrecognized encryption format on %s" % fname)
    if s[:MAGIC_LEN] not in magicList:
        raise ValueError("Invalid versioning on %s" % fname)
    magic = s[:8]
    s = s[MAGIC_LEN + 1:]
    if len(s) < 28:
        raise MixError("File %s is too short." % fname)
    salt = s[:SALT_LEN]
    s = s[SALT_LEN:]
    key = sha1(salt + password + salt)[:AES_KEY_LEN]
    s = ctr_crypt(s, key)
    data = s[:-DIGEST_LEN]
    digest = s[-DIGEST_LEN:]
    if digest != sha1(data + salt + magic):
        raise BadPassword()

    # We've decrypted it; now let's extract the data from the padding.
    if len(data) < 4:
        raise MixError("File %s is too short" % fname)
    length, = struct.unpack("!L", data[:4])
    if len(data) < length + 4:
        raise MixError("File %s is too short" % fname)

    return magic, data[4:4 + length]
Exemple #6
0
 def rotate(self, now=None):
     if now is None: now = time()
     if now < self.nextRotation:
         raise MixError("Not ready to rotate event stats")
     try:
         self._lock.acquire()
         self._rotate(now)
     finally:
         self._lock.release()
Exemple #7
0
    def __init__(self, s="", now=None):
        """Initialize this keyring representation from the encoded string
           's'.  If any keys are set to expire before 'now', delete them.
        """
        if now is None: now = time.time()

        # Build lists of recongized and unrecognized items in 'input'.
        self.unrecognized = []
        rec = []
        self.dirty = 0
        while s:
            if len(s) < 3:
                raise MixError("Corrupt keyring: truncated entry.")
            tp, length = struct.unpack("!BH", s[:3])
            if len(s) < 3 + length:
                raise MixError("Corrupt keyring: truncated entry.")
            val = s[3:3 + length]
            if tp == self.SURB_KEY_TYPE:
                rec.append((tp, val))
            else:
                self.unrecognized.append((tp, val))
            s = s[3 + length:]

        # Now, extract all the SURB keys from the keyring, and remove all
        # expired SURB keys from self.recognized.
        self.surbKeys = {}
        self.recognized = []
        for tp, val in rec:
            if len(val) < 5 or '\0' not in val[4:]:
                raise MixError("Truncated SURB key")
            expiry, = struct.unpack("!L", val[:4])
            if expiry < now:
                self.dirty = 1
            else:
                self.recognized.append((tp, val))
                val = val[4:]
                identity = val[:val.index('\0')].lower()
                secret = val[val.index('\0') + 1:]
                self.surbKeys.setdefault(identity, []).append((expiry, secret))
Exemple #8
0
    def checkPathSpec(self, pathSpec, messageDest):
        """Given a PathSpec object and a MsgDest object, raise MixError if
           no corresponding valid paths could be generated.
           DOCDOC forSurb
        """
        if pathSpec._forReply and not messageDest.isSURB():
            raise MixError(
                "Can't use a non-SURB destination for a reply message")
        elif messageDest.isSURB() and not pathSpec._forReply():
            raise MixError(
                "Can't use a SURB as a destination for a non-reply message")

        duration = self._getPathDuration(messageDest)
        startAt = time.time()
        endAt = time.time() + duration
        d = self._getClientDirectory()
        parsed = mixminion.ClientDirectory.parsePath(
            config=self._getConfig(),
            path=pathSpec._string,
            isReply=pathSpec._forReply,
            isSURB=pathSpec._forSURB)
        # XXXX Some way to set isSSFragmented
        d.validatePath(parsed, messageDest._getExitAdress(), startAt, endAt)
        pathSpec._parsed = parsed
Exemple #9
0
def buildForwardPacket(payload, exitType, exitInfo, path1, path2,
                       paddingPRNG=None, suppressTag=0):
    """Construct a forward message.
            payload: The payload to deliver.  Must be exactly 28K.  If the
                  payload is None, 28K of random data is sent.
            exitType: The routing type for the final node. (2 bytes, >=0x100)
            exitInfo: The routing info for the final node, not including tag.
            path1: Sequence of ServerInfo objects for the first leg of the path
            path2: Sequence of ServerInfo objects for the 2nd leg of the path
            paddingPRNG: random number generator used to generate padding.
                  If None, a new PRNG is initialized.
            suppressTag: if true, do not include a decodind handle in the
                  routingInfo for this packet.

        Neither path1 nor path2 may be empty.  If one is, MixError is raised.
    """
    if paddingPRNG is None:
        paddingPRNG = Crypto.getCommonPRNG()
    if not path1:
        raise MixError("First leg of path is empty")
    if not path2:
        raise MixError("Second leg of path is empty")

    assert len(payload) == PAYLOAD_LEN

    LOG.trace("  Building packet with path %s:%s; delivering to %04x:%r",
                   ",".join([s.getNickname() for s in path1]),
                   ",".join([s.getNickname() for s in path2]),
                   exitType, exitInfo)

    # Choose a random decoding tag.
    if not suppressTag:
        tag = _getRandomTag(paddingPRNG)
        exitInfo = tag + exitInfo
    return _buildPacket(payload, exitType, exitInfo, path1, path2,
                        paddingPRNG,suppressTag=suppressTag)
Exemple #10
0
 def _loadWithPassword(self, password):
     """Helper function: tries to load the file with a given password.
        If Successful, return 1. Else return 0."""
     try:
         m, val = _readEncryptedFile(self.fname, password,
                                     self.okMagic + self.obsoleteMagic)
         if m in self.obsoleteMagic:
             raise MixError(
                 "Found an obsolete keyring at %r.  Remove this file to use SURBs with this version of Mixminion."
                 % self.fname)
         self._decode(val, m)
         self.password = password
         self.loaded = 1
         return 1
     except MixError:
         return 0
Exemple #11
0
def _decodeReplyPayload(payload, secrets, check=0):
    """Helper function: decode a reply payload, given a known list of packet
         master secrets. If 'check' is true, then 'secrets' may be overlong.
         Return values are the same as decodePayload.
      [secrets must be in _reverse_ order]
    """
    # Reverse the 'decrypt' operations of the reply mixes, and the initial
    # 'decrypt' of the originating user...
    for sec in secrets:
        k = Crypto.Keyset(sec).getLionessKeys(Crypto.PAYLOAD_ENCRYPT_MODE)
        payload = Crypto.lioness_encrypt(payload, k)
        if check and _checkPayload(payload):
            return parsePayload(payload)

    # If 'check' is false, then we might still have a good payload.  If
    # 'check' is true, we don't.
    if check or not _checkPayload(payload):
        raise MixError("Invalid checksum on reply payload")

    return parsePayload(payload)
Exemple #12
0
def encodeMailHeaders(subject=None,
                      fromAddr=None,
                      inReplyTo=None,
                      references=None):
    """Given (optionally) any of the headers permissible for email
       messages, return a string to be prepended to a message before
       encoding.  Raise MixError on failure."""
    headers = {}
    if subject:
        headers['SUBJECT'] = subject
    if fromAddr:
        for badchar in ('"', '[', ']', ':'):
            if badchar in fromAddr:
                raise MixError("Forbidden character %r in from address" %
                               badchar)
        headers['FROM'] = fromAddr
    if inReplyTo:
        headers['IN-REPLY-TO'] = inReplyTo
    if references:
        headers['REFERENCES'] = references
    return encodeMessageHeaders(headers)
Exemple #13
0
def _buildPacket(payload, exitType, exitInfo,
                 path1, path2, paddingPRNG=None, paranoia=0,
                 suppressTag=0):
    """Helper method to create a message.

    The following fields must be set:
       payload: the intended exit payload.  Must be 28K.
       (exitType, exitInfo): the routing type and info for the final
              node.  (Ignored for reply messages; 'exitInfo' should
              include the 20-byte decoding tag.)
       path1: a sequence of ServerInfo objects, one for each node on
          the first leg of the path.
       path2:
        EITHER
             a sequence of ServerInfo objects, one for each node
             on the second leg of the path.
         OR
             a ReplyBlock object.

    The following fields are optional:
       paddingPRNG: A pseudo-random number generator used to pad the headers.
         If not provided, we use a counter-mode AES stream seeded from our
         entropy source.

       paranoia: If this is false, we use the padding PRNG to generate
         header secrets too.  Otherwise, we read all of our header secrets
         from the true entropy source.
    """
    assert len(payload) == PAYLOAD_LEN
    reply = None
    if isinstance(path2, ReplyBlock):
        reply = path2
        path2 = None
    else:
        if len(exitInfo) < TAG_LEN and not suppressTag:
            raise MixError("Implausibly short exit info: %r"%exitInfo)
        if exitType < MIN_EXIT_TYPE and exitType != DROP_TYPE:
            raise MixError("Invalid exit type: %4x"%exitType)

    checkPathLength(path1, path2, exitType, exitInfo,
                    explicitSwap=(reply is None),
                    suppressTag=suppressTag)

    ### SETUP CODE: let's handle all the variant cases.

    # Set up the random number generators.
    if paddingPRNG is None:
        paddingPRNG = Crypto.getCommonPRNG()
    if paranoia:
        nHops = len(path1)
        if path2: nHops += len(path2)
        secretRNG = Crypto.getTrueRNG()
    else:
        secretRNG = paddingPRNG

    # Determine exit routing for path1.
    if reply:
        path1exittype = reply.routingType
        path1exitinfo = reply.routingInfo
    else:
        path1exittype, path1exitinfo = path1[-1].getRoutingFor(path2[0],swap=1)

    # Generate secrets for path1.
    secrets1 = [ secretRNG.getBytes(SECRET_LEN) for _ in path1 ]

    if path2:
        # Make secrets for header 2, and construct header 2.  We do this before
        # making header1 so that our rng won't be used for padding yet.
        secrets2 = [ secretRNG.getBytes(SECRET_LEN) for _ in range(len(path2))]
        header2 = _buildHeader(path2,secrets2,exitType,exitInfo,paddingPRNG)
    else:
        secrets2 = None
        header2 = reply.header

    # Construct header1.
    header1 = _buildHeader(path1,secrets1,path1exittype,path1exitinfo,
                           paddingPRNG)

    return _constructMessage(secrets1, secrets2, header1, header2, payload)
Exemple #14
0
def decodePayload(payload, tag, key=None, userKeys=(), retNym=None):
    """Given a 28K payload and a 20-byte decoding tag, attempt to decode the
       original message.  Returns either a SingletonPayload instance, a
       FragmentPayload instance, or None.

           key: an RSA key to decode encrypted forward messages, or None
           userKeys: a sequence of (name,key) tuples maping identity names to
                SURB keys. For backward compatibility, 'userKeys' may also be
                None (no SURBs known), a dict (from name to key), or a single
                key (implied identity is "").
           retNym: If present, and if the payload was a reply, we call
                retNym.append(pseudonym).  (For the default SURB identity,
                we append the empty string.)

       If we can successfully decrypt the payload, we return it.  If we
       might be able to decrypt the payload given more/different keys,
       we return None.  If the payload is corrupt, we raise MixError.
    """
    if userKeys is None:
        userKeys = []
    elif type(userKeys) is types.StringType:
        userKeys = [ ("", userKeys) ]
    elif type(userKeys) is types.DictType:
        userKeys = userKeys.items()

    if len(payload) != PAYLOAD_LEN:
        raise MixError("Wrong payload length")

    if len(tag) not in (0, TAG_LEN):
        raise MixError("Wrong tag length: %s"%len(tag))

    # If the payload already contains a valid checksum, it's a forward
    # message.
    if _checkPayload(payload):
        return parsePayload(payload)

    if not tag:
        return None

    # If H(tag|userKey|"Validate") ends with 0, then the message _might_
    # be a reply message using H(tag|userKey|"Generate") as the seed for
    # its master secrets.  (There's a 1-in-256 chance that it isn't.)
    for name,userKey in userKeys:
        if Crypto.sha1(tag+userKey+"Validate")[-1] == '\x00':
            try:
                p = _decodeStatelessReplyPayload(payload, tag, userKey)
                if name:
                    LOG.info("Decoded reply message to identity %r", name)
                if retNym is not None:
                    retNym.append(name)
                return p
            except MixError:
                pass

    # If we have an RSA key, and none of the above steps get us a good
    # payload, then we may as well try to decrypt the start of tag+key with
    # our RSA key.
    if key is not None:
        p = _decodeEncryptedForwardPayload(payload, tag, key)
        if p is not None:
            return p

    return None
Exemple #15
0
                    string=str(server), validatedDigests=validatedDigests,
                    _keepContents=1)
        except ConfigError,e:
            LOG.warn("Rejecting malformed serverinfo: %s",e)
        else:
            valid.append(s)

    val = _generateDirectory(identity, 'vote', valid, goodServerNames,
                             voters, validAfter,
                             clientVersions, serverVersions)

    try:
        directory = mixminion.ServerInfo.SignedDirectory(
            string=val, validatedDigests=validatedDigests)
    except ConfigError,e:
        raise MixError("Generated a vote directory that we cannot parse: %s"%e)

    try:
        checkVoteDirectory(voters, validAfter, directory)
    except BadVote, e:
        raise MixError("Generated unacceptable vote directory: %s"%e)

    return val

def generateConsensusDirectory(identity, voters, validAfter, directories,
                               validatedDigests=None):
    # directories is (source, stringable) list

    # First -- whom shall we vote with?
    goodDirectories = {} # {fingerprint: (src,SignedDirectory)}
    serverMap = {} # digest->server info
Exemple #16
0
def _buildHeader(path,secrets,exitType,exitInfo,paddingPRNG):
    """Helper method to construct a single header.
           path: A sequence of serverinfo objects.
           secrets: A list of 16-byte strings to use as master-secrets for
               each of the subheaders.
           exitType: The routing type for the last node in the header
           exitInfo: The routing info for the last node in the header.
               (Must include 20-byte decoding tag, if any.)
           paddingPRNG: A pseudo-random number generator to generate padding
    """
    assert len(path) == len(secrets)

    for info in path:
        if not info.supportsPacketVersion():
            raise MixError("Server %s does not support any recognized packet format."%info.getNickname())

    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 ]

    # 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
    #                sees no junk.
    junkSeen = [""]
    for secret, headerKey, size in zip(secrets, headerKeys, sizes):
        # Here we're calculating the junk that node i+1 will see.
        #
        # 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 data that i removes.)
        newJunk = Crypto.prng(prngKey,size)
        lastJunk = junkSeen[-1]
        nextJunk = lastJunk + newJunk

        # 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(paddingLen)

    # Now, we build the subheaders, iterating through the nodes backwards.
    for i in range(len(path)-1, -1, -1):
        rt, ri = routing[i]

        # Create a subheader object for this node, but don't fill in the
        # digest until we've constructed the rest of the header.
        subhead = Subheader(MAJOR_NO, MINOR_NO,
                            secrets[i],
                            None, #placeholder for as-yet-uncalculated digest
                            rt, ri)

        # 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?  If so, prepend it.
        header = subhead.getOverflow() + header

        # Encrypt the symmetrically encrypted part of the header
        header = Crypto.ctr_crypt(header, headerKeys[i])

        # What digest will the next server see?
        subhead.digest = Crypto.sha1(header+junkSeen[i])

        # Encrypt the subheader, plus whatever portion of the previous header
        # underflows, into 'esh'.
        pubkey = path[i].getPacketKey()
        rsaPart = subhead.pack() + underflow
        esh = Crypto.pk_encrypt(rsaPart, pubkey)

        # Concatenate the asymmetric and symmetric parts, to get the next
        # header.
        header = esh + header

    return header
Exemple #17
0
    def generateDirectory(self,
                          startAt,
                          endAt,
                          extraTime,
                          identityKey,
                          publicationTime=None,
                          badServers=(),
                          excludeServers=()):
        """Generate and sign a new directory, to be effective from <startAt>
           through <endAt>.  It includes all servers that are valid at
           any time between <startAt> and <endAt>+<extraTime>.  The directory
           is signed with <identityKey>.

           Any servers whose nicknames appear in 'badServers' are marked as
           not recommended; any servers whose nicknames appear in
           'excludeServers' are left off the directory entirely.
        """
        try:
            self._lock()
            self.clean()
            if publicationTime is None:
                publicationTime = time.time()
            if previousMidnight(startAt) >= previousMidnight(endAt):
                raise MixError("Validity range does not contain a full day.")

            excludeServers = [nickname.lower() for nickname in excludeServers]

            # First, sort all servers by nickname.
            includedByNickname = {}
            for fn, s in self.servers.items():
                nickname = s.getNickname().lower()
                if nickname in excludeServers: continue
                includedByNickname.setdefault(nickname, []).append((s, fn))

            # Second, find all servers that are valid for part of the period,
            # and that aren't superseded for the whole period.
            timeRange = IntervalSet([(previousMidnight(startAt),
                                      endAt + extraTime)])

            for nickname, ss in includedByNickname.items():
                # We prefer the most-recently-published descriptor.  If two
                # are published at the same time, we prefer the one that
                # expires last.
                ss = [(s['Server']['Published'], s['Server']['Valid-Until'], s,
                       fn) for s, fn in ss]
                ss.sort()
                ss.reverse()
                uncovered = timeRange.copy()
                included = []
                for _, _, s, fn in ss:
                    valid = s.getIntervalSet()
                    if (uncovered * valid):
                        included.append((s, fn))
                        uncovered -= valid
                includedByNickname[nickname] = included

            # Now sort the remaining servers by nickname, then by valid-after.
            included = []
            for ss in includedByNickname.values():
                for s, fn in ss:
                    nickname = s.getNickname()
                    validAfter = s['Server']['Valid-After']
                    included.append((nickname, validAfter, fn))
            included.sort()

            # FFFF We should probably not do all of this in RAM, but
            # FFFF what the hey.  It will only matter if we have many, many
            # FFFF servers in the system.
            contents = []
            for _, _, fn in included:
                txt = readFile(os.path.join(self.serverDir, fn))
                contents.append(txt)

            goodServers = [n for n, _, _ in included if n not in badServers]
            g = {}
            for n in goodServers:
                g[n] = 1
            goodServers = g.keys()
            goodServers.sort()
            goodServers = ", ".join(goodServers)

            clientVersions = self.config['Directory']['ClientVersions']
            serverVersions = self.config['Directory']['ServerVersions']

            #FFFF Support for multiple signatures
            header = """\
            [Directory]
            Version: 0.2
            Published: %s
            Valid-After: %s
            Valid-Until: %s
            Recommended-Servers: %s
            [Signature]
            DirectoryIdentity: %s
            DirectoryDigest:
            DirectorySignature:
            [Recommended-Software]
            MixminionClient: %s
            MixminionServer: %s
            """ % (formatTime(publicationTime), formatDate(startAt),
                   formatDate(endAt), goodServers,
                   formatBase64(pk_encode_public_key(identityKey)),
                   ", ".join(clientVersions), ", ".join(serverVersions))

            directory = header + "".join(contents)
            directory = _getDirectoryDigestImpl(directory, identityKey)

            # Make sure that the directory checks out
            # FFFF remove this once we are _very_ confident.
            if 1:
                parsed = ServerDirectory(string=directory)
                includedDigests = {}
                for _, _, fn in included:
                    includedDigests[self.servers[fn]['Server']['Digest']] = 1
                foundDigests = {}
                for s in parsed.getAllServers():
                    foundDigests[s['Server']['Digest']] = 1
                assert foundDigests == includedDigests

            writeFile(os.path.join(self.baseDir, "directory"),
                      directory,
                      mode=0644)

            f, _ = openUnique(
                os.path.join(self.dirArchiveDir, "dir-" + formatFnameTime()))
            f.write(directory)
            f.close()
        finally:
            self._unlock()