def verify_sth_temporal_consistency(old_sth, new_sth): """Verify the temporal consistency for two STH responses. For two STHs, verify that the newer STH has bigger tree size. Does not verify STH signatures or consistency of hashes. Args: old_sth: client_pb2.SthResponse proto. The STH with the older timestamp must be supplied first. new_sth: client_pb2.SthResponse proto. Returns: True. The return value is enforced by a decorator and need not be checked by the caller. Raises: ct.crypto.error.ConsistencyError: STHs are inconsistent ValueError: "Older" STH is not older. """ if old_sth.timestamp > new_sth.timestamp: raise ValueError("Older STH has newer timestamp (%d vs %d), did " "you supply inputs in the wrong order?" % (old_sth.timestamp, new_sth.timestamp)) if (old_sth.timestamp == new_sth.timestamp and old_sth.tree_size != new_sth.tree_size): # Issuing two different STHs for the same timestamp is illegal, # even if they are otherwise consistent. raise error.ConsistencyError("Inconsistency: different tree sizes " "for the same timestamp") if (old_sth.timestamp < new_sth.timestamp and old_sth.tree_size > new_sth.tree_size): raise error.ConsistencyError("Inconsistency: older tree has bigger " "size") return True
def test_update_sth_fails_for_inconsistent_sth(self): client = FakeLogClient(self._NEW_STH) # The STH is in fact OK but fake failure. self.verifier.verify_sth_consistency.side_effect = ( error.ConsistencyError("Boom!")) m = self.create_monitor(client) def check_state(result): # Check that we kept the state. expected_state = client_pb2.MonitorState() expected_state.verified_sth.CopyFrom(self._DEFAULT_STH) self.verify_state(expected_state) audited_sths = list(self.db.scan_latest_sth_range("log_server")) self.assertEqual(len(audited_sths), 2) self.assertEqual(audited_sths[0].audit.status, client_pb2.VERIFY_ERROR) self.assertEqual(audited_sths[1].audit.status, client_pb2.UNVERIFIED) for audited_sth in audited_sths: self.assertEqual(self._DEFAULT_STH.sha256_root_hash, audited_sth.sth.sha256_root_hash) return m._update_sth().addCallback( self.assertFalse).addCallback(check_state)
def test_update_sth_fails_for_inconsistent_sth(self): client = FakeLogClient(self._NEW_STH) # The STH is in fact OK but fake failure. self.verifier.verify_sth_consistency.side_effect = ( error.ConsistencyError("Boom!")) m = self.create_monitor(client) self.assertFalse(m._update_sth()) # Check that we kept the state. expected_state = client_pb2.MonitorState() expected_state.verified_sth.CopyFrom(self._DEFAULT_STH) self.verify_state(expected_state)
def test_verify_sth_consistency_invalid_proof(self): old_sth = LogVerifierTest.default_sth new_sth = client_pb2.SthResponse() new_sth.CopyFrom(old_sth) new_sth.tree_size = old_sth.tree_size + 1 new_sth.timestamp = old_sth.timestamp + 1 new_sth.sha256_root_hash = "a new hash" proof = ["some proof the mock does not care about"] mock_merkle_verifier = mock.Mock() mock_merkle_verifier.verify_tree_consistency.side_effect = ( error.ConsistencyError("Evil")) verifier = verify.LogVerifier(LogVerifierTest.default_key_info, mock_merkle_verifier) self.assertRaises(error.ConsistencyError, verifier.verify_sth_consistency, old_sth, new_sth, proof)
def verify_tree_consistency(self, old_tree_size, new_tree_size, old_root, new_root, proof): """Verify the consistency between two root hashes. old_tree_size must be <= new_tree_size. Args: old_tree_size: size of the older tree. new_tree_size: size of the newer_tree. old_root: the root hash of the older tree. new_root: the root hash of the newer tree. proof: the consistency proof. Returns: True. The return value is enforced by a decorator and need not be checked by the caller. Raises: ConsistencyError: the proof indicates an inconsistency (this is usually really serious!). ProofError: the proof is invalid. ValueError: supplied tree sizes are invalid. """ old_size = long(old_tree_size) new_size = long(new_tree_size) if old_size < 0 or new_size < 0: raise ValueError("Negative tree size") if old_size > new_size: raise ValueError("Older tree has bigger size (%d vs %d), did " "you supply inputs in the wrong order?" % (old_size, new_size)) if old_size == new_size: if old_root == new_root: if proof: logging.warning("Trees are identical, ignoring proof") return True else: raise error.ConsistencyError("Inconsistency: different root " "hashes for the same tree size") if old_size == 0: if proof: # A consistency proof with an empty tree is an empty proof. # Anything is consistent with an empty tree, so ignore whatever # bogus proof was supplied. Note we do not verify here that the # root hash is a valid hash for an empty tree. logging.warning("Ignoring non-empty consistency proof for " "empty tree.") return True # Now 0 < old_size < new_size # A consistency proof is essentially an audit proof for the node with # index old_size - 1 in the newer tree. The sole difference is that # the path is already hashed together into a single hash up until the # first audit node that occurs in the newer tree only. node = old_size - 1 last_node = new_size - 1 # While we are the right child, everything is in both trees, so move one # level up. while node % 2: node //= 2 last_node //= 2 p = iter(proof) try: if node: # Compute the two root hashes in parallel. new_hash = old_hash = p.next() else: # The old tree was balanced (2**k nodes), so we already have # the first root hash. new_hash = old_hash = old_root while node: if node % 2: # node is a right child: left sibling exists in both trees. next_node = p.next() old_hash = self.hasher.hash_children(next_node, old_hash) new_hash = self.hasher.hash_children(next_node, new_hash) elif node < last_node: # node is a left child: right sibling only exists in the # newer tree. new_hash = self.hasher.hash_children(new_hash, p.next()) # else node == last_node: node is a left child with no sibling # in either tree. node //= 2 last_node //= 2 # Now old_hash is the hash of the first subtree. If the two trees # have different height, continue the path until the new root. while last_node: new_hash = self.hasher.hash_children(new_hash, p.next()) last_node //= 2 # If the second hash does not match, the proof is invalid for the # given pair. If, on the other hand, the newer hash matches but the # older one doesn't, then the proof (together with the signatures # on the hashes) is proof of inconsistency. # Continue to find out. if new_hash != new_root: raise error.ProofError("Bad Merkle proof: second root hash " "does not match. Expected hash: %s " ", computed hash: %s" % (new_root.encode("base64").strip(), new_hash.encode("base64").strip())) elif old_hash != old_root: raise error.ConsistencyError( "Inconsistency: first root hash " "does not match. Expected hash: " "%s, computed hash: %s" % (old_root.encode("base64").strip(), old_hash.encode("base64").strip())) except StopIteration: raise error.ProofError("Merkle proof is too short") # We've already verified consistency, so accept the proof even if # there's garbage left over (but log a warning). try: p.next() except StopIteration: pass else: logging.warning("Proof has extra nodes") return True