mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0015 (20260522T043516Z-29b8)
This commit is contained in:
parent
60914be62c
commit
ba6e12a3f7
3 changed files with 743 additions and 0 deletions
|
|
@ -577,6 +577,18 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
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.<EntryFn>(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.<EntryFn>(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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -593,6 +593,19 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -688,6 +688,18 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
|||
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(<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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue