Skip to content

Commit 644140b

Browse files
feat(aop): standard-allow low-risk entropy-device reads and default temp-file creation in the secure baseline
1 parent 86740e2 commit 644140b

4 files changed

Lines changed: 122 additions & 120 deletions

File tree

src/main/java/de/tum/cit/ase/ares/api/aop/java/instrumentation/advice/JavaInstrumentationAdviceAbstractToolbox.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ static boolean isClassLoadingInProgress() {
368368
* {@link java.security.Provider}, whose {@code engineGenerateSeed} genuinely
369369
* runs beneath a real {@code java.security.SecureRandom.generateSeed(...)}
370370
* frame) — which is exactly why the trusted name sets below are restricted to
371-
* internal implementation packages stable code cannot reach, reference, or
371+
* internal implementation packages student code cannot reach, reference, or
372372
* subclass, rather than public dispatch/callback-accepting classes. The
373373
* class-loader check is defence in depth against a class-loader impersonating
374374
* one of those package names.

src/test/java/de/tum/cit/ase/ares/api/aop/java/AspectJBaselineLowRiskExemptionUnitTest.java

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
import static org.junit.jupiter.api.Assertions.assertFalse;
44

5-
import java.security.Provider;
6-
import java.security.SecureRandom;
7-
import java.security.SecureRandomSpi;
85
import java.time.LocalDate;
96
import java.time.temporal.TemporalQuery;
107
import java.util.concurrent.atomic.AtomicBoolean;
118

129
import org.junit.jupiter.api.Test;
1310

11+
import de.tum.cit.ase.ares.testutilities.FakeSecureRandomSeedingFixture;
12+
1413
import example.student.AspectJSecurityProbe;
1514

1615
/**
@@ -40,7 +39,7 @@ void isSecureRandomSeedingInProgressIsNotForgedByACustomSecureRandomSpi() throws
4039
// sun.security.provider.* internal implementation frames are - so this must
4140
// stay false.
4241
AtomicBoolean seedingDetected = new AtomicBoolean();
43-
triggerFakeSecureRandomSeeding(
42+
FakeSecureRandomSeedingFixture.triggerFakeSecureRandomSeeding(
4443
() -> seedingDetected.set(AspectJSecurityProbe.isSecureRandomSeedingInProgress()));
4544

4645
assertFalse(seedingDetected.get());
@@ -57,7 +56,7 @@ void isEntropySourceReadReturnsFalseWithoutSecureRandomFrame() throws Exception
5756
@Test
5857
void isEntropySourceReadIsNotForgedByACustomSecureRandomSpi() throws Exception {
5958
AtomicBoolean entropyReadExempt = new AtomicBoolean();
60-
triggerFakeSecureRandomSeeding(
59+
FakeSecureRandomSeedingFixture.triggerFakeSecureRandomSeeding(
6160
() -> entropyReadExempt.set(AspectJSecurityProbe.isEntropySourceRead("read", "/dev/urandom")));
6261

6362
assertFalse(entropyReadExempt.get(),
@@ -106,57 +105,4 @@ void isSystemTimezoneReadIsNotForgedByATemporalQueryCallback() throws Exception
106105
assertFalse(timezoneReadExempt.get(),
107106
"a java.time.temporal.Temporal callback must not be able to forge the system-timezone read exemption");
108107
}
109-
110-
/**
111-
* Registers a synthetic, student-authored-style {@link SecureRandomSpi} whose
112-
* {@code engineGenerateSeed} runs the given probe, then calls
113-
* {@link SecureRandom#generateSeed(int)} on it. This makes
114-
* {@code java.security.SecureRandom.generateSeed(...)} a genuine caller frame
115-
* on the real stack while the probe runs — the exact spoof the narrowed
116-
* {@code sun.security.provider.*}-only trust set is designed to reject, since
117-
* no genuine JDK seeding is actually taking place.
118-
*/
119-
private static void triggerFakeSecureRandomSeeding(SeedingProbe probe) throws Exception {
120-
ProbingSecureRandomSpi.PROBE = probe;
121-
try {
122-
Provider provider = new Provider("ares-hotfix-test-secure-random-provider", "1.0",
123-
"Ares test fixture provider for exercising the SecureRandom-seeding stack detector") {
124-
private static final long serialVersionUID = 1L;
125-
};
126-
provider.put("SecureRandom.AresProbe", ProbingSecureRandomSpi.class.getName());
127-
SecureRandom.getInstance("AresProbe", provider).generateSeed(1);
128-
} finally {
129-
ProbingSecureRandomSpi.PROBE = null;
130-
}
131-
}
132-
133-
@FunctionalInterface
134-
private interface SeedingProbe {
135-
void run() throws Exception;
136-
}
137-
138-
public static final class ProbingSecureRandomSpi extends SecureRandomSpi {
139-
140-
private static volatile SeedingProbe PROBE;
141-
142-
@Override
143-
protected byte[] engineGenerateSeed(int numBytes) {
144-
try {
145-
PROBE.run();
146-
} catch (Exception e) {
147-
throw new IllegalStateException(e);
148-
}
149-
return new byte[numBytes];
150-
}
151-
152-
@Override
153-
protected void engineSetSeed(byte[] seed) {
154-
// Not exercised by these tests.
155-
}
156-
157-
@Override
158-
protected void engineNextBytes(byte[] bytes) {
159-
// Not exercised by these tests.
160-
}
161-
}
162108
}

