uofa-demo / src /uofa_cli /interrogate /packager.py
cloudronin's picture
push build context (uofa source + packs + space app)
a28ec65 verified
Raw
History Blame Contribute Delete
2.68 kB
"""Evidence packager + signer (SIP §3 component 4).
Assembles the §5 contract dict and signs it. Validation against the frozen
schema runs BEFORE signing, so a forbidden verdict field can never be given a
signature — the firewall, enforced at the packaging seam. Signing reuses
UofA's ``integrity.sign_file`` (ed25519 / SHA-256), so a SIP bundle verifies
with the same ``uofa verify`` machinery as any UofA package.
"""
from __future__ import annotations
import json
from pathlib import Path
from uofa_cli.interrogate.schema import validate_bundle
SCHEMA_VERSION = "sip-evidence-bundle/v0.1"
def assemble_bundle(
*,
bundle_id: str,
sip_version: str,
generated_at: str,
subject: dict,
declared_scope: dict,
measurement_provenance: list,
measurements: dict,
completeness: dict,
provenance: dict,
parent_snapshot: dict | None = None,
) -> dict:
"""Build the SIP §5 bundle dict (unsigned). ``parent_snapshot`` is optional."""
bundle = {
"bundleId": bundle_id,
"sipVersion": sip_version,
"schemaVersion": SCHEMA_VERSION,
"generatedAt": generated_at,
"subject": subject,
"declaredScope": declared_scope,
"measurementProvenance": measurement_provenance,
"measurements": measurements,
"provenance": provenance,
"completeness": completeness,
}
if parent_snapshot is not None:
bundle["parentModelSnapshot"] = parent_snapshot
return bundle
def emit_unsigned(bundle: dict, output_path: Path) -> Path:
"""Validate (firewall) then write the unsigned bundle JSON."""
validate_bundle(bundle)
output_path = Path(output_path)
output_path.write_text(
json.dumps(bundle, indent=2, ensure_ascii=False), encoding="utf-8"
)
return output_path
def emit_and_sign(
bundle: dict, output_path: Path, key_path: Path, *, context_path: Path | None = None
) -> tuple[str, str]:
"""Validate (firewall), write, then sign the MEASUREMENT scope. Returns (hash, sig).
Validation precedes signing so a forbidden field is rejected before any
signature is computed. Signs over the measurement bundle (excluding any
``engineerDecision``) via ``signing.sign_measurement``, so the measurement
signature keeps verifying after an engineer decision is appended (Addendum
A6). At emit there is no decision, so this equals a whole-doc signature.
``context_path`` is unused — the SIP bundle carries no top-level ``@context``.
"""
emit_unsigned(bundle, output_path)
from uofa_cli.interrogate.signing import sign_measurement
return sign_measurement(Path(output_path), Path(key_path))