use super::evidence::{Evidence, EvidenceTier, ExecutionIdentity};
use super::model::{Category, Verdict};
use super::registry::Scenario;
#[derive(Debug, Clone)]
pub struct OracleInput<'a> {
pub scenario: &'a Scenario,
pub evidence: &'a Evidence,
pub nonce: Option<&'a str>,
pub probe_nonce: Option<&'a str>,
pub control_nonce: Option<&'a str>,
pub payload_intact: bool,
pub env_poisoned: bool,
pub agreeing_vectors: usize,
pub violation_observed: bool,
pub control_observed: bool,
pub execution_identity: Option<&'a ExecutionIdentity>,
pub stdio_complete: bool,
}
pub fn judge(input: &OracleInput<'_>) -> Verdict {
if input.env_poisoned {
return match input.scenario.category {
Category::Aux => Verdict::Inconclusive,
_ => Verdict::Fail,
};
}
if !input.payload_intact {
return Verdict::Inconclusive;
}
if input.violation_observed {
return Verdict::Fail;
}
if !input.evidence.verify_integrity() {
return Verdict::Inconclusive;
}
match (input.nonce, input.probe_nonce, input.control_nonce) {
(Some(n), Some(p), Some(c)) if p == n && c == n => {}
_ => return Verdict::Inconclusive,
}
if !input.stdio_complete {
return Verdict::Inconclusive;
}
if !input.control_observed {
return Verdict::Inconclusive;
}
let identity = match input.execution_identity {
Some(id) if id.is_well_formed() => id,
_ => return Verdict::Inconclusive,
};
if identity.scenario_id != input.scenario.id {
return Verdict::Inconclusive;
}
if input.nonce != Some(identity.session_nonce.as_str()) {
return Verdict::Inconclusive;
}
if !input.evidence.has_verified_control(identity) {
return Verdict::Inconclusive;
}
if !input.evidence.has_host_fact() {
return Verdict::Inconclusive;
}
if input.agreeing_vectors < input.scenario.quorum.max(1) {
return Verdict::Inconclusive;
}
Verdict::Pass
}
pub fn apply_strength_ceiling(verdict: Verdict, strength: super::model::ClaimStrength) -> Verdict {
match (verdict, strength) {
(Verdict::Pass, super::model::ClaimStrength::Unsupported) => Verdict::Inconclusive,
_ => verdict,
}
}
pub fn judge_with_ceiling(
input: &OracleInput<'_>,
strength: super::model::ClaimStrength,
host_value: Option<&str>,
) -> Verdict {
let _ = host_value;
let v = judge(input);
let v = if v == Verdict::Pass
&& !input
.evidence
.facts
.iter()
.any(|f| f.tier == EvidenceTier::HostFact && f.can_support_pass())
{
Verdict::Inconclusive
} else {
v
};
apply_strength_ceiling(v, strength)
}
#[cfg(test)]
mod oracle_tests {
use super::*;
use crate::verify_ng::model::ClaimStrength;
use crate::verify_ng::registry::Severity;
fn scenario() -> Scenario {
Scenario {
id: "T".to_string(),
category: Category::FsRead,
severity: Severity::High,
required_caps: vec![],
strength: Default::default(),
quorum: 1,
known_limitation: "test".to_string(),
residual_risk: String::new(),
}
}
fn input<'a>(scenario: &'a Scenario, evidence: &'a Evidence) -> OracleInput<'a> {
OracleInput {
scenario,
evidence,
nonce: Some("n"),
probe_nonce: Some("n"),
control_nonce: Some("n"),
payload_intact: true,
env_poisoned: false,
agreeing_vectors: 1,
violation_observed: false,
control_observed: true,
stdio_complete: true,
execution_identity: None,
}
}
fn verified_setup(scenario_id: &str) -> (ExecutionIdentity, Evidence) {
let id = ExecutionIdentity::new(scenario_id, "n", "reg-test", "frozen-test");
let expected = crate::verify_ng::evidence::derive_expected_response("test-challenge", "n");
let verified =
crate::verify_ng::evidence::attest_control(&id, &expected, expected.as_bytes())
.expect("test attestation must mint");
let mut e = Evidence::default();
e.host_fact("postmortem", "absent".to_string());
e.host_control_fact(&verified);
(id, e)
}
#[allow(clippy::too_many_arguments)]
fn full_input<'a>(
scenario: &'a Scenario,
evidence: &'a Evidence,
nonce: Option<&'a str>,
probe_nonce: Option<&'a str>,
control_nonce: Option<&'a str>,
payload_intact: bool,
violation_observed: bool,
control_observed: bool,
stdio_complete: bool,
agreeing_vectors: usize,
identity: Option<&'a ExecutionIdentity>,
) -> OracleInput<'a> {
OracleInput {
scenario,
evidence,
nonce,
probe_nonce,
control_nonce,
payload_intact,
env_poisoned: false,
agreeing_vectors,
violation_observed,
control_observed,
stdio_complete,
execution_identity: identity,
}
}
#[test]
fn oracle_deceit_trap_self_report_only_is_not_pass() {
let s = scenario();
let mut e = Evidence::default();
e.self_report("marker", "PASS PASS PASS".to_string());
e.constrained("errno", "EACCES".to_string());
let v = judge(&input(&s, &e));
assert_eq!(v, Verdict::Inconclusive);
}
#[test]
fn host_fact_with_control_passes() {
let s = scenario();
let mut legacy = Evidence::default();
legacy.host_fact("postmortem", "absent".to_string());
assert_eq!(judge(&input(&s, &legacy)), Verdict::Inconclusive);
let (id, e) = verified_setup(&s.id);
let i = OracleInput {
scenario: &s,
evidence: &e,
nonce: Some("n"),
probe_nonce: Some("n"),
control_nonce: Some("n"),
payload_intact: true,
env_poisoned: false,
agreeing_vectors: 1,
violation_observed: false,
control_observed: true,
stdio_complete: true,
execution_identity: Some(&id),
};
assert_eq!(judge(&i), Verdict::Pass);
}
#[test]
fn control_split_is_inconclusive() {
let s = scenario();
let (id, e) = verified_setup(&s.id);
let mut i = OracleInput {
scenario: &s,
evidence: &e,
nonce: Some("n"),
probe_nonce: None,
control_nonce: Some("n"),
payload_intact: true,
env_poisoned: false,
agreeing_vectors: 1,
violation_observed: false,
control_observed: true,
stdio_complete: true,
execution_identity: Some(&id),
};
assert_eq!(judge(&i), Verdict::Inconclusive);
i.probe_nonce = Some("n");
i.execution_identity = None;
assert_eq!(judge(&i), Verdict::Inconclusive);
}
#[test]
fn foreign_identity_evidence_is_inconclusive() {
let s = scenario();
let (id, e) = verified_setup(&s.id);
let own = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&own), Verdict::Pass);
let other_session = ExecutionIdentity::new(&s.id, "nonce-B", "reg-test", "frozen-test");
let replay = full_input(
&s,
&e,
Some("nonce-B"),
Some("nonce-B"),
Some("nonce-B"),
true,
false,
true,
true,
1,
Some(&other_session),
);
assert_eq!(judge(&replay), Verdict::Inconclusive);
let mut other_scenario = scenario();
other_scenario.id = "OTHER".to_string();
let wrong_scen = full_input(
&other_scenario,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&wrong_scen), Verdict::Inconclusive);
let other_registry = ExecutionIdentity::new(&s.id, "n", "reg-B", "frozen-test");
let wrong_reg = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&other_registry),
);
assert_eq!(judge(&wrong_reg), Verdict::Inconclusive);
let other_frozen = ExecutionIdentity::new(&s.id, "n", "reg-test", "frozen-B");
let wrong_frozen = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&other_frozen),
);
assert_eq!(judge(&wrong_frozen), Verdict::Inconclusive);
let malformed = ExecutionIdentity::new(&s.id, "", "reg-test", "frozen-test");
let bad = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&malformed),
);
assert_eq!(judge(&bad), Verdict::Inconclusive);
}
#[test]
fn mutated_payload_is_inconclusive() {
let s = scenario();
let (id, e) = verified_setup(&s.id);
let i = OracleInput {
scenario: &s,
evidence: &e,
nonce: Some("n"),
probe_nonce: Some("n"),
control_nonce: Some("n"),
payload_intact: false,
env_poisoned: false,
agreeing_vectors: 1,
violation_observed: false,
control_observed: true,
stdio_complete: true,
execution_identity: Some(&id),
};
assert_eq!(judge(&i), Verdict::Inconclusive);
}
#[test]
fn host_violation_beats_self_report() {
let s = scenario();
let mut e = Evidence::default();
e.host_fact("postmortem", "LEAK bytes present".to_string());
e.self_report("marker", "PASS".to_string());
let mut i = input(&s, &e);
i.violation_observed = true;
assert_eq!(judge(&i), Verdict::Fail);
}
#[test]
fn quorum_not_met_is_inconclusive() {
let mut s = scenario();
s.quorum = 2;
let (id, e) = verified_setup(&s.id);
let i = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&i), Verdict::Inconclusive);
let i = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
2,
Some(&id),
);
assert_eq!(judge(&i), Verdict::Pass);
}
#[test]
fn unsupported_ceiling_demotes_pass() {
assert_eq!(
apply_strength_ceiling(Verdict::Pass, ClaimStrength::Unsupported),
Verdict::Inconclusive
);
assert_eq!(
apply_strength_ceiling(Verdict::Fail, ClaimStrength::Unsupported),
Verdict::Fail
);
}
#[test]
fn incomplete_collection_cannot_pass() {
let s = scenario();
let (id, e) = verified_setup(&s.id);
let i = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
false,
1,
Some(&id),
);
assert_eq!(judge(&i), Verdict::Inconclusive);
}
#[test]
fn evidence_integrity_tamper_fails_closed() {
let s = scenario();
let (id, mut e) = verified_setup(&s.id);
e.facts.push(crate::verify_ng::evidence::Fact {
tier: EvidenceTier::HostFact,
name: "forged_leak".to_string(),
value: "leak".to_string(),
provenance: None,
source: Some(crate::verify_ng::evidence::EvidenceSource::SandboxedArbitraryJson),
timestamp_epoch_ms: None,
});
let i = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&i), Verdict::Inconclusive);
}
#[test]
fn untrusted_stdout_or_json_cannot_smuggle_pass() {
let s = scenario();
let (id, mut e) = verified_setup(&s.id);
e.add_stdout("marker", "PASS".to_string());
e.add_arbitrary_json("output", "{\"status\":\"ok\"}".to_string());
e.add_unprovenanced_snapshot("snap", "stat".to_string());
e.add_unbound_observation("obs", "event".to_string());
let i = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&i), Verdict::Pass);
let mut deceit_e = Evidence::default();
deceit_e.add_stdout("marker", "PASS".to_string());
deceit_e.add_arbitrary_json("output", "{\"status\":\"ok\"}".to_string());
let i2 = full_input(
&s,
&deceit_e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id),
);
assert_eq!(judge(&i2), Verdict::Inconclusive);
}
#[test]
fn contract_digest_binding_enforced() {
let s = scenario();
let id_with_digest = ExecutionIdentity::new(&s.id, "n", "reg-test", "frozen-test")
.with_contract_digest("digest-correct");
let expected = crate::verify_ng::evidence::derive_expected_response("test-challenge", "n");
let verified = crate::verify_ng::evidence::attest_control(
&id_with_digest,
&expected,
expected.as_bytes(),
)
.expect("test attestation must mint");
let mut e = Evidence::default();
e.host_fact("postmortem", "absent".to_string());
e.host_control_fact(&verified);
let pass_input = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&id_with_digest),
);
assert_eq!(judge(&pass_input), Verdict::Pass);
let tampered_id = id_with_digest
.clone()
.with_contract_digest("digest-tampered");
let fail_input = full_input(
&s,
&e,
Some("n"),
Some("n"),
Some("n"),
true,
false,
true,
true,
1,
Some(&tampered_id),
);
assert_eq!(judge(&fail_input), Verdict::Inconclusive);
}
}