Download src/hdk/core/hash_engine.py from JakCwi/HDK-audit: direct link, hf CLI and curl.
- Browser
- Download file 7.7 kB
-
https://huggingface.co/JakCwi/HDK-audit/resolve/main/src/hdk/core/hash_engine.py
- Command line
-
hf download hf://JakCwi/HDK-audit/src/hdk/core/hash_engine.py
-
curl -L -o hash_engine.py https://huggingface.co/JakCwi/HDK-audit/resolve/main/src/hdk/core/hash_engine.py
7.7 kB
| # SPDX-License-Identifier: CC-BY-NC-4.0 | |
| # Copyright (c) 2026 RE::AI Strategy | |
| """HDK Hash Engine — hierarchical SHA-256 hashing with genealogy.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import hmac | |
| import json | |
| from typing import TYPE_CHECKING | |
| if TYPE_CHECKING: | |
| from hdk.models import HDKNode | |
| class HashEngine: | |
| """Produces hierarchical hashes where every hash encodes its full ancestry.""" | |
| def __init__(self, algorithm: str = "sha256", node_key: bytes | None = None): | |
| """Initialize with hash algorithm (currently only sha256). | |
| `node_key` (0.1.2): a key derived from the canary secret (CanaryLayer.node_key). With it, | |
| the node_hash of every canary-woven node is an HMAC under that key, so a node cannot be | |
| rewritten and re-hashed by someone who does not hold the canary key. | |
| """ | |
| if algorithm != "sha256": | |
| raise ValueError(f"Unsupported algorithm: {algorithm}. Only 'sha256' is supported.") | |
| self._algorithm = algorithm | |
| self.node_key = node_key | |
| def hash_content(self, content: bytes | str) -> str: | |
| """Hash raw content. Returns hex digest.""" | |
| if isinstance(content, str): | |
| content = content.encode("utf-8") | |
| return hashlib.sha256(content).hexdigest() | |
| def hash_with_parents(self, content_hash: str, parent_chain: list[str]) -> str: | |
| """ | |
| Hierarchical hash: H(content_hash || parent1 || parent2 || ... || root). | |
| This is the core HDK property — changing any ancestor | |
| invalidates this hash and all descendants. | |
| Args: | |
| content_hash: SHA-256 of the actual content | |
| parent_chain: [immediate_parent_hash, grandparent_hash, ..., root_hash] | |
| Returns: | |
| Hierarchical hash as hex string | |
| """ | |
| combined = "||".join([content_hash] + parent_chain) | |
| return hashlib.sha256(combined.encode("utf-8")).hexdigest() | |
| # RFC 6962 domain separation: a leaf hash can never be confused with an interior node | |
| LEAF_PREFIX = b"\x00" | |
| NODE_PREFIX = b"\x01" | |
| def merkle_leaf(cls, data: bytes | str) -> bytes: | |
| """Leaf hash: H(0x00 || data).""" | |
| if isinstance(data, str): | |
| data = data.encode("utf-8") | |
| return hashlib.sha256(cls.LEAF_PREFIX + data).digest() | |
| def merkle_node(cls, left: bytes, right: bytes) -> bytes: | |
| """Interior node hash: H(0x01 || left || right).""" | |
| return hashlib.sha256(cls.NODE_PREFIX + left + right).digest() | |
| def compute_merkle_root(self, node_hashes: list[str]) -> str: | |
| """ | |
| Merkle Tree Hash (RFC 6962, section 2.1) over a list of node hashes. | |
| leaf = H(0x00 || node_hash), node = H(0x01 || left || right). Leaves are never | |
| duplicated; for a level with an odd count the last *hashed* entry moves up a level, | |
| which yields the same root as RFC 6962's split at the largest power of two. A single | |
| leaf is still hashed, so the root never equals a bare input. Empty list: H(""). | |
| Args: | |
| node_hashes: list of hash strings (each leaf's data is its UTF-8 encoding) | |
| Returns: | |
| Merkle root as hex string | |
| """ | |
| if not node_hashes: | |
| return hashlib.sha256(b"").hexdigest() | |
| level = [self.merkle_leaf(h) for h in node_hashes] | |
| while len(level) > 1: | |
| nxt = [self.merkle_node(level[i], level[i + 1]) for i in range(0, len(level) - 1, 2)] | |
| if len(level) % 2: | |
| nxt.append(level[-1]) | |
| level = nxt | |
| return level[0].hex() | |
| NODE_HASH_DOMAIN = b"hdk-node-v2\x00" | |
| #: 0.1.2: the keyed node hash (HMAC-SHA256 under the canary-derived node key) | |
| NODE_HMAC_DOMAIN = b"hdk-node-v3-hmac\x00" | |
| def canonical_json(obj) -> bytes: | |
| """Canonical serialisation: sorted keys, no whitespace, UTF-8.""" | |
| return json.dumps( | |
| obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str | |
| ).encode("utf-8") | |
| def node_hash_fields(self, node: HDKNode) -> dict: | |
| """Every stored field of a node that node_hash commits to (all but node_hash itself).""" | |
| return { | |
| "id": node.id, | |
| "level": node.level.value if node.level else "", | |
| "timestamp": node.timestamp.isoformat() if node.timestamp else "", | |
| "content_hash": node.content_hash, | |
| "parent_chain": list(node.parent_chain), | |
| "self_hash": node.self_hash, | |
| "canary_commitment": node.canary_commitment, | |
| "canary_generation": node.canary_generation, | |
| "model": node.model or "", | |
| "provider": node.provider or "", | |
| "event_type": node.event_type.value if node.event_type else "", | |
| "token_count": node.token_count, | |
| "latency_ms": node.latency_ms, | |
| "user_id_token": node.user_id_token or "", | |
| "llm_model": node.llm_model or "", | |
| "llm_provider": node.llm_provider or "", | |
| "agent_name": node.agent_name or "", | |
| "custom": node.custom or {}, | |
| } | |
| def create_node_hash(self, node: HDKNode) -> str: | |
| """ | |
| Compute final node_hash for an HDKNode. | |
| node_hash = H("hdk-node-v2" || 0x00 || canonical_json(fields)) | |
| or, with a node key and a canary-woven node (0.1.2): | |
| node_hash = HMAC-SHA256(node_key, "hdk-node-v3-hmac" || 0x00 || canonical_json(fields)) | |
| where fields are all stored node fields (see node_hash_fields): the content and | |
| parent chain (via self_hash, and again explicitly), canary commitment, metadata, | |
| actor fields and custom fields. Canonical JSON makes the encoding unambiguous, so no | |
| two different field sets can serialise to the same bytes. | |
| """ | |
| payload = self.canonical_json(self.node_hash_fields(node)) | |
| if self.keyed(node): | |
| # 0.1.2: bound to the canary key — without it, a rewritten node cannot be re-hashed | |
| mac = hmac.new(self.node_key, self.NODE_HMAC_DOMAIN + payload, hashlib.sha256) | |
| return mac.hexdigest() | |
| return hashlib.sha256(self.NODE_HASH_DOMAIN + payload).hexdigest() | |
| def keyed(self, node: HDKNode) -> bool: | |
| """Whether `node` is hashed with the node key (a key is set and the node is woven).""" | |
| return self.node_key is not None and node.canary_generation > 0 | |
| def verify_chain(self, node: HDKNode, parent_nodes: list[HDKNode]) -> bool: | |
| """ | |
| Verify that a node's hash is consistent with its claimed parent chain. | |
| Recomputes self_hash, and checks that parent_nodes[i].node_hash == parent_chain[i] | |
| and that each parent's node_hash is itself genuine. | |
| Returns True if valid, False if any hash doesn't match. | |
| """ | |
| # Recompute self_hash from content_hash + parent_chain | |
| expected_self_hash = self.hash_with_parents(node.content_hash, node.parent_chain) | |
| if expected_self_hash != node.self_hash: | |
| return False | |
| if node.node_hash and self.create_node_hash(node) != node.node_hash: | |
| return False | |
| # parent_chain stores each parent's node_hash (see HDK._create_node), so compare | |
| # against that; every claimed parent must be supplied and match. | |
| if len(parent_nodes) < len(node.parent_chain): | |
| return False | |
| for parent_hash, parent in zip(node.parent_chain, parent_nodes): | |
| if parent.node_hash != parent_hash: | |
| return False | |
| if self.create_node_hash(parent) != parent.node_hash: | |
| return False | |
| return True | |