mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): adapt demonstrated proof evidence
This commit is contained in:
@@ -318,7 +318,7 @@
|
|||||||
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 14.3_
|
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 14.3_
|
||||||
- _Boundary: Model Adapter_
|
- _Boundary: Model Adapter_
|
||||||
- _Depends: 6.2, 7.2_
|
- _Depends: 6.2, 7.2_
|
||||||
- [ ] 9.3 (P) Translate demonstrated search results into trusted proof attachments
|
- [x] 9.3 (P) Translate demonstrated search results into trusted proof attachments
|
||||||
- Map existing typed affected/safe verification records without moving sandbox or oracle execution into core.
|
- Map existing typed affected/safe verification records without moving sandbox or oracle execution into core.
|
||||||
- Preserve producer, control, witness, storage, and verification identities required by proof validation.
|
- Preserve producer, control, witness, storage, and verification identities required by proof validation.
|
||||||
- Refuse inconclusive and mismatched fixtures at the adapter/core seam.
|
- Refuse inconclusive and mismatched fixtures at the adapter/core seam.
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Translate verifier-owned differential results into core proof contracts.
|
||||||
|
|
||||||
|
This adapter hashes verifier observations and copies immutable identities. It never
|
||||||
|
executes a sandbox, an oracle, or a demonstration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from openultrasast.core import canonical
|
||||||
|
from openultrasast.core.proof import (
|
||||||
|
DIFFERENTIAL_PROOF_SCHEMA,
|
||||||
|
ControlBinding,
|
||||||
|
DifferentialProofRecord,
|
||||||
|
ProofAttachment,
|
||||||
|
ProofOutcome,
|
||||||
|
ProofProducerIdentity,
|
||||||
|
ProofSideResult,
|
||||||
|
ProofVerificationReceipt,
|
||||||
|
derive_producer_identity_digest,
|
||||||
|
)
|
||||||
|
from openultrasast.core.semantics import SourceLocation
|
||||||
|
from openultrasast.search.verify import RunObservation, SideRecord, VerificationRecord
|
||||||
|
|
||||||
|
|
||||||
|
class ProofAdapterError(ValueError):
|
||||||
|
"""Search verification evidence cannot form a core proof attachment."""
|
||||||
|
|
||||||
|
|
||||||
|
def _duration(value: object, field: str) -> float:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
|
||||||
|
raise ProofAdapterError(f"{field} must be a finite non-negative duration")
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_value(value: RunObservation) -> canonical.JsonValue:
|
||||||
|
if type(value) is not RunObservation or not isinstance(value.observed, bool) or not isinstance(value.evidence, str):
|
||||||
|
raise ProofAdapterError("verification runs must use typed observations")
|
||||||
|
return {
|
||||||
|
"observed": value.observed,
|
||||||
|
"evidence_digest": canonical.bytes_digest(value.evidence.encode()),
|
||||||
|
"elapsed_seconds": _duration(value.elapsed_seconds, "run elapsed_seconds"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _side_value(value: SideRecord) -> canonical.JsonValue:
|
||||||
|
if type(value) is not SideRecord or not isinstance(value.outcome, str) or not isinstance(value.reason, str):
|
||||||
|
raise ProofAdapterError("verification sides must use typed records")
|
||||||
|
if type(value.runs) is not tuple:
|
||||||
|
raise ProofAdapterError("verification side runs must be an exact tuple")
|
||||||
|
if isinstance(value.scratch_peak_bytes, bool) or not isinstance(value.scratch_peak_bytes, int) or value.scratch_peak_bytes < 0:
|
||||||
|
raise ProofAdapterError("scratch_peak_bytes must be a non-negative integer")
|
||||||
|
if value.exit_code is not None and (isinstance(value.exit_code, bool) or not isinstance(value.exit_code, int)):
|
||||||
|
raise ProofAdapterError("verification exit_code must be an integer or null")
|
||||||
|
text = (value.isolation_mode, value.phase, value.stderr, value.stdout)
|
||||||
|
if not all(isinstance(item, str) for item in text):
|
||||||
|
raise ProofAdapterError("verification side metadata must be text")
|
||||||
|
if not isinstance(value.chromium_no_sandbox, bool) or value.control_observed not in (None, True, False):
|
||||||
|
raise ProofAdapterError("verification side flags are invalid")
|
||||||
|
return {
|
||||||
|
"outcome": value.outcome,
|
||||||
|
"runs": tuple(_run_value(item) for item in value.runs),
|
||||||
|
"elapsed_seconds": _duration(value.elapsed_seconds, "side elapsed_seconds"),
|
||||||
|
"reason_digest": canonical.bytes_digest(value.reason.encode()),
|
||||||
|
"build_seconds": _duration(value.build_seconds, "build_seconds"),
|
||||||
|
"ready_seconds": _duration(value.ready_seconds, "ready_seconds"),
|
||||||
|
"run_seconds": _duration(value.run_seconds, "run_seconds"),
|
||||||
|
"scratch_peak_bytes": value.scratch_peak_bytes,
|
||||||
|
"isolation_mode": value.isolation_mode,
|
||||||
|
"chromium_no_sandbox": value.chromium_no_sandbox,
|
||||||
|
"phase": value.phase,
|
||||||
|
"exit_code": value.exit_code,
|
||||||
|
"stderr_digest": canonical.bytes_digest(value.stderr.encode()),
|
||||||
|
"stdout_digest": canonical.bytes_digest(value.stdout.encode()),
|
||||||
|
"control_observed": value.control_observed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _verification_value(value: VerificationRecord) -> canonical.JsonValue:
|
||||||
|
if not isinstance(value.reason, str) or not isinstance(value.isolation_mode, str):
|
||||||
|
raise ProofAdapterError("verification metadata must be text")
|
||||||
|
if not isinstance(value.chromium_no_sandbox, bool):
|
||||||
|
raise ProofAdapterError("verification chromium flag must be boolean")
|
||||||
|
return {
|
||||||
|
"outcome": value.outcome,
|
||||||
|
"elapsed_seconds": _duration(value.elapsed_seconds, "verification elapsed_seconds"),
|
||||||
|
"reason_digest": canonical.bytes_digest(value.reason.encode()),
|
||||||
|
"isolation_mode": value.isolation_mode,
|
||||||
|
"chromium_no_sandbox": value.chromium_no_sandbox,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _observation_digest(side: SideRecord, verification: VerificationRecord) -> str:
|
||||||
|
return canonical.content_digest({"side": _side_value(side), "verification": _verification_value(verification)})
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_side_records(value: VerificationRecord, affected: SideRecord, safe: SideRecord) -> None:
|
||||||
|
if any(type(item) is not SideRecord or item.outcome != "observed" for item in value.sides):
|
||||||
|
raise ProofAdapterError("both proof sides must have observed outcomes")
|
||||||
|
if any(type(item.runs) is not tuple or not all(type(run) is RunObservation for run in item.runs) for item in value.sides):
|
||||||
|
raise ProofAdapterError("both proof sides require typed run tuples")
|
||||||
|
if any(len(item.runs) != 3 for item in value.sides):
|
||||||
|
raise ProofAdapterError("both proof sides require exactly three runs")
|
||||||
|
if not all(item.observed for item in affected.runs):
|
||||||
|
raise ProofAdapterError("affected proof side must be observed in every run")
|
||||||
|
if any(item.observed for item in safe.runs):
|
||||||
|
raise ProofAdapterError("safe proof side must be unobserved in every run")
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_sides(value: VerificationRecord) -> tuple[SideRecord, SideRecord]:
|
||||||
|
if type(value) is not VerificationRecord:
|
||||||
|
raise ProofAdapterError("verification must be an exact VerificationRecord")
|
||||||
|
if value.outcome != "demonstrated":
|
||||||
|
raise ProofAdapterError("only a demonstrated verification can form proof")
|
||||||
|
if type(value.sides) is not tuple or len(value.sides) != 2:
|
||||||
|
raise ProofAdapterError("demonstrated verification requires exactly two sides")
|
||||||
|
affected, safe = value.sides
|
||||||
|
_validate_side_records(value, affected, safe)
|
||||||
|
if any(item.isolation_mode != value.isolation_mode for item in value.sides):
|
||||||
|
raise ProofAdapterError("proof side isolation does not match verification")
|
||||||
|
if any(item.chromium_no_sandbox != value.chromium_no_sandbox for item in value.sides):
|
||||||
|
raise ProofAdapterError("proof side chromium flag does not match verification")
|
||||||
|
_verification_value(value)
|
||||||
|
return affected, safe
|
||||||
|
|
||||||
|
|
||||||
|
def map_verification_record(
|
||||||
|
verification: VerificationRecord,
|
||||||
|
*,
|
||||||
|
producer: ProofProducerIdentity,
|
||||||
|
affected_input_digest: str,
|
||||||
|
safe_input_digest: str,
|
||||||
|
control_manifest: canonical.ContentRef,
|
||||||
|
control_reviewer_identity: str,
|
||||||
|
witness: canonical.ContentRef,
|
||||||
|
) -> DifferentialProofRecord:
|
||||||
|
"""Map a completed affected/safe record without running verifier code."""
|
||||||
|
affected_record, safe_record = _validated_sides(verification)
|
||||||
|
try:
|
||||||
|
affected = ProofSideResult(
|
||||||
|
affected_input_digest, True, _observation_digest(affected_record, verification), len(affected_record.runs), ""
|
||||||
|
)
|
||||||
|
safe = ProofSideResult(safe_input_digest, False, _observation_digest(safe_record, verification), len(safe_record.runs), "")
|
||||||
|
control = ControlBinding(affected_input_digest, safe_input_digest, control_manifest, control_reviewer_identity, "")
|
||||||
|
return DifferentialProofRecord(DIFFERENTIAL_PROOF_SCHEMA, producer, affected, safe, control, witness, ProofOutcome.DEMONSTRATED, "")
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ProofAdapterError(f"proof identities are invalid: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def make_proof_attachment(
|
||||||
|
proof: DifferentialProofRecord,
|
||||||
|
*,
|
||||||
|
family_id: str,
|
||||||
|
packet_digest: str,
|
||||||
|
location: SourceLocation,
|
||||||
|
attestation: canonical.ContentRef,
|
||||||
|
) -> ProofAttachment:
|
||||||
|
"""Bind a mapped proof to its externally stored verification attestation."""
|
||||||
|
if type(proof) is not DifferentialProofRecord:
|
||||||
|
raise ProofAdapterError("proof must be an exact DifferentialProofRecord")
|
||||||
|
try:
|
||||||
|
receipt = ProofVerificationReceipt(
|
||||||
|
derive_producer_identity_digest(proof.producer),
|
||||||
|
proof.record_digest,
|
||||||
|
proof.affected.result_digest,
|
||||||
|
proof.safe.result_digest,
|
||||||
|
proof.witness.content_digest,
|
||||||
|
attestation,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
return ProofAttachment("", family_id, packet_digest, location, proof, receipt)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ProofAdapterError(f"proof attachment identities are invalid: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ProofAdapterError", "make_proof_attachment", "map_verification_record"]
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Search verification records cross a conservative core proof boundary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from detection_core_fixtures import InMemoryContentResolver, stored_artifact
|
||||||
|
from test_core_proof import _case, _claim
|
||||||
|
|
||||||
|
from openultrasast.core._proof_attestation import proof_attestation_bytes
|
||||||
|
from openultrasast.core.evidence import EvidenceBasis
|
||||||
|
from openultrasast.core.proof import (
|
||||||
|
ProofValidationError,
|
||||||
|
attach_differential_proof,
|
||||||
|
derive_producer_identity_digest,
|
||||||
|
)
|
||||||
|
from openultrasast.search.core_adapter import ProofAdapterError, make_proof_attachment, map_verification_record
|
||||||
|
from openultrasast.search.verify import RunObservation, SideRecord, VerificationRecord
|
||||||
|
|
||||||
|
|
||||||
|
def _verification(*, outcome: str = "demonstrated", affected: bool = True, safe: bool = False) -> VerificationRecord:
|
||||||
|
def side(observed: bool, name: str) -> SideRecord:
|
||||||
|
runs = tuple(RunObservation(observed, f"{name}:{index}", 0.01) for index in range(3))
|
||||||
|
return SideRecord("observed", runs, 0.03, "complete", isolation_mode="task-boundary")
|
||||||
|
|
||||||
|
return VerificationRecord(
|
||||||
|
outcome,
|
||||||
|
(side(affected, "affected"), side(safe, "safe")),
|
||||||
|
0.06,
|
||||||
|
"owned differential",
|
||||||
|
isolation_mode="task-boundary",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mapped(record: VerificationRecord):
|
||||||
|
case = _case()
|
||||||
|
proof = map_verification_record(
|
||||||
|
record,
|
||||||
|
producer=case.attachment.proof.producer,
|
||||||
|
affected_input_digest=case.attachment.proof.affected.input_digest,
|
||||||
|
safe_input_digest=case.attachment.proof.safe.input_digest,
|
||||||
|
control_manifest=case.attachment.proof.control.control_manifest,
|
||||||
|
control_reviewer_identity=case.attachment.proof.control.reviewer_identity,
|
||||||
|
witness=case.attachment.proof.witness,
|
||||||
|
)
|
||||||
|
return case, proof
|
||||||
|
|
||||||
|
|
||||||
|
def test_demonstrated_verification_maps_and_attaches_without_execution_ownership() -> None:
|
||||||
|
case, proof = _mapped(_verification())
|
||||||
|
attestation, resolver = stored_artifact(
|
||||||
|
proof_attestation_bytes(
|
||||||
|
derive_producer_identity_digest(proof.producer),
|
||||||
|
proof.record_digest,
|
||||||
|
proof.affected.result_digest,
|
||||||
|
proof.safe.result_digest,
|
||||||
|
proof.witness.content_digest,
|
||||||
|
),
|
||||||
|
artifact_id="search-verification-attestation",
|
||||||
|
)
|
||||||
|
attachment = make_proof_attachment(
|
||||||
|
proof,
|
||||||
|
family_id=case.attachment.family_id,
|
||||||
|
packet_digest=case.attachment.packet_digest,
|
||||||
|
location=case.attachment.location,
|
||||||
|
attestation=attestation,
|
||||||
|
)
|
||||||
|
merged = InMemoryContentResolver({**case.resolver.artifacts, **resolver.artifacts})
|
||||||
|
|
||||||
|
derived = attach_differential_proof(_claim(case.record), attachment, case.policy, merged)
|
||||||
|
|
||||||
|
assert derived.basis is EvidenceBasis.EXECUTION_BACKED
|
||||||
|
assert derived.record.decision.provenance.proof_record_digest == proof.record_digest
|
||||||
|
|
||||||
|
|
||||||
|
def test_complete_verification_metadata_is_bound_into_proof_identity() -> None:
|
||||||
|
record = _verification()
|
||||||
|
_, proof = _mapped(record)
|
||||||
|
|
||||||
|
_, changed_reason = _mapped(replace(record, reason="different verifier reason"))
|
||||||
|
changed_run = replace(record.sides[0].runs[0], evidence="different observation")
|
||||||
|
changed_side = replace(record.sides[0], runs=(changed_run, *record.sides[0].runs[1:]))
|
||||||
|
_, changed_evidence = _mapped(replace(record, sides=(changed_side, record.sides[1])))
|
||||||
|
|
||||||
|
assert changed_reason.record_digest != proof.record_digest
|
||||||
|
assert changed_evidence.record_digest != proof.record_digest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("record", "reason"),
|
||||||
|
(
|
||||||
|
(_verification(outcome="inconclusive"), "demonstrated"),
|
||||||
|
(_verification(affected=False), "affected"),
|
||||||
|
(_verification(safe=True), "safe"),
|
||||||
|
(replace(_verification(), sides=_verification().sides[:1]), "two sides"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_inconclusive_or_non_differential_verification_is_refused(record: VerificationRecord, reason: str) -> None:
|
||||||
|
with pytest.raises(ProofAdapterError, match=reason):
|
||||||
|
_mapped(record)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mismatched_fixture_remains_at_prior_basis() -> None:
|
||||||
|
case, proof = _mapped(_verification())
|
||||||
|
affected = replace(
|
||||||
|
proof.affected,
|
||||||
|
input_digest=case.attachment.proof.safe.input_digest,
|
||||||
|
result_digest="",
|
||||||
|
)
|
||||||
|
wrong = replace(proof, affected=affected, record_digest="")
|
||||||
|
attestation, resolver = stored_artifact(
|
||||||
|
proof_attestation_bytes(
|
||||||
|
derive_producer_identity_digest(wrong.producer),
|
||||||
|
wrong.record_digest,
|
||||||
|
wrong.affected.result_digest,
|
||||||
|
wrong.safe.result_digest,
|
||||||
|
wrong.witness.content_digest,
|
||||||
|
),
|
||||||
|
artifact_id="mismatched-search-attestation",
|
||||||
|
)
|
||||||
|
attachment = make_proof_attachment(
|
||||||
|
wrong,
|
||||||
|
family_id=case.attachment.family_id,
|
||||||
|
packet_digest=case.attachment.packet_digest,
|
||||||
|
location=case.attachment.location,
|
||||||
|
attestation=attestation,
|
||||||
|
)
|
||||||
|
merged = InMemoryContentResolver({**case.resolver.artifacts, **resolver.artifacts})
|
||||||
|
claim = _claim(case.record)
|
||||||
|
|
||||||
|
with pytest.raises(ProofValidationError, match="affected proof input"):
|
||||||
|
attach_differential_proof(claim, attachment, case.policy, merged)
|
||||||
|
assert claim.basis is EvidenceBasis.DETERMINISTIC
|
||||||
Reference in New Issue
Block a user