java-topology/defects/openssl/unit/OpenSslStoreCertSubjectsTest.java

194 lines
7.4 KiB
Java

package unit;
/**
* openssl-0004 unit test
*
* Models the O(N²) CA subject name dedup in add_uris_recursive()
* (called from SSL_add_store_cert_subjects_to_stack) and the O(N) fixed
* version that uses a hash set.
*
* The defect: sk_X509_NAME_find() on an unsorted stack is O(N) linear scan.
* Loading N certs from a store calls it N times → O(N²).
* The existing SSL_add_dir/file functions already use LHASH for O(1) dedup;
* add_uris_recursive was left behind.
*
* Compile: javac -d . OpenSslStoreCertSubjectsTest.java
* Run: java unit.OpenSslStoreCertSubjectsTest
*/
public class OpenSslStoreCertSubjectsTest {
// ------------------------------------------------------------------ //
// DEFECTIVE: O(N²) — linear scan for each new cert (unsorted stack) //
// ------------------------------------------------------------------ //
/** Models an X509_NAME as a unique integer DN string. */
static final class Name {
final String dn;
Name(String dn) { this.dn = dn; }
@Override public boolean equals(Object o) {
return o instanceof Name && ((Name) o).dn.equals(dn);
}
@Override public int hashCode() { return dn.hashCode(); }
}
/**
* SLOW — simulates add_uris_recursive with sk_X509_NAME_find (linear).
* Returns number of comparison operations performed.
*/
static long defectiveAddToStack(Name[] incoming, java.util.List<Name> stack) {
long ops = 0;
for (Name xn : incoming) {
// sk_X509_NAME_find: linear scan when stack is unsorted
boolean found = false;
for (Name existing : stack) {
ops++;
if (existing.equals(xn)) {
found = true;
break;
}
}
if (!found) {
stack.add(xn);
}
}
return ops;
}
// ------------------------------------------------------------------ //
// FIXED: O(N) — hash set for O(1) dedup (mirrors LHASH_OF fix) //
// ------------------------------------------------------------------ //
/**
* FAST — simulates add_uris_recursive with lh_X509_NAME_retrieve (hash).
* Returns number of hash operations performed.
*/
static long fixedAddToStack(Name[] incoming, java.util.List<Name> stack) {
long ops = 0;
java.util.HashSet<Name> nameHash = new java.util.HashSet<>(stack);
ops += stack.size(); // pre-populate cost (O(N_existing))
for (Name xn : incoming) {
ops++;
if (!nameHash.contains(xn)) {
stack.add(xn);
nameHash.add(xn);
}
}
return ops;
}
// ------------------------------------------------------------------ //
// Tests //
// ------------------------------------------------------------------ //
static int pass = 0, fail = 0;
static void check(String name, boolean cond) {
if (cond) {
System.out.println(" PASS " + name);
pass++;
} else {
System.out.println(" FAIL " + name);
fail++;
}
}
public static void main(String[] args) {
System.out.println("=== openssl-0004: SSL_add_store_cert_subjects O(N²) defect ===\n");
// --- Correctness tests ---
// Test 1: dedup - distinct names all added
{
Name[] certs = {new Name("CN=A"), new Name("CN=B"), new Name("CN=C")};
java.util.List<Name> slow = new java.util.ArrayList<>();
java.util.List<Name> fast = new java.util.ArrayList<>();
defectiveAddToStack(certs, slow);
fixedAddToStack(certs, fast);
check("all distinct names added (slow)", slow.size() == 3);
check("all distinct names added (fast)", fast.size() == 3);
}
// Test 2: duplicates are suppressed
{
Name a1 = new Name("CN=A");
Name a2 = new Name("CN=A"); // same DN, different object
Name b = new Name("CN=B");
Name[] certs = {a1, a2, b, a1};
java.util.List<Name> slow = new java.util.ArrayList<>();
java.util.List<Name> fast = new java.util.ArrayList<>();
defectiveAddToStack(certs, slow);
fixedAddToStack(certs, fast);
check("duplicates suppressed (slow)", slow.size() == 2);
check("duplicates suppressed (fast)", fast.size() == 2);
}
// Test 3: pre-existing stack entries are not duplicated
{
Name pre = new Name("CN=Pre");
Name[] certs = {new Name("CN=Pre"), new Name("CN=New")};
java.util.List<Name> slow = new java.util.ArrayList<>();
java.util.List<Name> fast = new java.util.ArrayList<>();
slow.add(pre);
fast.add(pre);
defectiveAddToStack(certs, slow);
fixedAddToStack(certs, fast);
check("pre-existing not duplicated (slow)", slow.size() == 2);
check("pre-existing not duplicated (fast)", fast.size() == 2);
}
// Test 4: empty store — no names added
{
Name[] empty = {};
java.util.List<Name> slow = new java.util.ArrayList<>();
java.util.List<Name> fast = new java.util.ArrayList<>();
defectiveAddToStack(empty, slow);
fixedAddToStack(empty, fast);
check("empty store → empty stack (slow)", slow.size() == 0);
check("empty store → empty stack (fast)", fast.size() == 0);
}
// Test 5: single cert, no existing stack — added once
{
Name[] certs = {new Name("CN=Solo")};
java.util.List<Name> slow = new java.util.ArrayList<>();
java.util.List<Name> fast = new java.util.ArrayList<>();
defectiveAddToStack(certs, slow);
fixedAddToStack(certs, fast);
check("single cert added (slow)", slow.size() == 1);
check("single cert added (fast)", fast.size() == 1);
}
// --- Complexity comparison ---
System.out.println();
int[] sizes = {100, 500, 1000};
for (int N : sizes) {
// N unique certs + N duplicate certs (every cert appears twice)
Name[] incoming = new Name[N * 2];
for (int i = 0; i < N; i++) {
incoming[i] = new Name("CN=Issuer-" + i);
incoming[N + i] = new Name("CN=Issuer-" + i); // duplicate
}
java.util.List<Name> slowStack = new java.util.ArrayList<>();
java.util.List<Name> fastStack = new java.util.ArrayList<>();
long slowOps = defectiveAddToStack(incoming, slowStack);
long fastOps = fixedAddToStack(incoming, fastStack);
double ratio = (double) slowOps / Math.max(fastOps, 1);
System.out.printf(
" N=%4d slow=%7d ops fast=%5d ops ratio=%.1fx%n",
N, slowOps, fastOps, ratio);
check("N=" + N + ": both stacks have " + N + " names",
slowStack.size() == N && fastStack.size() == N);
check("N=" + N + ": slow uses at least 5x more ops than fast",
ratio >= 5.0);
}
System.out.println("\n--- " + (pass + fail) + " tests: " + pass
+ " passed, " + fail + " failed ---");
if (fail > 0) System.exit(1);
}
}