Ejemplo n.º 1
0
    def __init__(self):
        self.node_certificates = {}
        self.node_activity_status = {}
        self.signature_count = 0
        self.chosen_hash = ec.ECDSA(utils.Prehashed(hashes.SHA256()))

        # Start with empty bytes array. CCF MerkleTree uses an empty array as the first leaf of it's merkle tree.
        # Don't hash empty bytes array.
        self.merkle = MerkleTree()
        empty_bytes_array = bytearray(self.SHA_256_HASH_SIZE)
        self.merkle.add_leaf(empty_bytes_array, do_hash=False)

        self.last_verified_seqno = 0
        self.last_verified_view = 0
Ejemplo n.º 2
0
    def __init__(self, accept_deprecated_entry_types: bool = True):
        self.accept_deprecated_entry_types = accept_deprecated_entry_types
        self.chosen_hash = ec.ECDSA(utils.Prehashed(hashes.SHA256()))

        # Start with empty bytes array. CCF MerkleTree uses an empty array as the first leaf of its merkle tree.
        # Don't hash empty bytes array.
        self.merkle = MerkleTree()
        empty_bytes_array = bytearray(hashes.SHA256.digest_size)
        self.merkle.add_leaf(empty_bytes_array, do_hash=False)

        self.last_verified_seqno = 0
        self.last_verified_view = 0

        self.service_status = None
Ejemplo n.º 3
0
 def _verify_merkle_root(self, merkletree: MerkleTree, existing_root: bytes):
     """Verify item 3, by comparing the roots from the merkle tree that's maintained by this class and from the one extracted from the ledger"""
     root = merkletree.get_merkle_root()
     if root != existing_root:
         raise InvalidRootException(
             f"\nComputed root: {root.hex()} \nExisting root from ledger: {existing_root.hex()}"
         )
