def createHandshake(self): """ Create and return a ready-to-be-sent UniformDH handshake. The returned handshake data includes the public key, pseudo-random padding, the mark and the HMAC. If a UniformDH object has not been initialised yet, a new instance is created. """ assert self.sharedSecret is not None log.debug("Creating UniformDH handshake message.") if self.udh is None: self.udh = obfs3_dh.UniformDH() publicKey = self.udh.get_public() assert (const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH) >= 0 # Subtract the length of the public key to make the handshake on # average as long as a redeemed ticket. That should thwart statistical # length-based attacks. padding = mycrypto.strongRandom( random.randint(0, const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH)) # Add a mark which enables efficient location of the HMAC. mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey) # Authenticate the handshake including the current approximate epoch. mac = mycrypto.HMAC_SHA256_128( self.sharedSecret, publicKey + padding + mark + util.getEpoch()) return publicKey + padding + mark + mac
def extractPublicKey(self, data, srvState=None): """ Extract and return a UniformDH public key out of `data'. Before the public key is touched, the HMAC is verified. If the HMAC is invalid or some other error occurs, `False' is returned. Otherwise, the public key is returned. The extracted data is finally drained from the given `data' object. """ assert self.sharedSecret is not None # Do we already have the minimum amount of data? if len(data) < (const.PUBLIC_KEY_LENGTH + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH): return False log.debug("Attempting to extract the remote machine's UniformDH " "public key out of %d bytes of data." % len(data)) handshake = data.peek() # First, find the mark to efficiently locate the HMAC. publicKey = handshake[:const.PUBLIC_KEY_LENGTH] mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey) index = util.locateMark(mark, handshake) if not index: return False # Now that we know where the authenticating HMAC is: verify it. hmacStart = index + const.MARK_LENGTH existingHMAC = handshake[hmacStart:(hmacStart + const.HMAC_SHA256_128_LENGTH)] myHMAC = mycrypto.HMAC_SHA256_128( self.sharedSecret, handshake[0:hmacStart] + util.getEpoch()) if not util.isValidHMAC(myHMAC, existingHMAC, self.sharedSecret): log.warning("The HMAC is invalid: `%s' vs. `%s'." % (myHMAC.encode('hex'), existingHMAC.encode('hex'))) return False # Do nothing if the ticket is replayed. Immediately closing the # connection would be suspicious. if srvState is not None and srvState.isReplayed(existingHMAC): log.warning("The HMAC was already present in the replay table.") return False data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH) if srvState is not None: log.debug("Adding the HMAC authenticating the UniformDH message " \ "to the replay table: %s." % existingHMAC.encode('hex')) srvState.registerKey(existingHMAC) return handshake[:const.PUBLIC_KEY_LENGTH]
def createHandshake(self, srvState=None): """ Create and return a ready-to-be-sent UniformDH handshake. The returned handshake data includes the public key, pseudo-random padding, the mark and the HMAC. If a UniformDH object has not been initialised yet, a new instance is created. """ assert self.sharedSecret is not None log.debug("Creating UniformDH handshake message.") if self.udh is None: self.udh = obfs3_dh.UniformDH() publicKey = self.udh.get_public() assert (const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH) >= 0 # Subtract the length of the public key to make the handshake on # average as long as a redeemed ticket. That should thwart statistical # length-based attacks. padding = mycrypto.strongRandom( random.randint(0, const.MAX_PADDING_LENGTH - const.PUBLIC_KEY_LENGTH)) # Add a mark which enables efficient location of the HMAC. mark = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey) if self.echoEpoch is None: epoch = util.getEpoch() else: epoch = self.echoEpoch log.debug("Echoing epoch rather than recreating it.") # Authenticate the handshake including the current approximate epoch. mac = mycrypto.HMAC_SHA256_128(self.sharedSecret, publicKey + padding + mark + epoch) if self.weAreServer and (srvState is not None): log.debug("Adding the HMAC authenticating the server's UniformDH " "message to the replay table: %s." % mac.encode('hex')) srvState.registerKey(mac) return publicKey + padding + mark + mac
def createTicketMessage(rawTicket, HMACKey): """ Create and return a ready-to-be-sent ticket authentication message. Pseudo-random padding and a mark are added to `rawTicket' and the result is then authenticated using `HMACKey' as key for a HMAC. The resulting authentication message is then returned. """ assert len(rawTicket) == const.TICKET_LENGTH assert len(HMACKey) == const.TICKET_HMAC_KEY_LENGTH # Subtract the length of the ticket to make the handshake on # average as long as a UniformDH handshake message. padding = mycrypto.strongRandom( random.randint(0, const.MAX_PADDING_LENGTH - const.TICKET_LENGTH)) mark = mycrypto.HMAC_SHA256_128(HMACKey, rawTicket) hmac = mycrypto.HMAC_SHA256_128( HMACKey, rawTicket + padding + mark + util.getEpoch()) return rawTicket + padding + mark + hmac
def isValidHMAC( hmac1, hmac2, key ): """ Compares `hmac1' and `hmac2' after HMACing them again using `key'. The arguments `hmac1' and `hmac2' are compared. If they are equal, `True' is returned and otherwise `False'. To prevent timing attacks, double HMAC verification is used meaning that the two arguments are HMACed again before (variable-time) string comparison. The idea is taken from: https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx """ assert len(hmac1) == len(hmac2) # HMAC the arguments again to prevent timing attacks. doubleHmac1 = mycrypto.HMAC_SHA256_128(key, hmac1) doubleHmac2 = mycrypto.HMAC_SHA256_128(key, hmac2) if doubleHmac1 != doubleHmac2: return False log.debug("The computed HMAC is valid.") return True
def extract(self, data, aes, hmacKey): """ Extracts (i.e., decrypts and authenticates) protocol messages. The raw `data' coming directly from the wire is decrypted using `aes' and authenticated using `hmacKey'. The payload is then returned as unencrypted protocol messages. In case of invalid headers or HMACs, an exception is raised. """ self.recvBuf += data msgs = [] # Keep trying to unpack as long as there is at least a header. while len(self.recvBuf) >= const.HDR_LENGTH: # If necessary, extract the header fields. if self.totalLen is None and self.payloadLen is None and self.flags is None: self.totalLen = pack.ntohs(aes.decrypt(self.recvBuf[16:18])) self.payloadLen = pack.ntohs(aes.decrypt(self.recvBuf[18:20])) self.flags = ord(aes.decrypt(self.recvBuf[20])) if not isSane(self.totalLen, self.payloadLen, self.flags): raise base.PluggableTransportError("Invalid header.") # Parts of the message are still on the wire; waiting. if (len(self.recvBuf) - const.HDR_LENGTH) < self.totalLen: break rcvdHMAC = self.recvBuf[0:const.HMAC_SHA256_128_LENGTH] vrfyHMAC = mycrypto.HMAC_SHA256_128( hmacKey, self.recvBuf[const.HMAC_SHA256_128_LENGTH:(self.totalLen + const.HDR_LENGTH)]) if rcvdHMAC != vrfyHMAC: raise base.PluggableTransportError("Invalid message HMAC.") # Decrypt the message and remove it from the input buffer. extracted = aes.decrypt(self.recvBuf[const.HDR_LENGTH:( self.totalLen + const.HDR_LENGTH)])[:self.payloadLen] msgs.append(ProtocolMessage(payload=extracted, flags=self.flags)) self.recvBuf = self.recvBuf[const.HDR_LENGTH + self.totalLen:] # Protocol message processed; now reset length fields. self.totalLen = self.payloadLen = self.flags = None return msgs
def encryptAndHMAC(self, crypter, hmacKey): """ Encrypt and authenticate this protocol message. This protocol message is encrypted using `crypter' and authenticated using `hmacKey'. Finally, the encrypted message prepended by a HMAC-SHA256-128 is returned and ready to be sent over the wire. """ encrypted = crypter.encrypt( pack.htons(self.totalLen) + pack.htons(self.payloadLen) + chr(self.flags) + self.payload + (self.totalLen - self.payloadLen) * '\0') hmac = mycrypto.HMAC_SHA256_128(hmacKey, encrypted) return hmac + encrypted
def receiveTicket(self, data): """ Extract and verify a potential session ticket. The given `data' is treated as a session ticket. The ticket is being decrypted and authenticated (yes, in that order). If all these steps succeed, `True' is returned. Otherwise, `False' is returned. """ if len(data) < (const.TICKET_LENGTH + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH): return False potentialTicket = data.peek() # Now try to decrypt and parse the ticket. We need the master key # inside to verify the HMAC in the next step. if not self.decryptedTicket: newTicket = ticket.decrypt(potentialTicket[:const.TICKET_LENGTH], self.srvState) if newTicket != None and newTicket.isValid(): self.deriveSecrets(newTicket.masterKey) self.decryptedTicket = True else: return False # First, find the mark to efficiently locate the HMAC. mark = mycrypto.HMAC_SHA256_128(self.recvHMAC, potentialTicket[:const.TICKET_LENGTH]) index = util.locateMark(mark, potentialTicket) if not index: return False # Now, verify if the HMAC is valid. existingHMAC = potentialTicket[index + const.MARK_LENGTH:index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH] authenticated = False for epoch in util.expandedEpoch(): myHMAC = mycrypto.HMAC_SHA256_128(self.recvHMAC, potentialTicket[0:index + \ const.MARK_LENGTH] + epoch) if util.isValidHMAC(myHMAC, existingHMAC, self.recvHMAC): authenticated = True break log.debug("HMAC invalid. Trying next epoch value.") if not authenticated: log.warning("Could not verify the authentication message's HMAC.") return False # Do nothing if the ticket is replayed. Immediately closing the # connection would be suspicious. if self.srvState.isReplayed(existingHMAC): log.warning("The HMAC was already present in the replay table.") return False data.drain(index + const.MARK_LENGTH + const.HMAC_SHA256_128_LENGTH) log.debug("Adding the HMAC authenticating the ticket message to the " \ "replay table: %s." % existingHMAC.encode('hex')) self.srvState.registerKey(existingHMAC) log.debug("Switching to state ST_CONNECTED.") self.protoState = const.ST_CONNECTED return True
def test6_HMAC_SHA256_128( self ): self.assertRaises(AssertionError, mycrypto.HMAC_SHA256_128, "x" * (const.SHARED_SECRET_LENGTH - 1), "test") self.failUnless(len(mycrypto.HMAC_SHA256_128("x" * \ const.SHARED_SECRET_LENGTH, "test")) == 16)