def _startSendingNextPacket(self): "Helper: begin transmitting the next available packet." # There _is_ a next available packet, right? assert self.packets and self._isConnected pkt = self.packets.pop(0) if pkt.isJunk(): control = "JUNK\r\n" serverControl = "RECEIVED\r\n" hashExtra = "JUNK" serverHashExtra = "RECEIVED JUNK" else: control = "SEND\r\n" serverControl = "RECEIVED\r\n" hashExtra = "SEND" serverHashExtra = "RECEIVED" EventStats.log.attemptedRelay() m = pkt.getContents() if m == 'RENEGOTIATE': # Renegotiate has been removed from the spec. return data = "".join([control, m, sha1(m + hashExtra)]) assert len(data) == self.MESSAGE_LEN acceptedAck = serverControl + sha1(m + serverHashExtra) rejectedAck = "REJECTED\r\n" + sha1(m + "REJECTED") assert len(acceptedAck) == len(rejectedAck) == self.ACK_LEN self.expectedAcks.append((acceptedAck, rejectedAck)) self.pendingPackets.append(pkt) self.beginWriting(data) self.nPacketsSent += 1
def _startSendingNextPacket(self): "Helper: begin transmitting the next available packet." # There _is_ a next available packet, right? assert self.packets and self._isConnected pkt = self.packets.pop(0) if pkt.isJunk(): control = "JUNK\r\n" serverControl = "RECEIVED\r\n" hashExtra = "JUNK" serverHashExtra = "RECEIVED JUNK" else: control = "SEND\r\n" serverControl = "RECEIVED\r\n" hashExtra = "SEND" serverHashExtra = "RECEIVED" EventStats.elog.attemptedRelay() m = pkt.getContents() if m == 'RENEGOTIATE': # Renegotiate has been removed from the spec. return data = "".join([control, m, sha1(m+hashExtra)]) assert len(data) == self.MESSAGE_LEN acceptedAck = serverControl + sha1(m+serverHashExtra) rejectedAck = "REJECTED\r\n" + sha1(m+"REJECTED") assert len(acceptedAck) == len(rejectedAck) == self.ACK_LEN self.expectedAcks.append( (acceptedAck, rejectedAck) ) self.pendingPackets.append(pkt) self.beginWriting(data) self.nPacketsSent += 1
def onDataRead(self): while self.inbuflen >= self.MESSAGE_LEN: data = self.getInbuf(self.MESSAGE_LEN, clear=1) control = data[:SEND_CONTROL_LEN] pkt = data[SEND_CONTROL_LEN:-DIGEST_LEN] digest = data[-DIGEST_LEN:] if control == JUNK_CONTROL: expectedDigest = sha1(pkt + "JUNK") replyDigest = sha1(pkt + "RECEIVED JUNK") replyControl = RECEIVED_CONTROL isJunk = 1 elif control == SEND_CONTROL: expectedDigest = sha1(pkt + "SEND") if self.rejectPackets: replyDigest = sha1(pkt + "REJECTED") replyControl = REJECTED_CONTROL else: replyDigest = sha1(pkt + "RECEIVED") replyControl = RECEIVED_CONTROL isJunk = 0 else: LOG.warn( "Unrecognized command (%r) from %s. Closing connection.", control, self.address) #failed self.startShutdown() return if expectedDigest != digest: LOG.warn("Invalid checksum from %s. Closing connection.", self.address) #failed self.startShutdown() return else: if isJunk: LOG.debug("Link padding received from %s; Checksum valid.", self.address) else: LOG.debug("Packet received from %s; Checksum valid.", self.address) # Make sure we process the packet before we queue the ack. if isJunk: self.junkCallback() elif self.rejectPackets: self.rejectCallback() else: self.packetConsumer(pkt) # Queue the ack. self.beginWriting(replyControl + replyDigest)
def onDataRead(self): while self.inbuflen >= self.MESSAGE_LEN: data = self.getInbuf(self.MESSAGE_LEN, clear=1) control = data[:SEND_CONTROL_LEN] pkt = data[SEND_CONTROL_LEN:-DIGEST_LEN] digest = data[-DIGEST_LEN:] if control == JUNK_CONTROL: expectedDigest = sha1(pkt+"JUNK") replyDigest = sha1(pkt+"RECEIVED JUNK") replyControl = RECEIVED_CONTROL isJunk = 1 elif control == SEND_CONTROL: expectedDigest = sha1(pkt+"SEND") if self.rejectPackets: replyDigest = sha1(pkt+"REJECTED") replyControl = REJECTED_CONTROL else: replyDigest = sha1(pkt+"RECEIVED") replyControl = RECEIVED_CONTROL isJunk = 0 else: LOG.warn("Unrecognized command (%r) from %s. Closing connection.", control, self.address) #failed self.startShutdown() return if expectedDigest != digest: LOG.warn("Invalid checksum from %s. Closing connection.", self.address) #failed self.startShutdown() return else: if isJunk: LOG.debug("Link padding received from %s; Checksum valid.", self.address) else: LOG.debug("Packet received from %s; Checksum valid.", self.address) # Make sure we process the packet before we queue the ack. if isJunk: self.junkCallback() elif self.rejectPackets: self.rejectCallback() else: self.packetConsumer(pkt) # Queue the ack. self.beginWriting(replyControl+replyDigest)
def reconstruct(self, store): """If any of the chunks in this message are pending reconstruction, reconstruct them in a given store.""" if not self.readyChunks: return for chunkno in self.readyChunks.keys(): # Get the first K fragments in the chunk. (list of h,fm) ch = self.fragmentsByChunk[chunkno].values()[:self.params.k] minDate = min([fm.insertedDate for h, fm in ch]) # Build a list of (position-within-chunk, fragment-contents). frags = [(self.params.getPosition(fm.idx)[1], store.messageContents(h)) for h,fm in ch] chunkText = "".join(self.params.getFEC().decode(frags)) del frags fm2 = FragmentMetadata(messageid=self.messageid, idx=chunkno, size=self.params.length, isChunk=1, chunkNum=chunkno, overhead=self.overhead, insertedDate=minDate, nym=self.nym, digest=sha1(chunkText)) # Queue the chunk. h2 = store.queueMessageAndMetadata(chunkText, fm2) del chunkText # Remove superceded fragments. for h, fm in ch: store.removeMessage(h) # Update this MessageState object. self.fragmentsByChunk[chunkno] = {} del self.readyChunks[chunkno] self.addChunk(h2, fm2)
def reconstruct(self, store): """If any of the chunks in this message are pending reconstruction, reconstruct them in a given store.""" if not self.readyChunks: return for chunkno in self.readyChunks.keys(): # Get the first K fragments in the chunk. (list of h,fm) ch = self.fragmentsByChunk[chunkno].values()[:self.params.k] minDate = min([fm.insertedDate for h, fm in ch]) # Build a list of (position-within-chunk, fragment-contents). frags = [(self.params.getPosition(fm.idx)[1], store.messageContents(h)) for h, fm in ch] chunkText = "".join(self.params.getFEC().decode(frags)) del frags fm2 = FragmentMetadata(messageid=self.messageid, idx=chunkno, size=self.params.length, isChunk=1, chunkNum=chunkno, overhead=self.overhead, insertedDate=minDate, nym=self.nym, digest=sha1(chunkText)) # Queue the chunk. h2 = store.queueMessageAndMetadata(chunkText, fm2) del chunkText # Remove superceded fragments. for h, fm in ch: store.removeMessage(h) # Update this MessageState object. self.fragmentsByChunk[chunkno] = {} del self.readyChunks[chunkno] self.addChunk(h2, fm2)
def _writeEncryptedFile(fname, password, magic, data): """Write 'data' into an encrypted file named 'fname', replacing it if necessary. Encrypts the data with the password 'password', and uses the filetype 'magic'.""" assert len(magic) == MAGIC_LEN prng = getCommonPRNG() length = struct.pack("!L", len(data)) paddingLen = ceilDiv(len(data), 1024) * 1024 - len(data) padding = prng.getBytes(paddingLen) data = "".join([length, data, padding]) salt = prng.getBytes(SALT_LEN) key = sha1(salt + password + salt)[:AES_KEY_LEN] digest = sha1("".join([data, salt, magic])) encrypted = ctr_crypt(data + digest, key) contents = "".join([magic, "\x00", salt, encrypted]) writeFile(fname, armorText(contents, "TYPE III KEYRING", [("Version", "0.1")]))
def _writeEncryptedFile(fname, password, magic, data): """Write 'data' into an encrypted file named 'fname', replacing it if necessary. Encrypts the data with the password 'password', and uses the filetype 'magic'.""" assert len(magic) == MAGIC_LEN prng = getCommonPRNG() length = struct.pack("!L", len(data)) paddingLen = ceilDiv(len(data), 1024)*1024 - len(data) padding = prng.getBytes(paddingLen) data = "".join([length,data,padding]) salt = prng.getBytes(SALT_LEN) key = sha1(salt+password+salt)[:AES_KEY_LEN] digest = sha1("".join([data,salt,magic])) encrypted = ctr_crypt(data+digest, key) contents = "".join([magic,"\x00",salt,encrypted]) writeFile(fname, armorText(contents, "TYPE III KEYRING", [("Version","0.1")]))
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]
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]
def _getMultisignedDirectoryDigest(directory): try: if directory.startswith("[Directory-Info]"): idx = 0 else: idx = directory.index("\n[Directory-Info]\n") + 1 except IndexError: raise ConfigError("No [Directory-Info] found.") digest = sha1(directory[idx:]) return digest
def _getMultisignedDirectoryDigest(directory): try: if directory.startswith("[Directory-Info]"): idx = 0 else: idx = directory.index("\n[Directory-Info]\n")+1 except IndexError: raise ConfigError("No [Directory-Info] found.") digest = sha1(directory[idx:]) return digest
def _getDigestImpl(info, regex, digestField=None, sigField=None, rsa=None): """Helper method. Calculates the correct digest of a server descriptor or directory (as provided in a string). If rsa is provided, signs the digest and creates a new descriptor. Otherwise just returns the digest. info -- the string to digest or sign. regex -- a compiled regex that matches the line containing the digest and the line containing the signature. digestField -- If not signing, None. Otherwise, the name of the digest field. sigField -- If not signing, None. Otherwise, the name of the signature field. rsa -- our public key """ info = _cleanForDigest(info) def replaceFn(m): s = m.group(0) return s[:s.index(':') + 1] info = regex.sub(replaceFn, info, 2) digest = sha1(info) if rsa is None: return digest signature = pk_sign(digest, rsa) digest = formatBase64(digest) signature = formatBase64(signature) def replaceFn2(s, digest=digest, signature=signature, digestField=digestField, sigField=sigField): if s.group(0).startswith(digestField): return "%s: %s" % (digestField, digest) else: assert s.group(0).startswith(sigField) return "%s: %s" % (sigField, signature) info = regex.sub(replaceFn2, info, 2) return info
def _getDigestImpl(info, regex, digestField=None, sigField=None, rsa=None): """Helper method. Calculates the correct digest of a server descriptor or directory (as provided in a string). If rsa is provided, signs the digest and creates a new descriptor. Otherwise just returns the digest. info -- the string to digest or sign. regex -- a compiled regex that matches the line containing the digest and the line containing the signature. digestField -- If not signing, None. Otherwise, the name of the digest field. sigField -- If not signing, None. Otherwise, the name of the signature field. rsa -- our public key """ info = _cleanForDigest(info) def replaceFn(m): s = m.group(0) return s[:s.index(':')+1] info = regex.sub(replaceFn, info, 2) digest = sha1(info) if rsa is None: return digest signature = pk_sign(digest,rsa) digest = formatBase64(digest) signature = formatBase64(signature) def replaceFn2(s, digest=digest, signature=signature, digestField=digestField, sigField=sigField): if s.group(0).startswith(digestField): return "%s: %s" % (digestField, digest) else: assert s.group(0).startswith(sigField) return "%s: %s" % (sigField, signature) info = regex.sub(replaceFn2, info, 2) return info
def getIdentityDigest(self): """Return the digest of this server's public identity key. (SHA-1 digest of ASN.1-encoded key). """ return sha1(pk_encode_public_key(self.getIdentity()))
def getKeyDigest(self): """Returns a hash of this server's identity key.""" return sha1(pk_encode_public_key(self['Server']['Identity']))
def addFragment(self, fragmentPacket, nym=None, now=None, verbose=0): """Given an instance of mixminion.Packet.FragmentPayload, record the fragment if appropriate and update the state of the fragment pool if necessary. Returns the message ID that was updated, or None if the fragment was redundant or misformed. fragmentPacket -- the new fragment to add. nym -- a string representing the identity that received this fragment. [Tracking nyms is important, to prevent an attack where we send 2 fragments to 'MarkTwain' and 2 fragments to 'SClemens', and see that the message is reconstructed.] verbose -- if true, log information at the INFO level; otherwise, log at DEBUG. """ if verbose: say = LOG.info else: say = LOG.debug if now is None: now = time.time() today = previousMidnight(now) # If the message has already been rejected or completed, we can # drop this packet. s = self.db.getStatusAndTime(fragmentPacket.msgID) if s: say("Dropping fragment of %s message %r", s[0].lower(), disp64(fragmentPacket.msgID,12)) return None # Otherwise, create a new metadata object for this fragment... meta = FragmentMetadata(messageid=fragmentPacket.msgID, idx=fragmentPacket.index, size=fragmentPacket.msgLen, isChunk=0, chunkNum=None, overhead=fragmentPacket.getOverhead(), insertedDate=today, nym=nym, digest=sha1(fragmentPacket.data)) # ... and allocate or find the MessageState for this message. state = self._getState(meta) try: # Check whether we can/should add this message, but do not # add it. state.addFragment(None, meta, noop=1) # No exception was thrown; queue the message. h = self.store.queueMessageAndMetadata(fragmentPacket.data, meta) # And *now* update the message state. state.addFragment(h, meta) say("Stored fragment %s of message %s", fragmentPacket.index+1, disp64(fragmentPacket.msgID,12)) return fragmentPacket.msgID except MismatchedFragment, s: # Remove the other fragments, mark msgid as bad. LOG.warn("Found inconsistent fragment %s in message %s: %s", fragmentPacket.index+1, disp64(fragmentPacket.msgID,12), s) self._deleteMessageIDs({ meta.messageid : 1}, "REJECTED", now) return None
def computeHash(self): """Update the hash field of this payload to correspond to the hash of the data.""" self.hash = sha1(self.data)
def _encodeKey(self, surb): return binascii.b2a_hex(sha1(surb.pack()))
def computeHash(self): """Update the hash field of this payload to correspond to the hash of the data.""" self.hash = "X" * DIGEST_LEN p = self.pack() self.hash = sha1(p[23:])
def getHexDigest(self): """DOCDOC""" return binascii.b2a_hex(sha1(self.pack()))
def addFragment(self, fragmentPacket, nym=None, now=None, verbose=0): """Given an instance of mixminion.Packet.FragmentPayload, record the fragment if appropriate and update the state of the fragment pool if necessary. Returns the message ID that was updated, or None if the fragment was redundant or misformed. fragmentPacket -- the new fragment to add. nym -- a string representing the identity that received this fragment. [Tracking nyms is important, to prevent an attack where we send 2 fragments to 'MarkTwain' and 2 fragments to 'SClemens', and see that the message is reconstructed.] verbose -- if true, log information at the INFO level; otherwise, log at DEBUG. """ if verbose: say = LOG.info else: say = LOG.debug if now is None: now = time.time() today = previousMidnight(now) # If the message has already been rejected or completed, we can # drop this packet. s = self.db.getStatusAndTime(fragmentPacket.msgID) if s: say("Dropping fragment of %s message %r", s[0].lower(), disp64(fragmentPacket.msgID, 12)) return None # Otherwise, create a new metadata object for this fragment... meta = FragmentMetadata(messageid=fragmentPacket.msgID, idx=fragmentPacket.index, size=fragmentPacket.msgLen, isChunk=0, chunkNum=None, overhead=fragmentPacket.getOverhead(), insertedDate=today, nym=nym, digest=sha1(fragmentPacket.data)) # ... and allocate or find the MessageState for this message. state = self._getState(meta) try: # Check whether we can/should add this message, but do not # add it. state.addFragment(None, meta, noop=1) # No exception was thrown; queue the message. h = self.store.queueMessageAndMetadata(fragmentPacket.data, meta) # And *now* update the message state. state.addFragment(h, meta) say("Stored fragment %s of message %s", fragmentPacket.index + 1, disp64(fragmentPacket.msgID, 12)) return fragmentPacket.msgID except MismatchedFragment, s: # Remove the other fragments, mark msgid as bad. LOG.warn("Found inconsistent fragment %s in message %s: %s", fragmentPacket.index + 1, disp64(fragmentPacket.msgID, 12), s) self._deleteMessageIDs({meta.messageid: 1}, "REJECTED", now) return None
def computeHash(self): """Update the hash field of this payload to correspond to the hash of the data.""" self.hash = "X"*DIGEST_LEN p = self.pack() self.hash = sha1(p[23:])
return # All is well. else: # We recognize the key, but some other identity signed it. raise MixProtocolBadAuth( "Mismatch between expected and actual key ID") except KeyError: pass # We haven't found an identity for this pk yet. Try to check the # signature on it. try: identity = tls.verify_cert_and_get_identity_pk() except _ml.TLSError, e: raise MixProtocolBadAuth("Invalid KeyID (allegedly) from %s: %s" % serverName) # Okay, remember who has signed this certificate. hashed_identity = sha1(identity.encode_key(public=1)) LOG.trace("Remembering valid certificate for %s", serverName) self.cache[hashed_peer_pk] = hashed_identity # Note: we don't need to worry about two identities signing the # same certificate. While this *is* possible to do, it's useless: # You could get someone else's certificate and sign it, but you # couldn't start up a TLS connection with that certificate without # stealing their private key too. # Was the signer the right person? if hashed_identity != targetKeyID: raise MixProtocolBadAuth("Invalid KeyID for %s" % serverName)
return # All is well. else: # We recognize the key, but some other identity signed it. raise MixProtocolBadAuth( "Mismatch between expected and actual key ID") except KeyError: pass # We haven't found an identity for this pk yet. Try to check the # signature on it. try: identity = tls.verify_cert_and_get_identity_pk() except _ml.TLSError, e: raise MixProtocolBadAuth("Invalid KeyID (allegedly) from %s: %s" %serverName) # Okay, remember who has signed this certificate. hashed_identity = sha1(identity.encode_key(public=1)) log.trace("Remembering valid certificate for %s", serverName) self.cache[hashed_peer_pk] = hashed_identity # Note: we don't need to worry about two identities signing the # same certificate. While this *is* possible to do, it's useless: # You could get someone else's certificate and sign it, but you # couldn't start up a TLS connection with that certificate without # stealing their private key too. # Was the signer the right person? if hashed_identity != targetKeyID: raise MixProtocolBadAuth("Invalid KeyID for %s" % serverName)
class PeerCertificateCache: """A PeerCertificateCache validates certificate chains from MMTP servers, and remembers which chains we've already seen and validated.""" ## Fields # cache: A map from peer (temporary) KeyID's to a (signing) KeyID. def __init__(self): self.cache = {} def check(self, tls, targetKeyID, serverName): """Check whether the certificate chain on the TLS connection 'tls' is valid, current, and matches the keyID 'targetKeyID'. If so, return. If not, raise MixProtocolBadAuth. Display all messages using the server 'serverName'. """ # First, make sure the certificate is neither premature nor expired. try: tls.check_cert_alive() except _ml.TLSError, e: s = str(e) skewed = 0 notBefore, notAfter = tls.get_cert_lifetime() # XXXX 'stringContains' is not the best possible check here... if stringContains(s, "expired"): s += " [expired at %s]" % notAfter skewed = 1 elif stringContains(s, "not yet valid"): s += " [not valid until %s]" % notBefore skewed = 1 if skewed: s += " (One of you may have a skewed clock or wrong time zone)" raise MixProtocolBadAuth("Invalid certificate from %s: %s " % (serverName, s)) # If we don't care whom we're talking to, we don't need to check # them out. if targetKeyID is None: return # Get the KeyID for the peer (temporary) key. hashed_peer_pk = sha1(tls.get_peer_cert_pk().encode_key(public=1)) # Before 0.0.4alpha, a server's keyID was a hash of its current # TLS public key. In 0.0.4alpha, we allowed this for backward # compatibility. As of 0.0.4alpha2, since we've dropped backward # compatibility with earlier packet formats, we drop certificate # compatibility as well. if targetKeyID == hashed_peer_pk: raise MixProtocolBadAuth( "Pre-0.0.4 (non-rotatable) certificate from %s" % serverName) try: if targetKeyID == self.cache[hashed_peer_pk]: # We recognize the key, and have already seen it to be # signed by the target identity. LOG.trace("Got a cached certificate from %s", serverName) return # All is well. else: # We recognize the key, but some other identity signed it. raise MixProtocolBadAuth( "Mismatch between expected and actual key ID") except KeyError: pass # We haven't found an identity for this pk yet. Try to check the # signature on it. try: identity = tls.verify_cert_and_get_identity_pk() except _ml.TLSError, e: raise MixProtocolBadAuth("Invalid KeyID (allegedly) from %s: %s" % serverName)