Ejemplo n.º 4
0
class LedgerValidator:
    """
    Ledger Validator contains the logic to verify that the ledger hasn't been tampered with.
    It has the ability to take transactions and it maintains a MerkleTree data structure similar to CCF.

    Ledger is valid and hasn't been tampered with if following conditions are met:
        1) The merkle proof is signed by a TRUSTED node in the given network
        2) The merkle root and signature are verified with the node cert
        3) The merkle proof is correct for each set of transactions
    """

    # The node that is expected to sign the signature transaction
    # The certificate used to sign the signature transaction
    # https://github.com/microsoft/CCF/blob/main/src/node/nodes.h
    EXPECTED_NODE_CERT_INDEX = 1
    # The current network trust status of the Node at the time of the current transaction
    EXPECTED_NODE_STATUS_INDEX = 4

    # Signature table contains PrimarySignature which extends NodeSignature. NodeId should be at index 1 in the serialized Node
    # https://github.com/microsoft/CCF/blob/main/src/node/signatures.h
    EXPECTED_NODE_SIGNATURE_INDEX = 0
    EXPECTED_NODE_SEQNO_INDEX = 1
    EXPECTED_NODE_VIEW_INDEX = 2
    EXPECTED_ROOT_INDEX = 5
    # https://github.com/microsoft/CCF/blob/main/src/node/node_signature.h
    EXPECTED_SIGNING_NODE_ID_INDEX = 1
    EXPECTED_SIGNATURE_INDEX = 0
    # Constant for the size of a hashed transaction
    SHA_256_HASH_SIZE = 32

    def __init__(self):
        self.node_certificates = {}
        self.node_activity_status = {}
        self.signature_count = 0
        self.chosen_hash = ec.ECDSA(utils.Prehashed(hashes.SHA256()))

        # Start with empty bytes array. CCF MerkleTree uses an empty array as the first leaf of it's merkle tree.
        # Don't hash empty bytes array.
        self.merkle = MerkleTree()
        empty_bytes_array = bytearray(self.SHA_256_HASH_SIZE)
        self.merkle.add_leaf(empty_bytes_array, do_hash=False)

        self.last_verified_seqno = 0
        self.last_verified_view = 0

    def add_transaction(self, transaction):
        """
        To validate the ledger, ledger transactions need to be added via this method.
        Depending on the tables that were part of the transaction, it does different things.
        When transaction contains signature table, it starts the verification process and verifies that the root of merkle tree was signed by a node which was part of the network.
        It also matches the root of the merkle tree that this class maintains with the one extracted from the ledger.
        If any of the above checks fail, this method throws.
        """
        transaction_public_domain = transaction.get_public_domain()
        tables = transaction_public_domain.get_tables()

        # Add contributing nodes certs and update nodes network trust status for verification
        if NODES_TABLE_NAME in tables:
            node_table = tables[NODES_TABLE_NAME]
            for nodeid, values in node_table.items():
                # Add the nodes certificate
                self.node_certificates[nodeid] = values[
                    self.EXPECTED_NODE_CERT_INDEX]
                # Update node trust status
                self.node_activity_status[nodeid] = NodeStatus(
                    values[self.EXPECTED_NODE_STATUS_INDEX])

        # This is a merkle root/signature tx if the table exists
        if SIGNATURE_TX_TABLE_NAME in tables:
            self.signature_count += 1
            signature_table = tables[SIGNATURE_TX_TABLE_NAME]

            for nodeid, values in signature_table.items():
                current_seqno = values[self.EXPECTED_NODE_SEQNO_INDEX]
                current_view = values[self.EXPECTED_NODE_VIEW_INDEX]
                signing_node = str(values[self.EXPECTED_NODE_SIGNATURE_INDEX][
                    self.EXPECTED_SIGNING_NODE_ID_INDEX])

                # Get binary representations for the cert, existing root, and signature
                cert = b"".join(self.node_certificates[signing_node])
                existing_root = b"".join(values[self.EXPECTED_ROOT_INDEX])
                signature = values[self.EXPECTED_NODE_SIGNATURE_INDEX][
                    self.EXPECTED_SIGNATURE_INDEX]

                tx_info = TxBundleInfo(
                    self.merkle,
                    existing_root,
                    cert,
                    signature,
                    self.node_activity_status,
                    signing_node,
                )

                # validations for 1, 2 and 3
                # throws if ledger validation failed.
                self._verify_tx_set(tx_info)

                self.last_verified_seqno = current_seqno
                self.last_verified_view = current_view
                LOG.debug(
                    f"Ledger verified till seqno: {current_seqno} and view: {current_view}"
                )

        # Checks complete, add this transaction to tree
        self.merkle.add_leaf(transaction.get_raw_tx())

    def _verify_tx_set(self, tx_info: TxBundleInfo):
        """
        Verify items 1, 2, and 3 for all the transactions up until a signature.
        """
        # 1) The merkle root is signed by a TRUSTED node in the given network, else throws
        self._verify_node_status(tx_info)
        # 2) The merkle root and signature are verified with the node cert, else throws
        self._verify_root_signature(tx_info)
        # 3) The merkle root is correct for the set of transactions and matches with the one extracted from the ledger, else throws
        self._verify_merkle_root(tx_info.merkle_tree, tx_info.existing_root)

    @staticmethod
    def _verify_node_status(tx_info: TxBundleInfo):
        """Verify item 1, The merkle root is signed by a TRUSTED node in the given network"""
        if tx_info.node_activity[tx_info.signing_node] != NodeStatus.TRUSTED:
            LOG.error(
                f"The signing node {tx_info.signing_node!r} is not trusted by the network"
            )
            raise UntrustedNodeException

    def _verify_root_signature(self, tx_info: TxBundleInfo):
        """Verify item 2, that the Merkle root signature validates against the node certificate"""
        try:
            cert = load_pem_x509_certificate(tx_info.node_cert,
                                             default_backend())
            pub_key = cert.public_key()

            assert isinstance(pub_key, ec.EllipticCurvePublicKey)
            pub_key.verify(tx_info.signature, tx_info.existing_root,
                           self.chosen_hash)  # type: ignore[override]
        # This exception is thrown from x509, catch for logging and raise our own
        except InvalidSignature:
            LOG.error("Signature verification failed:" +
                      f"\nCertificate: {tx_info.node_cert!r}" +
                      f"\nSignature: {tx_info.signature!r}" +
                      f"\nRoot: {tx_info.existing_root!r}")
            raise InvalidRootSignatureException from InvalidSignature

    def _verify_merkle_root(self, merkletree: MerkleTree,
                            existing_root: bytes):
        """Verify item 3, by comparing the roots from the merkle tree that's maintained by this class and from the one extracted from the ledger"""
        root = merkletree.get_merkle_root()
        if root != existing_root:
            LOG.error(
                f"\nRoot: {root.hex()} \nExisting root from ledger: {existing_root.hex()}"
            )
            raise InvalidRootException
