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 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 stack) { long ops = 0; java.util.HashSet 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 slow = new java.util.ArrayList<>(); java.util.List 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 slow = new java.util.ArrayList<>(); java.util.List 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 slow = new java.util.ArrayList<>(); java.util.List 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 slow = new java.util.ArrayList<>(); java.util.List 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 slow = new java.util.ArrayList<>(); java.util.List 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 slowStack = new java.util.ArrayList<>(); java.util.List 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); } }