216 lines
7.4 KiB
Python
216 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail-closed construction of verbatim-hashed normative fragments.
|
|
|
|
This is the single construction path for the Claude and Pi adapters. The
|
|
payload contains only versioned source metadata and exact validated source
|
|
bytes. ``h_payload`` is derived afterwards and is deliberately not representable
|
|
as a payload input, preventing cryptographic self-reference.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import struct
|
|
from collections.abc import Sequence
|
|
from typing import Final
|
|
|
|
MAX_FRAGMENT_BYTES: Final = 64 * 1024
|
|
HASH_DOMAIN_SEPARATOR: Final = b"MOSAIC/H_PAYLOAD/v1\x00"
|
|
SOURCE_DOMAIN_SEPARATOR: Final = b"MOSAIC/H_SOURCE/v1\x00"
|
|
_PAYLOAD_VERSION_LABEL: Final = b"MOSAIC/B_PAYLOAD/v1"
|
|
|
|
|
|
class NormativeFragment:
|
|
"""A source identity, its expected digest, and its exact resolved bytes."""
|
|
|
|
def __init__(self, source_id: str, content: bytes | None, expected_sha256: str) -> None:
|
|
self.source_id = source_id
|
|
self.content = content
|
|
self.expected_sha256 = expected_sha256
|
|
|
|
|
|
class ConstructionResult:
|
|
"""A source-admission decision and, only when admitted, derived payload values.
|
|
|
|
``promotion`` means the construction has produced the only values a later
|
|
receipt protocol may use to attempt promotion. It never performs broker
|
|
promotion itself. A REFUSED result has no payload/hash values, so it cannot
|
|
advance to that later protocol.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
injection_decision: str,
|
|
promotion: bool,
|
|
source_reason: str | None,
|
|
b_payload: bytes | None,
|
|
h_payload: str | None,
|
|
h_source: str | None,
|
|
) -> None:
|
|
self.injectionDecision = injection_decision
|
|
self.promotion = promotion
|
|
self.source_reason = source_reason
|
|
self.b_payload = b_payload
|
|
self.h_payload = h_payload
|
|
self.h_source = h_source
|
|
|
|
|
|
def length_frame(parts: Sequence[bytes]) -> bytes:
|
|
"""Encode a finite ordered byte sequence with unambiguous 64-bit framing."""
|
|
|
|
framed = bytearray(struct.pack(">Q", len(parts)))
|
|
for part in parts:
|
|
if not isinstance(part, bytes):
|
|
raise TypeError("length framing requires bytes")
|
|
framed.extend(struct.pack(">Q", len(part)))
|
|
framed.extend(part)
|
|
return bytes(framed)
|
|
|
|
|
|
def _sha256_hex(payload: bytes) -> str:
|
|
return hashlib.sha256(payload).hexdigest()
|
|
|
|
|
|
def _valid_digest(value: object) -> bool:
|
|
return (
|
|
isinstance(value, str)
|
|
and len(value) == 64
|
|
and all(character in "0123456789abcdef" for character in value)
|
|
)
|
|
|
|
|
|
def _source_reason(fragment: NormativeFragment) -> str | None:
|
|
if not isinstance(fragment.source_id, str) or not fragment.source_id:
|
|
return "missing"
|
|
if fragment.content is None:
|
|
return "missing"
|
|
if not isinstance(fragment.content, bytes):
|
|
return "missing"
|
|
if len(fragment.content) > MAX_FRAGMENT_BYTES:
|
|
return "oversize"
|
|
if not _valid_digest(fragment.expected_sha256):
|
|
return "hash-mismatch"
|
|
actual = _sha256_hex(fragment.content)
|
|
if not hmac.compare_digest(actual, fragment.expected_sha256):
|
|
return "hash-mismatch"
|
|
return None
|
|
|
|
|
|
def _refused(reason: str) -> ConstructionResult:
|
|
return ConstructionResult(
|
|
injection_decision="REFUSED",
|
|
promotion=False,
|
|
source_reason=reason,
|
|
b_payload=None,
|
|
h_payload=None,
|
|
h_source=None,
|
|
)
|
|
|
|
|
|
def build_payload(
|
|
*,
|
|
manifest_version: int,
|
|
generator_version: str,
|
|
fragments: Sequence[NormativeFragment],
|
|
) -> ConstructionResult:
|
|
"""Construct B_payload then H_payload after fail-closed source validation.
|
|
|
|
The sequence order is caller-supplied resolved-source order and is encoded
|
|
directly. Changing source order, source identity, metadata, or any exact
|
|
fragment byte therefore changes the framed B_payload and its derived hash.
|
|
"""
|
|
|
|
if type(manifest_version) is not int or manifest_version < 0:
|
|
return _refused("missing")
|
|
if not isinstance(generator_version, str) or not generator_version:
|
|
return _refused("missing")
|
|
|
|
validated = list(fragments)
|
|
if not validated:
|
|
return _refused("missing")
|
|
for fragment in validated:
|
|
if not isinstance(fragment, NormativeFragment):
|
|
return _refused("missing")
|
|
reason = _source_reason(fragment)
|
|
if reason is not None:
|
|
return _refused(reason)
|
|
|
|
source_identities = [
|
|
length_frame([
|
|
fragment.source_id.encode("utf-8"),
|
|
fragment.expected_sha256.encode("ascii"),
|
|
])
|
|
for fragment in validated
|
|
]
|
|
h_source = _sha256_hex(SOURCE_DOMAIN_SEPARATOR + length_frame(source_identities))
|
|
payload_parts = [
|
|
_PAYLOAD_VERSION_LABEL,
|
|
str(manifest_version).encode("ascii"),
|
|
generator_version.encode("utf-8"),
|
|
*source_identities,
|
|
*(fragment.content for fragment in validated),
|
|
]
|
|
# h_payload is intentionally not an argument or field in payload_parts.
|
|
b_payload = length_frame(payload_parts)
|
|
h_payload = _sha256_hex(HASH_DOMAIN_SEPARATOR + length_frame([b_payload]))
|
|
return ConstructionResult(
|
|
injection_decision="ACCEPTED",
|
|
promotion=True,
|
|
source_reason=None,
|
|
b_payload=b_payload,
|
|
h_payload=h_payload,
|
|
h_source=h_source,
|
|
)
|
|
|
|
|
|
def build_payload_from_wire(value: object) -> ConstructionResult:
|
|
"""Decode a bounded broker request then invoke the sole payload builder.
|
|
|
|
Wire inputs contain only source bytes and their claimed source digests. The
|
|
authoritative ``build_payload`` implementation remains the only code that
|
|
admits those bytes and derives ``h_source``/``h_payload``.
|
|
"""
|
|
|
|
if not isinstance(value, dict) or set(value) != {
|
|
"manifest_version", "generator_version", "fragments"
|
|
}:
|
|
raise ValueError("invalid construction")
|
|
raw_fragments = value["fragments"]
|
|
if not isinstance(raw_fragments, list):
|
|
raise ValueError("invalid construction")
|
|
fragments: list[NormativeFragment] = []
|
|
for raw_fragment in raw_fragments:
|
|
if not isinstance(raw_fragment, dict) or set(raw_fragment) != {
|
|
"source_id", "content_base64", "expected_sha256"
|
|
}:
|
|
raise ValueError("invalid construction")
|
|
source_id = raw_fragment["source_id"]
|
|
encoded = raw_fragment["content_base64"]
|
|
expected_sha256 = raw_fragment["expected_sha256"]
|
|
if not isinstance(source_id, str) or not isinstance(encoded, str) or not isinstance(expected_sha256, str):
|
|
raise ValueError("invalid construction")
|
|
try:
|
|
content = base64.b64decode(encoded.encode("ascii"), validate=True)
|
|
except (UnicodeEncodeError, ValueError) as exc:
|
|
raise ValueError("invalid construction") from exc
|
|
fragments.append(NormativeFragment(source_id, content, expected_sha256))
|
|
return build_payload(
|
|
manifest_version=value["manifest_version"],
|
|
generator_version=value["generator_version"],
|
|
fragments=fragments,
|
|
)
|
|
|
|
|
|
def build_for_claude(**kwargs: object) -> ConstructionResult:
|
|
"""Claude adapter entrypoint; delegates to the sole shared constructor."""
|
|
|
|
return build_payload(**kwargs) # type: ignore[arg-type]
|
|
|
|
|
|
def build_for_pi(**kwargs: object) -> ConstructionResult:
|
|
"""Pi adapter entrypoint; delegates to the sole shared constructor."""
|
|
|
|
return build_payload(**kwargs) # type: ignore[arg-type]
|