Ejemplo n.º 5
0
class LedgerValidator:
    """
    Ledger Validator contains the logic to verify that the ledger hasn't been tampered with.
    It has the ability to take transactions and it maintains a MerkleTree data structure similar to CCF.

    Ledger is valid and hasn't been tampered with if following conditions are met:
        1) The merkle proof is signed by a Trusted node in the given network
        2) The merkle root and signature are verified with the node cert
        3) The merkle proof is correct for each set of transactions
    """

    # Constant for the size of a hashed transaction
    SHA_256_HASH_SIZE = 32

    def __init__(self):
        self.node_certificates = {}
        self.node_activity_status = {}
        self.signature_count = 0
        self.chosen_hash = ec.ECDSA(utils.Prehashed(hashes.SHA256()))

        # Start with empty bytes array. CCF MerkleTree uses an empty array as the first leaf of it's merkle tree.
        # Don't hash empty bytes array.
        self.merkle = MerkleTree()
        empty_bytes_array = bytearray(self.SHA_256_HASH_SIZE)
        self.merkle.add_leaf(empty_bytes_array, do_hash=False)

        self.last_verified_seqno = 0
        self.last_verified_view = 0

    def add_transaction(self, transaction):
        """
        To validate the ledger, ledger transactions need to be added via this method.
        Depending on the tables that were part of the transaction, it does different things.
        When transaction contains signature table, it starts the verification process and verifies that the root of merkle tree was signed by a node which was part of the network.
        It also matches the root of the merkle tree that this class maintains with the one extracted from the ledger.
        If any of the above checks fail, this method throws.
        """
        transaction_public_domain = transaction.get_public_domain()
        tables = transaction_public_domain.get_tables()

        # Add contributing nodes certs and update nodes network trust status for verification
        node_certs = {}
        if NODES_TABLE_NAME in tables:
            node_table = tables[NODES_TABLE_NAME]
            for node_id, node_info in node_table.items():
                node_id = node_id.decode()
                node_info = json.loads(node_info)
                # Add the self-signed node certificate (only available in 1.x,
                # refer to node endorsed certificates table otherwise)
                if "cert" in node_info:
                    node_certs[node_id] = node_info["cert"].encode()
                    self.node_certificates[node_id] = node_certs[node_id]
                # Update node trust status
                # Also record the seqno at which the node status changed to
                # track when a primary node should stop issuing signatures
                self.node_activity_status[node_id] = (
                    node_info["status"],
                    transaction_public_domain.get_seqno(),
                )

        if ENDORSED_NODE_CERTIFICATES_TABLE_NAME in tables:
            node_endorsed_certificates_tables = tables[
                ENDORSED_NODE_CERTIFICATES_TABLE_NAME]
            for (
                    node_id,
                    endorsed_node_cert,
            ) in node_endorsed_certificates_tables.items():
                node_id = node_id.decode()
                assert (
                    node_id not in node_certs
                ), f"Only one of node self-signed certificate and endorsed certificate should be recorded for node {node_id}"
                node_cert = endorsed_node_cert
                self.node_certificates[node_id] = node_cert

        # This is a merkle root/signature tx if the table exists
        if SIGNATURE_TX_TABLE_NAME in tables:
            self.signature_count += 1
            signature_table = tables[SIGNATURE_TX_TABLE_NAME]

            for _, signature in signature_table.items():
                signature = json.loads(signature)
                current_seqno = signature["seqno"]
                current_view = signature["view"]
                signing_node = signature["node"]

                # Get binary representations for the cert, existing root, and signature
                cert = self.node_certificates[signing_node]
                existing_root = bytes.fromhex(signature["root"])
                sig = base64.b64decode(signature["sig"])

                tx_info = TxBundleInfo(
                    self.merkle,
                    existing_root,
                    cert,
                    sig,
                    self.node_activity_status,
                    signing_node,
                )

                # validations for 1, 2 and 3
                # throws if ledger validation failed.
                self._verify_tx_set(tx_info)

                # Forget about nodes whose retirement has been committed
                for node_id, (status, seqno) in list(
                        self.node_activity_status.items()):
                    if (status == NodeStatus.RETIRED.value
                            and signature["commit_seqno"] >= seqno):
                        self.node_activity_status.pop(node_id)

                self.last_verified_seqno = current_seqno
                self.last_verified_view = current_view

        # Checks complete, add this transaction to tree
        self.merkle.add_leaf(transaction.get_raw_tx())

    def _verify_tx_set(self, tx_info: TxBundleInfo):
        """
        Verify items 1, 2, and 3 for all the transactions up until a signature.
        """
        # 1) The merkle root is signed by a Trusted node in the given network, else throws
        self._verify_node_status(tx_info)
        # 2) The merkle root and signature are verified with the node cert, else throws
        self._verify_root_signature(tx_info)
        # 3) The merkle root is correct for the set of transactions and matches with the one extracted from the ledger, else throws
        self._verify_merkle_root(tx_info.merkle_tree, tx_info.existing_root)

    @staticmethod
    def _verify_node_status(tx_info: TxBundleInfo):
        """Verify item 1, The merkle root is signed by a valid node in the given network"""
        # Note: A retired primary will still issue signature transactions until
        # its retirement is committed
        node_status = NodeStatus(
            tx_info.node_activity[tx_info.signing_node][0])
        if node_status not in (
                NodeStatus.TRUSTED,
                NodeStatus.RETIRING,
                NodeStatus.RETIRED,
        ):
            raise UntrustedNodeException(
                f"The signing node {tx_info.signing_node} has unexpected status {node_status.value}"
            )

    def _verify_root_signature(self, tx_info: TxBundleInfo):
        """Verify item 2, that the Merkle root signature validates against the node certificate"""
        try:
            cert = load_pem_x509_certificate(tx_info.node_cert,
                                             default_backend())
            pub_key = cert.public_key()

            assert isinstance(pub_key, ec.EllipticCurvePublicKey)
            pub_key.verify(tx_info.signature, tx_info.existing_root,
                           self.chosen_hash)  # type: ignore[override]
        # This exception is thrown from x509, catch for logging and raise our own
        except InvalidSignature:
            raise InvalidRootSignatureException(
                "Signature verification failed:" +
                f"\nCertificate: {tx_info.node_cert.decode()}" +
                f"\nSignature: {base64.b64encode(tx_info.signature).decode()}"
                +
                f"\nRoot: {tx_info.existing_root.hex()}") from InvalidSignature

    def _verify_merkle_root(self, merkletree: MerkleTree,
                            existing_root: bytes):
        """Verify item 3, by comparing the roots from the merkle tree that's maintained by this class and from the one extracted from the ledger"""
        root = merkletree.get_merkle_root()
        if root != existing_root:
            raise InvalidRootException(
                f"\nComputed root: {root.hex()} \nExisting root from ledger: {existing_root.hex()}"
            )