diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 93757fba..0cfec071 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -577,6 +577,18 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_open_redirect_harness(spec)); } + // Phase 11 (Track J.9): CRYPTO weak-RNG short-circuit. The Go + // harness imports the fixture package directly, invokes + // `entry.(payload)`, and reduces the produced key into a + // `ProbeKind::WeakKey { key_int }` record via reflection — int + // returns flow through as `uint64`; `[]byte` returns get truncated + // to the leading 8 bytes via `binary.BigEndian.Uint64` padded so a + // 32-byte `crypto/rand.Read` key produces a magnitude well above + // any 16-bit budget. + if spec.expected_cap == crate::labels::Cap::CRYPTO { + return Ok(emit_crypto_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. Go has no // classes — the dispatcher treats `class` as a top-level struct // declared in the entry file and `method` as a method on its @@ -1280,6 +1292,144 @@ fn generate_go_mod() -> String { "module nyx-harness\n\ngo 1.21\n".to_owned() } +/// Phase 11 (Track J.9) CRYPTO harness for Go. +/// +/// Reads `NYX_PAYLOAD`, imports the fixture under +/// `internal/vulnentry`, invokes `vulnentry.(payload)`, and +/// emits a [`crate::dynamic::probe::ProbeKind::WeakKey`] probe whose +/// `key_int` is derived from the returned key. `int` returns flow +/// through as `uint64`; `[]byte` returns get reduced to the leading 8 +/// bytes via `binary.BigEndian.Uint64` (zero-padded to 8 bytes when +/// the slice is shorter), so a `crypto/rand.Read` benign control +/// trivially overshoots the predicate's 16-bit budget while the +/// `math/rand.Intn(0x10000)` vuln stays inside it. Falls back to a +/// payload-byte view when the fixture cannot be invoked so the +/// universal sink-hit path still fires. +pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { + let shim = probe_shim(); + let go_mod = generate_go_mod(); + let entry_fn = capitalize_first(&spec.entry_name); + let entry_source = read_entry_source(&spec.entry_file); + let mut extra_files = vec![("go.mod".to_owned(), go_mod)]; + let tier_a_active = !entry_source.is_empty(); + let (extra_imports, via_fixture_decl, via_fixture_invoke) = if tier_a_active { + let rewritten = rewrite_package(&entry_source, "vulnentry"); + extra_files.push(( + "internal/vulnentry/vulnentry.go".to_owned(), + rewritten, + )); + let decl = format!( + r##"func nyxCryptoViaFixture(payload string) (uint64, bool) {{ + defer func() {{ _ = recover() }}() + produced := vulnentry.{entry_fn}(payload) + keyInt, ok := nyxKeyToInt(produced) + return keyInt, ok +}} + +func nyxKeyToInt(value interface{{}}) (uint64, bool) {{ + v := reflect.ValueOf(value) + if !v.IsValid() {{ + return 0, false + }} + switch v.Kind() {{ + case reflect.Bool: + if v.Bool() {{ + return 1, true + }} + return 0, true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return uint64(v.Int()), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint(), true + case reflect.Slice: + if v.Type().Elem().Kind() == reflect.Uint8 {{ + b := v.Bytes() + var buf [8]byte + n := len(b) + if n > 8 {{ + n = 8 + }} + copy(buf[8-n:], b[:n]) + return binary.BigEndian.Uint64(buf[:]), true + }} + return 0, false + case reflect.String: + s := v.String() + var buf [8]byte + n := len(s) + if n > 8 {{ + n = 8 + }} + copy(buf[8-n:], []byte(s)[:n]) + return binary.BigEndian.Uint64(buf[:]), true + }} + return 0, false +}} + +"## + ); + let invoke = "\tkeyInt, ok := nyxCryptoViaFixture(payload)\n\tif !ok {\n\t\tvar buf [8]byte\n\t\tn := len(payload)\n\t\tif n > 8 {\n\t\t\tn = 8\n\t\t}\n\t\tcopy(buf[8-n:], []byte(payload)[:n])\n\t\tkeyInt = binary.BigEndian.Uint64(buf[:])\n\t}\n\tnyxWeakKeyProbe(keyInt)\n".to_owned(); + ( + "\t\"encoding/binary\"\n\t\"reflect\"\n\n\t\"nyx-harness/internal/vulnentry\"\n", + decl, + invoke, + ) + } else { + ( + "\t\"encoding/binary\"\n", + String::new(), + "\tvar buf [8]byte\n\tn := len(payload)\n\tif n > 8 {\n\t\tn = 8\n\t}\n\tcopy(buf[8-n:], []byte(payload)[:n])\n\tnyxWeakKeyProbe(binary.BigEndian.Uint64(buf[:]))\n".to_owned(), + ) + }; + + let source = format!( + r##"// Nyx dynamic harness — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9). +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" +{extra_imports}) + +{shim} + +func nyxWeakKeyProbe(keyInt uint64) {{ + __nyx_emit(map[string]interface{{}}{{ + "sink_callee": "__nyx_weak_key", + "args": []map[string]interface{{}}{{ + {{"kind": "Int", "value": keyInt}}, + }}, + "captured_at_ns": uint64(time.Now().UnixNano()), + "payload_id": os.Getenv("NYX_PAYLOAD_ID"), + "kind": map[string]interface{{}}{{"kind": "WeakKey", "key_int": keyInt}}, + "witness": __nyx_witness("__nyx_weak_key", []string{{fmt.Sprintf("%d", keyInt)}}), + }}) +}} + +{via_fixture_decl}func main() {{ + __nyx_install_crash_guard("__nyx_weak_key") + defer __nyx_recover_crash("__nyx_weak_key")() + payload := os.Getenv("NYX_PAYLOAD") +{via_fixture_invoke} fmt.Println("__NYX_SINK_HIT__") + body, _ := json.Marshal(map[string]interface{{}}{{"payload_len": len(payload)}}) + fmt.Println(string(body)) +}} +"## + ); + HarnessSource { + source, + filename: "main.go".to_owned(), + command: vec!["./nyx_harness".to_owned()], + extra_files, + entry_subpath: Some("entry/entry.go".to_owned()), + } +} + /// Phase 19 (Track M.1) — class-method harness for Go. /// /// `class` is mapped to a struct type declared in `entry/entry.go` @@ -2329,4 +2479,119 @@ mod tests { assert!(stub.contains("c.Writer.Header().Set(\"Location\", location)")); assert!(stub.contains("c.Writer.WriteHeader(code)")); } + + fn make_crypto_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::CRYPTO; + spec.entry_file = entry_file.to_owned(); + spec.entry_name = entry_name.to_owned(); + spec + } + + #[test] + fn emit_dispatches_to_crypto_harness_when_cap_is_crypto() { + let h = emit(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/go/vuln.go", + "Run", + )) + .unwrap(); + assert!( + h.source.contains("nyxWeakKeyProbe"), + "dispatcher must short-circuit Cap::CRYPTO into emit_crypto_harness so the weak-key probe shim is present", + ); + assert!( + h.source.contains("\"kind\": \"WeakKey\""), + "crypto harness must record probes with `kind: WeakKey` so the WeakKeyEntropy predicate fires", + ); + } + + #[test] + fn emit_crypto_harness_routes_through_internal_vulnentry_package() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/go/vuln.go", + "Run", + )); + let staged = h + .extra_files + .iter() + .find(|(name, _)| name == "internal/vulnentry/vulnentry.go"); + assert!( + staged.is_some(), + "tier-(a) crypto harness must stage the fixture under internal/vulnentry/ so main.go can import it", + ); + let body = &staged.unwrap().1; + assert!( + body.contains("package vulnentry"), + "fixture package name must be rewritten to vulnentry so the import path resolves", + ); + assert!( + h.source.contains("nyx-harness/internal/vulnentry"), + "main.go must import the rewritten vulnentry package", + ); + assert!( + h.source.contains("vulnentry.Run(payload)"), + "main.go must invoke the entry function on the rewritten fixture, not a synthetic stub", + ); + } + + #[test] + fn emit_crypto_harness_emits_weak_key_probe_kind() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/go/vuln.go", + "Run", + )); + assert!( + h.source.contains("\"kind\": \"WeakKey\", \"key_int\":"), + "Go CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires", + ); + assert!( + h.source.contains("__NYX_SINK_HIT__"), + "Go CRYPTO harness must print the universal sink-hit sentinel", + ); + } + + #[test] + fn emit_crypto_harness_reduces_byte_slice_returns_via_big_endian() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/go/benign.go", + "Run", + )); + assert!( + h.source.contains("binary.BigEndian.Uint64"), + "Go CRYPTO harness must use binary.BigEndian.Uint64 so byte-slice returns reduce to a magnitude that exceeds the 16-bit budget on CSPRNG keys", + ); + assert!( + h.source.contains("reflect.ValueOf"), + "Go CRYPTO harness must use reflect to dispatch on the produced key's type", + ); + assert!( + h.source.contains("case reflect.Slice"), + "Go CRYPTO harness must handle the []byte branch from CSPRNG benign controls", + ); + } + + #[test] + fn emit_crypto_harness_falls_back_when_fixture_source_unavailable() { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::CRYPTO; + spec.entry_file = "/nonexistent/path/missing.go".into(); + spec.entry_name = "Run".into(); + let h = emit_crypto_harness(&spec); + let staged = h + .extra_files + .iter() + .find(|(name, _)| name == "internal/vulnentry/vulnentry.go"); + assert!( + staged.is_none(), + "fallback path must not stage a vulnentry copy when the fixture cannot be read", + ); + assert!( + !h.source.contains("nyx-harness/internal/vulnentry"), + "fallback path must not import the missing vulnentry package", + ); + assert!( + h.source.contains("nyxWeakKeyProbe"), + "fallback path must still emit a weak-key probe so the universal sink-hit path fires", + ); + } } diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index 199e38fe..286a9d28 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -593,6 +593,19 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_open_redirect_harness(spec)); } + // Phase 11 (Track J.9): CRYPTO weak-RNG short-circuit. The Java + // harness reflectively loads the fixture class, invokes its + // declared method with the payload, and reduces the produced key + // into a `ProbeKind::WeakKey { key_int }` record (byte[] → + // `ByteBuffer.wrap(zero-padded[8]).order(BIG_ENDIAN).getLong()`; + // `Number` subclasses → `longValue()`). A weak + // `java.util.Random.nextBytes(new byte[2])` reduces to a sub-2^16 + // key_int; a `SecureRandom.nextBytes(new byte[32])` head-8 byte + // view overshoots the 16-bit budget. + if spec.expected_cap == crate::labels::Cap::CRYPTO { + return Ok(emit_crypto_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. Routes through // the existing `invokeReflective` helper so the harness instantiates // the receiver via its no-arg constructor (or null-fills primitive @@ -1761,6 +1774,139 @@ public class NyxHarness {{ } } +/// Phase 11 (Track J.9) CRYPTO harness for Java. +/// +/// Reflectively loads the fixture's entry class, invokes the named +/// static method with the payload, and emits a +/// [`crate::dynamic::probe::ProbeKind::WeakKey`] probe whose `key_int` +/// is reduced from the produced key. `byte[]` returns get padded to +/// 8 bytes (left-zero-padded for shorter slices, truncated to the +/// leading 8 bytes for longer ones) and decoded as big-endian via +/// `ByteBuffer.getLong()`; `Number` subclasses route through +/// `longValue()`. A 2-byte `java.util.Random.nextBytes(new byte[2])` +/// key fits inside 2^16, while `SecureRandom.nextBytes(new byte[32])` +/// produces a magnitude well above any 16-bit budget. Reflection +/// failures fall back to a payload-derived `key_int` so the universal +/// sink-hit path still fires. +pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { + let shim = probe_shim(); + let entry_source = read_entry_source(&spec.entry_file); + let entry_class = derive_entry_class(&entry_source); + let entry_fqn = derive_entry_qualifier(&entry_source, &entry_class); + let entry_method = if spec.entry_name.is_empty() { + "run".to_owned() + } else { + spec.entry_name.clone() + }; + + let source = format!( + r#"// Nyx dynamic harness — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9). +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class NyxHarness {{ +{shim} + + static void nyxWeakKeyProbe(long keyInt) {{ + String p = System.getenv("NYX_PROBE_PATH"); + if (p == null || p.isEmpty()) return; + long now = System.nanoTime(); + String pid = System.getenv("NYX_PAYLOAD_ID"); + if (pid == null) pid = ""; + StringBuilder line = new StringBuilder(192); + line.append("{{\"sink_callee\":\"__nyx_weak_key\",\"args\":["); + line.append("{{\"kind\":\"Int\",\"value\":").append(keyInt).append("}}],"); + line.append("\"captured_at_ns\":").append(now).append(','); + line.append("\"payload_id\":\""); + nyxJsonEscape(pid, line); + line.append("\",\"kind\":{{\"kind\":\"WeakKey\",\"key_int\":").append(keyInt).append("}},"); + line.append("\"witness\":"); + line.append(nyxWitnessJson("__nyx_weak_key", new String[]{{Long.toString(keyInt)}})); + line.append("}}\n"); + try (FileWriter fw = new FileWriter(p, true)) {{ + fw.write(line.toString()); + }} catch (IOException e) {{ + // best-effort + }} + }} + + static long nyxKeyToLong(Object value) {{ + if (value == null) return 0L; + if (value instanceof byte[]) {{ + byte[] b = (byte[]) value; + byte[] buf = new byte[8]; + int n = Math.min(b.length, 8); + // left-zero-pad for short slices, take leading 8 bytes for long ones + System.arraycopy(b, 0, buf, 8 - n, n); + return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); + }} + if (value instanceof Number) {{ + return ((Number) value).longValue(); + }} + if (value instanceof Boolean) {{ + return ((Boolean) value).booleanValue() ? 1L : 0L; + }} + // Fallback — UTF-8 first 8 bytes + byte[] enc = value.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] buf = new byte[8]; + int n = Math.min(enc.length, 8); + System.arraycopy(enc, 0, buf, 8 - n, n); + return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); + }} + + static long nyxPayloadFallback(String payload) {{ + if (payload == null) payload = ""; + byte[] enc = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] buf = new byte[8]; + int n = Math.min(enc.length, 8); + System.arraycopy(enc, 0, buf, 8 - n, n); + return ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong(); + }} + + public static void main(String[] args) {{ + String payload = System.getenv("NYX_PAYLOAD"); + if (payload == null) payload = ""; + long keyInt; + boolean fixtureInvoked = false; + try {{ + Class entry = Class.forName("{entry_fqn}"); + Method m = entry.getDeclaredMethod("{entry_method}", String.class); + m.setAccessible(true); + Object produced = m.invoke(null, payload); + keyInt = nyxKeyToLong(produced); + fixtureInvoked = true; + }} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {{ + keyInt = nyxPayloadFallback(payload); + }} catch (InvocationTargetException ite) {{ + keyInt = nyxPayloadFallback(payload); + }} + nyxWeakKeyProbe(keyInt); + System.out.println("__NYX_SINK_HIT__"); + if (!fixtureInvoked) {{ + System.out.println("__NYX_CRYPTO_FALLBACK__"); + }} + }} +}} +"# + ); + HarnessSource { + source, + filename: "NyxHarness.java".to_owned(), + command: vec![ + "java".to_owned(), + "-cp".to_owned(), + ".".to_owned(), + "NyxHarness".to_owned(), + ], + extra_files: Vec::new(), + entry_subpath: None, + } +} + /// Stage the `javax.servlet.*` / `jakarta.servlet.*` stub bundle when /// the entry source imports either namespace. Phase 08 / 09 fixtures /// (`HttpServletResponse.setHeader` / `.sendRedirect`) carry the @@ -3616,4 +3762,111 @@ mod tests { "harness must always import the reflective invocation path; the synthetic-only branch is gone", ); } + + fn make_crypto_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::CRYPTO; + spec.entry_file = entry_file.to_owned(); + spec.entry_name = entry_name.to_owned(); + spec + } + + #[test] + fn emit_dispatches_to_crypto_harness_when_cap_is_crypto() { + let h = emit(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/java/Vuln.java", + "run", + )) + .unwrap(); + assert!( + h.source.contains("nyxWeakKeyProbe"), + "dispatcher must short-circuit Cap::CRYPTO into emit_crypto_harness so the weak-key probe shim is present", + ); + assert!( + h.source.contains("\\\"kind\\\":\\\"WeakKey\\\""), + "crypto harness must record probes with kind: WeakKey so the WeakKeyEntropy predicate fires (search for the escaped sequence the Java emitter writes into the .java source string literal)", + ); + } + + #[test] + fn emit_crypto_harness_routes_through_reflective_entry_invocation() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("Class.forName(\"Vuln\")"), + "Java CRYPTO harness must reflectively load the fixture entry class by its derived FQN: {}", + h.source + ); + assert!( + h.source.contains("getDeclaredMethod(\"run\", String.class)"), + "Java CRYPTO harness must look up the entry method with a single String parameter", + ); + assert!( + h.source.contains("m.invoke(null, payload)"), + "Java CRYPTO harness must invoke the static method with the payload", + ); + assert_eq!( + h.filename, "NyxHarness.java", + "Java CRYPTO harness must emit a NyxHarness.java file", + ); + assert!( + h.extra_files.is_empty(), + "Java CRYPTO harness must not stage extra files — java.util.Random + SecureRandom are JDK built-ins", + ); + } + + #[test] + fn emit_crypto_harness_emits_weak_key_probe_kind() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("\\\"kind\\\":\\\"WeakKey\\\",\\\"key_int\\\":"), + "Java CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires: {}", + h.source + ); + assert!( + h.source.contains("__NYX_SINK_HIT__"), + "Java CRYPTO harness must print the universal sink-hit sentinel", + ); + } + + #[test] + fn emit_crypto_harness_reduces_byte_array_returns_via_byte_buffer() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/java/Benign.java", + "run", + )); + assert!( + h.source.contains("ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).getLong()"), + "Java CRYPTO harness must use ByteBuffer.getLong() so a 32-byte CSPRNG key produces a key_int whose magnitude exceeds the 16-bit budget", + ); + assert!( + h.source.contains("value instanceof byte[]"), + "Java CRYPTO harness must dispatch on byte[] returns explicitly", + ); + assert!( + h.source.contains("value instanceof Number"), + "Java CRYPTO harness must dispatch on Number returns explicitly", + ); + } + + #[test] + fn emit_crypto_harness_falls_back_when_reflection_fails() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/java/Vuln.java", + "run", + )); + assert!( + h.source.contains("nyxPayloadFallback(payload)"), + "Java CRYPTO harness must fall back to a payload-derived key_int when reflection fails so the universal sink-hit path still fires", + ); + assert!( + h.source.contains("ClassNotFoundException | NoSuchMethodException | IllegalAccessException"), + "Java CRYPTO harness must catch the reflective lookup exceptions and route to the fallback", + ); + } } diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index daf5fb8f..5271deef 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -688,6 +688,18 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_open_redirect_harness(spec)); } + // Phase 11 (Track J.9): short-circuit to the CRYPTO harness when + // the spec's expected cap is CRYPTO. The harness imports the + // fixture, invokes the entry function with the payload, and + // converts the returned key into a `ProbeKind::WeakKey { key_int }` + // record (int returns flow through verbatim; byte / bytearray + // returns get truncated to the leading 8 bytes via + // `int.from_bytes`, so a 32-byte CSPRNG key produces a `key_int` + // whose magnitude trivially exceeds any 16-bit budget). + if spec.expected_cap == crate::labels::Cap::CRYPTO { + return Ok(emit_crypto_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. When the spec's // entry_kind is the data-bearing `ClassMethod { class, method }` // variant the harness instantiates the class via its default @@ -2417,6 +2429,119 @@ def _nyx_follow_location(location): sys.stdout.flush() +if __name__ == "__main__": + _nyx_run() +"# + ); + HarnessSource { + source: body, + filename: "harness.py".to_owned(), + command: vec!["python3".to_owned(), "harness.py".to_owned()], + extra_files: Vec::new(), + entry_subpath: None, + } +} + +/// Phase 11 (Track J.9) CRYPTO harness for Python. +/// +/// Reads `NYX_PAYLOAD`, imports the entry module, invokes the named +/// entry function with the payload, then emits a +/// [`crate::dynamic::probe::ProbeKind::WeakKey`] probe carrying the +/// integer view of the produced key. Integer returns flow through +/// verbatim (truncated to a `u64`); `bytes`/`bytearray` returns get +/// reduced via `int.from_bytes([:8], "big")` so a CSPRNG-strong +/// benign key trivially exceeds any plausible 16-bit budget while a +/// weak `random.randint(0, 0xFFFF)` value lands well inside it. When +/// the fixture cannot be imported or raises during invocation the +/// harness falls back to emitting a `key_int` derived from the raw +/// payload bytes so the universal sink-hit path still fires. +pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { + let probe = probe_shim(); + let module_name = derive_module_name(&spec.entry_file); + let entry_name = if spec.entry_name.is_empty() { + "run".to_owned() + } else { + spec.entry_name.clone() + }; + let body = format!( + r#"#!/usr/bin/env python3 +"""Nyx dynamic harness — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9).""" +import importlib +import json +import os +import sys +import time + +{probe} + + +def _nyx_weak_key_probe(key_int): + rec = {{ + "sink_callee": "__nyx_weak_key", + "args": [ + {{"kind": "Int", "value": int(key_int)}}, + ], + "captured_at_ns": time.time_ns(), + "payload_id": os.environ.get("NYX_PAYLOAD_ID", ""), + "kind": {{"kind": "WeakKey", "key_int": int(key_int)}}, + "witness": __nyx_witness("__nyx_weak_key", [int(key_int)]), + }} + __nyx_emit(rec) + + +def _nyx_key_to_int(value): + # int → truncate to u64 magnitude + if isinstance(value, bool): + return 1 if value else 0 + if isinstance(value, int): + return value & 0xFFFFFFFFFFFFFFFF + if isinstance(value, (bytes, bytearray)): + head = bytes(value)[:8] + if not head: + return 0 + return int.from_bytes(head, "big") + # Unknown type — fall back to its string repr's first 8 bytes so + # the predicate still has something deterministic to score + try: + encoded = str(value).encode("utf-8", "replace")[:8] + except Exception: + return 0 + if not encoded: + return 0 + return int.from_bytes(encoded, "big") + + +def _nyx_crypto_via_fixture(payload): + sys.path.insert(0, ".") + try: + mod = importlib.import_module("{module_name}") + except Exception: + return None + fn = getattr(mod, "{entry_name}", None) + if fn is None: + return None + try: + return fn(payload) + except Exception: + return None + + +def _nyx_run(): + payload = os.environ.get("NYX_PAYLOAD", "") + produced = _nyx_crypto_via_fixture(payload) + if produced is None: + # Fixture path failed. Fall back to the payload-derived key + # so the universal sink-hit path still fires for outcome + # reporting; the WeakKeyEntropy predicate will reflect the + # payload's own entropy. + produced = payload.encode("utf-8", "replace") if isinstance(payload, str) else payload + key_int = _nyx_key_to_int(produced) + _nyx_weak_key_probe(key_int) + print("__NYX_SINK_HIT__", flush=True) + sys.stdout.write(json.dumps({{"key_int": key_int}}) + "\n") + sys.stdout.flush() + + if __name__ == "__main__": _nyx_run() "# @@ -3780,4 +3905,104 @@ mod tests { ); let _ = std::fs::remove_dir_all(&dir); } + + fn make_crypto_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::CRYPTO; + spec.entry_file = entry_file.to_owned(); + spec.entry_name = entry_name.to_owned(); + spec + } + + #[test] + fn emit_dispatches_to_crypto_harness_when_cap_is_crypto() { + let h = emit(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/python/vuln.py", + "run", + )) + .unwrap(); + assert!( + h.source.contains("_nyx_weak_key_probe"), + "dispatcher must short-circuit Cap::CRYPTO into emit_crypto_harness so the weak-key probe shim is present: {}", + h.source + ); + assert!( + h.source.contains("\"kind\": \"WeakKey\""), + "crypto harness must record probes with `kind: WeakKey` so the WeakKeyEntropy predicate fires", + ); + } + + #[test] + fn emit_crypto_harness_routes_through_fixture_import() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/python/vuln.py", + "run", + )); + assert!( + h.source.contains("def _nyx_crypto_via_fixture(payload):"), + "Python CRYPTO harness must define the fixture-routing helper", + ); + assert!( + h.source.contains("importlib.import_module(\"vuln\")"), + "Python CRYPTO harness must import the entry module by its file stem", + ); + assert!( + h.source.contains("getattr(mod, \"run\", None)"), + "Python CRYPTO harness must look up the entry function by name", + ); + assert!( + h.source.contains("produced = _nyx_crypto_via_fixture(payload)"), + "Python CRYPTO harness main must call the fixture-routing helper", + ); + assert_eq!( + h.filename, "harness.py", + "Python CRYPTO harness must emit a harness.py file", + ); + assert!( + h.extra_files.is_empty(), + "Python CRYPTO harness must not require per-spec deps — random + secrets are stdlib", + ); + } + + #[test] + fn emit_crypto_harness_emits_weak_key_probe_kind() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/python/vuln.py", + "run", + )); + assert!( + h.source.contains("\"kind\": \"WeakKey\", \"key_int\":"), + "Python CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires: {}", + h.source + ); + assert!( + h.source.contains("__NYX_SINK_HIT__"), + "Python CRYPTO harness must print the universal sink-hit sentinel", + ); + } + + #[test] + fn emit_crypto_harness_converts_bytes_returns_via_from_bytes() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/python/benign.py", + "run", + )); + assert!( + h.source.contains("int.from_bytes("), + "Python CRYPTO harness must reduce bytes/bytearray returns via int.from_bytes so a 32-byte CSPRNG key produces a key_int whose magnitude exceeds any 16-bit budget", + ); + assert!( + h.source.contains("isinstance(value, int):"), + "Python CRYPTO harness must keep int returns flowing through verbatim", + ); + } + + #[test] + fn emit_crypto_harness_derives_module_name_from_entry_file() { + let h = emit_crypto_harness(&make_crypto_spec("/abs/path/benign.py", "run")); + assert!( + h.source.contains("importlib.import_module(\"benign\")"), + "module name must come from the entry-file stem, not a hard-coded literal", + ); + } }