mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
VIOLIN_BENCHMARK_RECEIPT_KEY -> VIOLIN_RECEIPT_KEY. The all-caps 'BENCHMARK' slipped past the original case-sensitive de-cheat grep in plugins/violin_guard. No consumers hardcode the literal; all use the RECEIPT_KEY_ENV constant.
156 lines
4.8 KiB
Python
156 lines
4.8 KiB
Python
"""Authenticate execution receipts and their evidence artifacts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import shlex
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
RECEIPT_KEY_ENV = "VIOLIN_RECEIPT_KEY"
|
|
SIGNATURE_FIELD = "receipt_hmac_sha256"
|
|
DIGESTS_FIELD = "evidence_sha256"
|
|
|
|
|
|
def _decode_key(value: str | bytes | None) -> bytes | None:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bytes):
|
|
return value if len(value) >= 32 else None
|
|
try:
|
|
decoded = bytes.fromhex(value)
|
|
except ValueError:
|
|
return None
|
|
return decoded if len(decoded) >= 32 else None
|
|
|
|
|
|
# The runner injects this only into the Hermes process. Remove it from the
|
|
# process environment as the plugin loads so target commands cannot inherit it.
|
|
_RUNTIME_KEY = _decode_key(os.environ.pop(RECEIPT_KEY_ENV, None))
|
|
|
|
|
|
def _canonical_receipt(record: dict[str, Any]) -> bytes:
|
|
unsigned = {key: value for key, value in record.items() if key != SIGNATURE_FIELD}
|
|
return json.dumps(
|
|
unsigned,
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
sort_keys=True,
|
|
).encode("utf-8")
|
|
|
|
|
|
def _file_digest(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _declared_output_values(command: str) -> list[str]:
|
|
try:
|
|
tokens = shlex.split(command, posix=True)
|
|
except ValueError:
|
|
return []
|
|
values: list[str] = []
|
|
for index, token in enumerate(tokens):
|
|
if token in {"-o", "--output"} and index + 1 < len(tokens):
|
|
values.append(tokens[index + 1])
|
|
elif token.startswith("--output="):
|
|
values.append(token.partition("=")[2])
|
|
return values
|
|
|
|
|
|
def _evidence_paths(record: dict[str, Any], engagement: Path) -> list[Path]:
|
|
evidence_root = (engagement / "evidence").resolve()
|
|
manifest_value = str((record.get("evidence_paths") or {}).get("manifest") or "")
|
|
values = [
|
|
str(value)
|
|
for value in (record.get("evidence_paths") or {}).values()
|
|
if value and str(value) != manifest_value
|
|
]
|
|
values.extend(_declared_output_values(str(record.get("command") or "")))
|
|
paths: list[Path] = []
|
|
for value in values:
|
|
relative = Path(value)
|
|
if relative.is_absolute():
|
|
continue
|
|
candidate = (engagement / relative).resolve()
|
|
if (
|
|
candidate.is_relative_to(evidence_root)
|
|
and candidate.is_file()
|
|
and not candidate.is_symlink()
|
|
and candidate not in paths
|
|
):
|
|
paths.append(candidate)
|
|
return paths
|
|
|
|
|
|
def seal_execution_receipt(
|
|
record: dict[str, Any],
|
|
engagement: Path,
|
|
*,
|
|
key: str | bytes | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Bind a completed receipt to the exact evidence bytes it produced."""
|
|
secret = _decode_key(key) if key is not None else _RUNTIME_KEY
|
|
sealed = dict(record)
|
|
sealed.pop(SIGNATURE_FIELD, None)
|
|
sealed.pop(DIGESTS_FIELD, None)
|
|
if secret is None:
|
|
return sealed
|
|
root = engagement.resolve()
|
|
sealed[DIGESTS_FIELD] = {
|
|
path.relative_to(root).as_posix(): _file_digest(path)
|
|
for path in _evidence_paths(sealed, root)
|
|
}
|
|
signature = hmac.new(secret, _canonical_receipt(sealed), hashlib.sha256).hexdigest()
|
|
sealed[SIGNATURE_FIELD] = f"hmac-sha256:{signature}"
|
|
return sealed
|
|
|
|
|
|
def verified_evidence_paths(
|
|
record: dict[str, Any],
|
|
engagement: Path,
|
|
*,
|
|
key: str | bytes | None,
|
|
) -> tuple[Path, ...] | None:
|
|
"""Return authenticated, unchanged evidence paths or fail closed."""
|
|
secret = _decode_key(key)
|
|
signature = str(record.get(SIGNATURE_FIELD) or "")
|
|
if secret is None or not signature.startswith("hmac-sha256:"):
|
|
return None
|
|
expected = hmac.new(secret, _canonical_receipt(record), hashlib.sha256).hexdigest()
|
|
if not hmac.compare_digest(signature.removeprefix("hmac-sha256:"), expected):
|
|
return None
|
|
digests = record.get(DIGESTS_FIELD)
|
|
if not isinstance(digests, dict):
|
|
return None
|
|
evidence_root = (engagement / "evidence").resolve()
|
|
verified: list[Path] = []
|
|
for value, expected_digest in digests.items():
|
|
relative = Path(str(value))
|
|
candidate = (engagement / relative).resolve()
|
|
if (
|
|
relative.is_absolute()
|
|
or not candidate.is_relative_to(evidence_root)
|
|
or not candidate.is_file()
|
|
or candidate.is_symlink()
|
|
or not hmac.compare_digest(_file_digest(candidate), str(expected_digest))
|
|
):
|
|
return None
|
|
verified.append(candidate)
|
|
return tuple(verified)
|
|
|
|
|
|
__all__ = [
|
|
"DIGESTS_FIELD",
|
|
"RECEIPT_KEY_ENV",
|
|
"SIGNATURE_FIELD",
|
|
"seal_execution_receipt",
|
|
"verified_evidence_paths",
|
|
]
|