From 1fad701ded917d9761e1310776b8c3740487dbfb Mon Sep 17 00:00:00 2001 From: pitboss Date: Fri, 22 May 2026 06:04:46 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0019 (20260522T043516Z-29b8) --- tests/crypto_corpus.rs | 185 +++++++++++++++++++ tests/dynamic_fixtures/crypto/go/vuln.go | 27 ++- tests/dynamic_fixtures/crypto/java/vuln.java | 22 ++- tests/dynamic_fixtures/crypto/php/vuln.php | 14 +- tests/dynamic_fixtures/crypto/python/vuln.py | 21 ++- tests/dynamic_fixtures/crypto/rust/vuln.rs | 26 ++- 6 files changed, 273 insertions(+), 22 deletions(-) diff --git a/tests/crypto_corpus.rs b/tests/crypto_corpus.rs index a5c50172..40d3c5c1 100644 --- a/tests/crypto_corpus.rs +++ b/tests/crypto_corpus.rs @@ -12,6 +12,8 @@ #![cfg(feature = "dynamic")] +mod common; + use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; @@ -109,6 +111,189 @@ fn weak_key_entropy_clears_with_no_probe() { assert!(!oracle_fired(&oracle, &outcome(), &[])); } +// ── End-to-end Phase 11 CRYPTO acceptance via run_spec ─────────────────────── +// +// Drives `run_spec` directly on a `Cap::CRYPTO` spec per language and +// asserts the polarity via the `ProbeKind::WeakKey { key_int }` probe. +// The vuln fixture is payload-branched: the curated `NYX_CRYPTO_WEAK` +// payload routes through the weak RNG (sub-2^16 key → predicate fires); +// the curated `NYX_CRYPTO_STRONG` benign control routes through the +// CSPRNG (huge key → predicate clears). Both attempts load the same +// `vuln.` fixture, so the runner's existing single-entry-file +// model holds — see the deferred items file for the rationale. +// +// Per-lang coverage: Python / PHP / Java / Go / Rust fixtures are +// payload-branched in tree. The Go case SKIPs on hosts without the +// `go` toolchain. + +mod e2e_phase_11_crypto { + use crate::common::fixture_harness::FIXTURE_LOCK; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; + use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; + use nyx_scanner::dynamic::spec::{ + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, + }; + use nyx_scanner::evidence::DifferentialVerdict; + use nyx_scanner::labels::Cap; + use nyx_scanner::symbol::Lang; + use std::path::PathBuf; + use std::process::Command; + use tempfile::TempDir; + + fn command_available(bin: &str) -> bool { + Command::new(bin) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + fn toolchain_for(lang: Lang) -> &'static str { + match lang { + Lang::Python => "python3", + Lang::Php => "php", + Lang::Java => "java", + Lang::Rust => "cargo", + Lang::Go => "go", + _ => unreachable!("e2e_phase_11_crypto covers Python/PHP/Java/Rust/Go today"), + } + } + + fn lang_subdir(lang: Lang) -> &'static str { + match lang { + Lang::Python => "python", + Lang::Php => "php", + Lang::Java => "java", + Lang::Rust => "rust", + Lang::Go => "go", + _ => unreachable!(), + } + } + + fn build_spec(lang: Lang, fixture: &str, entry_name: &str) -> (HarnessSpec, TempDir) { + let fixture_src = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/dynamic_fixtures/crypto") + .join(lang_subdir(lang)) + .join(fixture); + let tmp = TempDir::new().expect("create tempdir"); + let dst = tmp.path().join(fixture); + std::fs::copy(&fixture_src, &dst).expect("copy fixture into tempdir"); + + let entry_file = dst.to_string_lossy().into_owned(); + let mut digest = blake3::Hasher::new(); + digest.update(b"phase11-e2e-crypto|"); + digest.update(lang_subdir(lang).as_bytes()); + digest.update(b"|"); + digest.update(fixture.as_bytes()); + let spec_hash = format!("{:016x}", { + let bytes = digest.finalize(); + u64::from_le_bytes(bytes.as_bytes()[..8].try_into().unwrap()) + }); + + if matches!(lang, Lang::Java | Lang::Rust) { + let workdir = std::path::PathBuf::from("/tmp/nyx-harness").join(&spec_hash); + let _ = std::fs::remove_dir_all(&workdir); + } + + let spec = HarnessSpec { + finding_id: spec_hash.clone(), + entry_file: entry_file.clone(), + entry_name: entry_name.to_owned(), + entry_kind: EntryKind::Function, + lang, + toolchain_id: default_toolchain_id(lang).into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: Cap::CRYPTO, + constraint_hints: vec![], + sink_file: entry_file, + sink_line: 1, + spec_hash: spec_hash.clone(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: nyx_scanner::dynamic::spec::JavaToolchain::default(), + }; + + (spec, tmp) + } + + fn run(lang: Lang, fixture: &str, entry_name: &str) -> Option { + let bin = toolchain_for(lang); + if !command_available(bin) { + eprintln!("SKIP {lang:?} {fixture}: missing toolchain {bin}"); + return None; + } + let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let (spec, _tmp) = build_spec(lang, fixture, entry_name); + let opts = SandboxOptions { + backend: SandboxBackend::Process, + ..SandboxOptions::default() + }; + match run_spec(&spec, &opts) { + Ok(outcome) => Some(outcome), + Err(RunError::BuildFailed { stderr, attempts }) => { + eprintln!( + "SKIP {lang:?} {fixture}: harness build failed after {attempts} attempts: {stderr}", + ); + None + } + Err(e) => panic!("run_spec({lang:?} {fixture}) errored: {e:?}"), + } + } + + fn assert_confirmed(lang: Lang, outcome: &RunOutcome) { + assert!( + outcome.triggered_by.is_some(), + "{lang:?} CRYPTO vuln must Confirm via run_spec; got {outcome:?}", + ); + let diff = outcome + .differential + .as_ref() + .expect("Confirmed run must carry a DifferentialOutcome"); + assert_eq!(diff.verdict, DifferentialVerdict::Confirmed); + } + + #[test] + fn python_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; + assert_confirmed(Lang::Python, &outcome); + } + + #[test] + fn php_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; + assert_confirmed(Lang::Php, &outcome); + } + + #[test] + fn java_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Java, "vuln.java", "run") else { + return; + }; + assert_confirmed(Lang::Java, &outcome); + } + + #[test] + fn rust_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else { + return; + }; + assert_confirmed(Lang::Rust, &outcome); + } + + #[test] + fn go_vuln_confirms_via_run_spec() { + let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else { + return; + }; + assert_confirmed(Lang::Go, &outcome); + } +} + #[test] fn crypto_unsupported_for_other_langs() { for lang in [ diff --git a/tests/dynamic_fixtures/crypto/go/vuln.go b/tests/dynamic_fixtures/crypto/go/vuln.go index 8c2f9c35..74e320ce 100644 --- a/tests/dynamic_fixtures/crypto/go/vuln.go +++ b/tests/dynamic_fixtures/crypto/go/vuln.go @@ -1,12 +1,27 @@ // Phase 11 (Track J.9) — Go CRYPTO vuln fixture. // -// Uses math/rand.Intn(0x10000) (a non-CSPRNG) to derive a 16-bit -// key. The harness's instrumented key path writes a -// `ProbeKind::WeakKey` probe and the `WeakKeyEntropy` oracle fires. +// Models a config-driven crypto endpoint that picks the RNG based on +// the request payload — `*_WEAK` routes through math/rand.Intn (a +// non-CSPRNG, returning a 16-bit key) and `*_STRONG` routes through +// crypto/rand.Read (a CSPRNG, returning the leading 63 bits of an 8- +// byte read). This shape is needed by the differential runner: the +// vuln-payload attempt and the benign-control attempt both load the +// same fixture, and only the payload-routed weak branch trips the +// `WeakKeyEntropy` predicate. package vuln -import "math/rand" +import ( + crand "crypto/rand" + "encoding/binary" + mrand "math/rand" + "strings" +) -func Run(_ string) int { - return rand.Intn(0x10000) +func Run(value string) int { + if strings.Contains(value, "STRONG") { + var buf [8]byte + _, _ = crand.Read(buf[:]) + return int(binary.BigEndian.Uint64(buf[:]) >> 1) + } + return mrand.Intn(0x10000) } diff --git a/tests/dynamic_fixtures/crypto/java/vuln.java b/tests/dynamic_fixtures/crypto/java/vuln.java index b93f8fc9..680ce390 100644 --- a/tests/dynamic_fixtures/crypto/java/vuln.java +++ b/tests/dynamic_fixtures/crypto/java/vuln.java @@ -1,14 +1,24 @@ // Phase 11 (Track J.9) — Java CRYPTO vuln fixture. // -// Uses java.util.Random (a non-CSPRNG) to derive key bytes, producing -// a key bounded inside a 16-bit search space. The harness's -// instrumented key-generation path writes a `ProbeKind::WeakKey` -// probe; the `WeakKeyEntropy` oracle fires for `key_int < 2^16`. +// Models a config-driven crypto endpoint that picks the RNG based on +// the request payload — `*_WEAK` routes through `java.util.Random` +// (a non-CSPRNG, seeded from the payload hash, returning a 16-bit +// key) and `*_STRONG` routes through `java.security.SecureRandom` +// (a CSPRNG, returning 32 bytes). This shape is needed by the +// differential runner: the vuln-payload attempt and the benign- +// control attempt both load the same fixture, and only the payload- +// routed weak branch trips the `WeakKeyEntropy` predicate. import java.util.Random; +import java.security.SecureRandom; public class Vuln { - public static byte[] run(String seedTag) { - Random r = new Random(seedTag.hashCode()); + public static byte[] run(String value) { + if (value != null && value.contains("STRONG")) { + byte[] key = new byte[32]; + new SecureRandom().nextBytes(key); + return key; + } + Random r = new Random(value == null ? 0L : (long) value.hashCode()); byte[] key = new byte[2]; r.nextBytes(key); return key; diff --git a/tests/dynamic_fixtures/crypto/php/vuln.php b/tests/dynamic_fixtures/crypto/php/vuln.php index 928ccaa2..45257e0b 100644 --- a/tests/dynamic_fixtures/crypto/php/vuln.php +++ b/tests/dynamic_fixtures/crypto/php/vuln.php @@ -1,7 +1,17 @@ u16 { - rand::thread_rng().gen_range(0..=0xFFFF) as u16 +pub fn run(value: &str) -> [u8; 8] { + let mut key = [0u8; 8]; + if value.contains("STRONG") { + OsRng.fill_bytes(&mut key); + } else { + let weak = rand::thread_rng().gen_range(0..=0xFFFFu16); + key[6] = (weak >> 8) as u8; + key[7] = (weak & 0xFF) as u8; + } + key }