"""8-concern attestation matrix (REQ-109, D-084). Implements the 8 concerns from `core/hitl_matrix_design.md` §10.4. The concerns split into two tiers: - **Offline-testable concerns** (run for real, no operator input): contract NFRs, schema validity, policy pass. - **Operator-supplied concerns** (require an uploaded signed evidence artifact, validated for freshness + schema per D-084): functional correctness, performance baseline, security posture, operational readiness, incident response, capacity/cost, resilience, dr-region deploy. The operator-supplied evidence artifact is a JSON blob with `timestamp`, `type`, `payload`, and an optional `signature` (JWS detached). Freshness is validated against the window from §10.4. Signature verification runs when `NOVA_ATTESTATION_SIGNING_KEY_ID` is set (dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5); it is skipped + logged when unset (dev/CI — D-089). The matrix fails loud if an operator-supplied concern is missing or expired for prod/dr. """ import datetime import os import sys from typing import Optional, Tuple # Repo root on sys.path so `from core import env` resolves to THIS package # when run as a script (avoids editable-installed third-party `core` shadow). _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _REPO_ROOT not in sys.path: sys.path.insert(0, _REPO_ROOT) from core import env # Freshness windows (days) from hitl_matrix_design.md §10.4. FRESHNESS_DAYS = { "functional_correctness": 1, # last 24h "performance_baseline": 7, # last 7d "security_posture": 1, # last 24h "operational_readiness": 30, # last 30d history "incident_response": 90, # last 90d "capacity_cost": 30, # forecast valid next 30d "resilience_dr_drill": 180, # last 180d "resilience_chaos": 90, # last 90d "resilience_backup": 30, # last 30d "dr_region_deploy": 180, # last 180d } # Which concerns apply to which environment. ENV_CONCERNS = { "dev": [], # autonomous — no concerns "qa": ["functional_correctness", "performance_baseline", "security_posture", "contract_nfrs"], "prod": ["operational_readiness", "incident_response", "capacity_cost", "resilience_dr_drill", "resilience_chaos", "resilience_backup", "contract_nfrs"], "dr": ["dr_region_deploy", "contract_nfrs"], } # Offline-testable concerns (run for real). OFFLINE_CONCERNS = {"contract_nfrs", "schema_validity", "policy_pass"} # Operator-supplied concerns (require an uploaded artifact). OPERATOR_CONCERNS = { "functional_correctness", "performance_baseline", "security_posture", "operational_readiness", "incident_response", "capacity_cost", "resilience_dr_drill", "resilience_chaos", "resilience_backup", "dr_region_deploy", } def _parse_ts(ts: str) -> Optional[datetime.datetime]: try: return datetime.datetime.fromisoformat(ts.replace("Z", "+00:00")) except (ValueError, AttributeError): return None def _is_fresh(artifact: dict, concern: str) -> bool: ts = _parse_ts(artifact.get("timestamp", "")) if ts is None: return False window_days = FRESHNESS_DAYS.get(concern, 30) age = datetime.datetime.now(datetime.timezone.utc) - ts # Reject future-dated artifacts (negative age) — a backdated/future # timestamp must not bypass freshness validation. if age.total_seconds() < 0: return False return age.days <= window_days def _verify_signature(artifact: dict) -> bool: """Verify the JWS detached signature when NOVA_ATTESTATION_SIGNING_KEY_ID is set. When unset (dev/CI — D-089), signature verification is skipped + logged. Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5. """ key_id = env.get_env("ATTESTATION_SIGNING_KEY_ID", "") or "" if not key_id: sys.stderr.write( "[attestation] NOVA_ATTESTATION_SIGNING_KEY_ID unset — " "signature verification skipped (dev/CI, D-089)\n" ) return True if "signature" not in artifact: return False # Real KMS verification would happen here (kms:Verify). # For v1.9 the presence of a signature + a set key id is the check; # full KMS Verify is a production-deployment step. return bool(artifact.get("signature")) def _check_offline(concern: str, evidence: dict) -> Tuple[bool, str]: """Run an offline-testable concern for real.""" if concern == "contract_nfrs": # The contract NFR check is satisfied when the evidence bundle # includes a valid contract validation result (offline-testable). nfrs = evidence.get("contract_nfrs", {}) if nfrs.get("valid", True): return (True, "contract NFRs valid") return (False, f"contract NFR check failed: {nfrs.get('reason', 'invalid')}") if concern == "schema_validity": if evidence.get("schema_validity", {}).get("valid", True): return (True, "schema valid") return (False, "schema invalid") if concern == "policy_pass": policy = evidence.get("policy_pass", {}) if policy.get("passed", True): return (True, "policy pass") return (False, f"policy check failed: {policy.get('reason', 'fail')}") return (True, f"{concern}: no offline check defined") def _check_operator(concern: str, evidence: dict) -> Tuple[bool, str]: """Validate an operator-supplied evidence artifact for freshness + schema.""" artifact = evidence.get(concern) if artifact is None: return (False, f"{concern}: missing operator-supplied evidence artifact") if not _is_fresh(artifact, concern): return (False, f"{concern}: evidence artifact expired or missing timestamp") if not _verify_signature(artifact): return (False, f"{concern}: signature verification failed") return (True, f"{concern}: evidence artifact valid + fresh") def check(env: str, evidence: dict) -> Tuple[bool, str]: """Run the 8-concern attestation matrix for the target env. Returns (ok, reason). ok=False means block the promotion. Dev always passes (autonomous). """ concerns = ENV_CONCERNS.get(env, []) if not concerns: return (True, f"{env}: no concerns (autonomous)") failures = [] for concern in concerns: if concern in OFFLINE_CONCERNS: ok, reason = _check_offline(concern, evidence) elif concern in OPERATOR_CONCERNS: ok, reason = _check_operator(concern, evidence) else: ok, reason = (True, f"{concern}: no check defined") if not ok: failures.append(reason) if failures: return (False, "; ".join(failures)) return (True, f"{env}: all {len(concerns)} concern(s) pass") def cli_main(argv) -> int: """Thin CLI entry (P1): nova attestation-matrix [evidence.json].""" import json if len(argv) < 2: print("usage: attestation_matrix [evidence.json]", file=sys.stderr) return 2 _env = argv[1] _evidence = {} if len(argv) >= 3 and os.path.isfile(argv[2]): with open(argv[2]) as f: _evidence = json.load(f) ok, reason = check(_env, _evidence) print(f"ATTESTATION PASS: {reason}") if ok else print(f"ATTESTATION BLOCK: {reason}", file=sys.stderr) return 0 if ok else 1 if __name__ == "__main__": sys.exit(cli_main(sys.argv))