src/test/java/de/tum/cit/ase/ares/api/aop/java/instrumentation/advice/JavaInstrumentationAdviceFileSystemToolboxTest.java

Lines changed: 29 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
import java.nio.file.Path;
1414
import java.nio.file.StandardOpenOption;
1515
import java.nio.file.attribute.FileAttribute;
16-
import java.security.Provider;
16+
import java.security.NoSuchAlgorithmException;
1717
import java.security.SecureRandom;
18-
import java.security.SecureRandomSpi;
1918
import java.time.LocalDate;
2019
import java.time.temporal.TemporalQuery;
2120
import java.util.List;
@@ -29,6 +28,7 @@
2928
import de.tum.cit.ase.ares.api.aop.java.JavaAOPTestCase;
3029
import de.tum.cit.ase.ares.api.aop.java.JavaAOPTestCaseSettings;
3130
import de.tum.cit.ase.ares.api.aop.java.instrumentation.pointcut.JavaInstrumentationPointcutDefinitions;
31+
import de.tum.cit.ase.ares.testutilities.FakeSecureRandomSeedingFixture;
3232

3333
import example.student.InstrumentationSecurityProbe;
3434

@@ -484,7 +484,7 @@ void customSecureRandomSpiCannotForgeTheEntropyDeviceReadExemption() throws Exce
484484
// dispatch frame is not itself trusted - only genuine
485485
// sun.security.provider.* internal implementation frames are - so a
486486
// simulated entropy-device read from within it must still be denied.
487-
assertThrows(SecurityException.class, () -> triggerFakeSecureRandomSeeding(
487+
assertThrows(SecurityException.class, () -> FakeSecureRandomSeedingFixture.triggerFakeSecureRandomSeeding(
488488
() -> InstrumentationSecurityProbe.checkEntropyDeviceReadDirectly("/dev/urandom")));
489489
} finally {
490490
resetSettings();
@@ -493,20 +493,37 @@ void customSecureRandomSpiCannotForgeTheEntropyDeviceReadExemption() throws Exce
493493

494494
@Test
495495
void genuineSecureRandomEntropySeedingIsPermittedByAnActivePolicy() throws Exception {
496-
Assumptions.assumeTrue(Files.exists(Path.of("/dev/urandom")) || Files.exists(Path.of("/dev/random")),
497-
"requires an OS entropy device (Linux/BSD)");
496+
// A plain new SecureRandom().generateSeed(...) is not reliable here: the
497+
// JDK's own SeedGenerator/NativePRNG singletons open their entropy device at
498+
// most once per JVM and cache the stream for the rest of the process's
499+
// lifetime, so an unforced call could return a cached seed without ever
500+
// re-entering the woven FileInputStream constructor - meaning
501+
// assertDoesNotThrow could pass without exercising
502+
// isSecureRandomSeedingInProgress() at all. "NativePRNGBlocking" is a
503+
// distinct SecureRandomSpi (its own RandomIO instance, its own /dev/random
504+
// device) that nothing else in this codebase requests by name, which makes
505+
// it far less likely to already be warm from an earlier test in this fork -
506+
// the closest a black-box unit test can get to a genuinely reachable
507+
// JDK-internal seeding call.
508+
SecureRandom nativeBlockingSecureRandom;
509+
try {
510+
nativeBlockingSecureRandom = SecureRandom.getInstance("NativePRNGBlocking");
511+
} catch (NoSuchAlgorithmException e) {
512+
Assumptions.abort("NativePRNGBlocking unavailable on this platform (" + e.getMessage() + ")");
513+
return;
514+
}
498515
try {
499516
resetSettings();
500517
configureInstrumentationMode();
501518
JavaAOPTestCase.setJavaAdviceSettingValue("pathsAllowedToBeRead", new String[0], "ARCH", "INSTRUMENTATION");
502519

503-
// An unforced, genuinely fresh SecureRandom.generateSeed(...) call on the
504-
// JDK's own default provider must still be permitted even though the active
505-
// policy allows no read paths at all - proving the narrowed
506-
// sun.security.provider.*-only trust still recognises real JDK seeding, not
507-
// just the (now-denied) forged case above. Runs against the real
508-
// java-agent-instrumented FileInputStream constructor, not a simulated call.
509-
assertDoesNotThrow(() -> new SecureRandom().generateSeed(8));
520+
// Must still be permitted even though the active policy allows no read
521+
// paths at all - proving the narrowed sun.security.provider.*-only trust
522+
// still recognises real JDK seeding, not just the (now-denied) forged case
523+
// above. Runs against the real java-agent-instrumented FileInputStream
524+
// constructor, not a simulated call.
525+
SecureRandom finalNativeBlockingSecureRandom = nativeBlockingSecureRandom;
526+
assertDoesNotThrow(() -> finalNativeBlockingSecureRandom.generateSeed(8));
510527
} finally {
511528
resetSettings();
512529
}
@@ -775,54 +792,5 @@ private static File createNonTempDirOutsideDefaultTempDir(String name) throws IO
775792
return dir.toFile();
776793
}
777794

778-
/**
779-
* Registers a synthetic, student-authored-style {@link SecureRandomSpi} whose
780-
* {@code engineGenerateSeed} runs the given probe, then calls
781-
* {@link SecureRandom#generateSeed(int)} on it. This makes
782-
* {@code java.security.SecureRandom.generateSeed(...)} a genuine caller frame
783-
* on the real stack while the probe runs — the exact spoof the narrowed
784-
* {@code sun.security.provider.*}-only trust set is designed to reject, since
785-
* no genuine JDK seeding is actually taking place.
786-
*/
787-
private static void triggerFakeSecureRandomSeeding(SeedingProbe probe) throws Exception {
788-
ProbingSecureRandomSpi.PROBE = probe;
789-
try {
790-
Provider provider = new Provider("ares-hotfix-test-secure-random-provider", "1.0",
791-
"Ares test fixture provider for exercising the SecureRandom-seeding stack detector") {
792-
private static final long serialVersionUID = 1L;
793-
};
794-
provider.put("SecureRandom.AresProbe", ProbingSecureRandomSpi.class.getName());
795-
SecureRandom.getInstance("AresProbe", provider).generateSeed(1);
796-
} finally {
797-
ProbingSecureRandomSpi.PROBE = null;
798-
}
799-
}
800-
801-
@FunctionalInterface
802-
private interface SeedingProbe {
803-
void run();
804-
}
805-
806-
public static final class ProbingSecureRandomSpi extends SecureRandomSpi {
807-
808-
private static volatile SeedingProbe PROBE;
809-
810-
@Override
811-
protected byte[] engineGenerateSeed(int numBytes) {
812-
PROBE.run();
813-
return new byte[numBytes];
814-
}
815-
816-
@Override
817-
protected void engineSetSeed(byte[] seed) {
818-
// Not exercised by these tests.
819-
}
820-
821-
@Override
822-
protected void engineNextBytes(byte[] bytes) {
823-
// Not exercised by these tests.
824-
}
825-
}
826-
827795
// </editor-fold>
828796
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package de.tum.cit.ase.ares.testutilities;
2+
3+
import java.security.Provider;
4+
import java.security.SecureRandom;
5+
import java.security.SecureRandomSpi;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
/**
10+
* Registers a synthetic, student-authored-style {@link SecureRandomSpi} whose
11+
* {@code engineGenerateSeed} runs a caller-supplied probe, then calls
12+
* {@link SecureRandom#generateSeed(int)} on it. This makes
13+
* {@code java.security.SecureRandom.generateSeed(...)} a genuine caller frame
14+
* on the real stack while the probe runs — the exact spoof both AOP backends'
15+
* narrowed {@code sun.security.provider.*}-only SecureRandom-seeding stack
16+
* detector is designed to reject, since no genuine JDK seeding is actually
17+
* taking place.
18+
* <p>
19+
* Shared between the instrumentation and AspectJ baseline-low-risk-exemption
20+
* unit tests. The probe is threaded through {@link ProbingSecureRandomSpi} as a
21+
* constructor-injected instance field (via a custom
22+
* {@link Provider.Service#newInstance}) rather than a static field, so
23+
* concurrent/parallel test execution cannot cross-contaminate between callers.
24+
*/
25+
public final class FakeSecureRandomSeedingFixture {
26+
27+
private FakeSecureRandomSeedingFixture() {
28+
}
29+
30+
@FunctionalInterface
31+
public interface SeedingProbe {
32+
void run() throws Exception;
33+
}
34+
35+
public static void triggerFakeSecureRandomSeeding(SeedingProbe probe) throws Exception {
36+
Provider provider = new Provider("ares-hotfix-test-secure-random-provider", "1.0",
37+
"Ares test fixture provider for exercising the SecureRandom-seeding stack detector") {
38+
private static final long serialVersionUID = 1L;
39+
40+
{
41+
putService(new Service(this, "SecureRandom", "AresProbe", ProbingSecureRandomSpi.class.getName(),
42+
List.of(), Map.of()) {
43+
@Override
44+
public Object newInstance(Object constructorParameter) {
45+
return new ProbingSecureRandomSpi(probe);
46+
}
47+
});
48+
}
49+
};
50+
SecureRandom.getInstance("AresProbe", provider).generateSeed(1);
51+
}
52+
53+
public static final class ProbingSecureRandomSpi extends SecureRandomSpi {
54+
55+
private static final long serialVersionUID = 1L;
56+
57+
private final transient SeedingProbe probe;
58+
59+
ProbingSecureRandomSpi(SeedingProbe probe) {
60+
this.probe = probe;
61+
}
62+
63+
@Override
64+
protected byte[] engineGenerateSeed(int numBytes) {
65+
try {
66+
probe.run();
67+
} catch (RuntimeException e) {
68+
// Let unchecked exceptions (in particular the SecurityException this fixture
69+
// exists to provoke) propagate as-is; only a genuine checked exception from
70+
// the functional interface needs wrapping to cross the override boundary.
71+
throw e;
72+
} catch (Exception e) {
73+
throw new IllegalStateException(e);
74+
}
75+
return new byte[numBytes];
76+
}
77+
78+
@Override
79+
protected void engineSetSeed(byte[] seed) {
80+
// Not exercised by these tests.
81+
}
82+
83+
@Override
84+
protected void engineNextBytes(byte[] bytes) {
85+
// Not exercised by these tests.
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)