[RFC PATCH v1 13/13] tests: Add DecoyAuth interpolation cache coverage

Jeff Hansen x at jeffhansen.com
Fri Sep 11 12:20:04 PDT 2026


Verify cold and warm cache results against direct interpolation. Report
the measured cache preparation and reuse cost for configurable password
counts.

Signed-off-by: Jeff Hansen <x at jeffhansen.com>
---
 tests/Makefile               |  11 ++-
 tests/test-decoyauth-cache.c | 179 +++++++++++++++++++++++++++++++++++
 2 files changed, 189 insertions(+), 1 deletion(-)
 create mode 100644 tests/test-decoyauth-cache.c

diff --git a/tests/Makefile b/tests/Makefile
index ea2fa843f..dcd1b4051 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -3,7 +3,7 @@ ALL=test-base64 test-md4 test-milenage \
 	test-sha1 \
 	test-https test-https_server \
 	test-sha256 test-aes test-x509v3 test-list test-rc4 \
-	test-bss
+	test-bss test-decoyauth-cache
 
 include ../src/build.rules
 
@@ -24,6 +24,10 @@ endif
 CFLAGS += -DCONFIG_IEEE80211R_AP
 CFLAGS += -DCONFIG_IEEE80211R
 CFLAGS += -DCONFIG_TDLS
+CFLAGS += -DCONFIG_ECC
+CFLAGS += -DCONFIG_SAE_DECOYAUTH
+CFLAGS += -DCONFIG_SAE_DECOYAUTH_CACHE
+CFLAGS += -DCONFIG_SAE_DECOYAUTH_THREADS
 
 CFLAGS += -I../src
 CFLAGS += -I../src/utils
@@ -45,6 +49,7 @@ LLIBS = -Wl,--start-group $(DLIBS) -Wl,--end-group $(SLIBS)
 
 # glibc < 2.17 needs -lrt for clock_gettime()
 LLIBS += -lrt
+LLIBS += -pthread
 
 TEST=test-aes
 include test.mk
@@ -52,6 +57,9 @@ include test.mk
 TEST=test-base64
 include test.mk
 
+TEST=test-decoyauth-cache
+include test.mk
+
 TEST=test-https
 include test.mk
 
@@ -151,6 +159,7 @@ run-tests: $(ALL)
 	./test-sha1
 	./test-sha256
 	./test-bss
+	./test-decoyauth-cache
 	@echo
 	@echo All tests completed successfully.
 
diff --git a/tests/test-decoyauth-cache.c b/tests/test-decoyauth-cache.c
new file mode 100644
index 000000000..42ecfd9d1
--- /dev/null
+++ b/tests/test-decoyauth-cache.c
@@ -0,0 +1,179 @@
+/*
+ * DecoyAuth interpolation cache correctness and performance test
+ * Copyright (c) 2026, Jeff Hansen <x at jeffhansen.com>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <dirent.h>
+
+#include "common.h"
+#include "crypto/crypto.h"
+#include "crypto/sha256.h"
+
+
+static long elapsed_ms(const struct os_reltime *start,
+		       const struct os_reltime *end)
+{
+	struct os_reltime elapsed;
+
+	os_reltime_sub(end, start, &elapsed);
+	return elapsed.sec * 1000L + elapsed.usec / 1000;
+}
+
+
+static void free_values(struct crypto_bignum **values, int count)
+{
+	if (!values)
+		return;
+	for (int i = 0; i < count; i++)
+		crypto_bignum_deinit(values[i], 1);
+	os_free(values);
+}
+
+
+static struct crypto_bignum ** make_values(int count, const char *label)
+{
+	struct crypto_bignum **values;
+	u8 digest[SHA256_MAC_LEN];
+	char input[80];
+
+	values = os_calloc(count, sizeof(*values));
+	if (!values)
+		return NULL;
+	for (int i = 0; i < count; i++) {
+		const u8 *addr[1];
+		size_t len[1];
+
+		os_snprintf(input, sizeof(input), "%s-%d", label, i);
+		addr[0] = (const u8 *) input;
+		len[0] = os_strlen(input);
+		if (sha256_vector(1, addr, len, digest) < 0)
+			goto fail;
+		values[i] = crypto_bignum_init_set(digest, sizeof(digest));
+		if (!values[i])
+			goto fail;
+	}
+	forced_memzero(digest, sizeof(digest));
+	return values;
+fail:
+	forced_memzero(digest, sizeof(digest));
+	free_values(values, count);
+	return NULL;
+}
+
+
+static int coefficients_equal(struct crypto_bignum **a,
+			      struct crypto_bignum **b, int count)
+{
+	for (int i = 0; i < count; i++) {
+		if (!a[i] || !b[i] || crypto_bignum_cmp(a[i], b[i]) != 0)
+			return 0;
+	}
+	return 1;
+}
+
+
+static void remove_cache_dir(const char *path)
+{
+	DIR *dir = opendir(path);
+	struct dirent *entry;
+	char file[512];
+
+	if (!dir)
+		return;
+	while ((entry = readdir(dir))) {
+		if (os_strcmp(entry->d_name, ".") == 0 ||
+		    os_strcmp(entry->d_name, "..") == 0)
+			continue;
+		os_snprintf(file, sizeof(file), "%s/%s", path, entry->d_name);
+		unlink(file);
+	}
+	closedir(dir);
+	rmdir(path);
+}
+
+
+int main(int argc, char *argv[])
+{
+	struct crypto_ec *ec = NULL;
+	struct crypto_bignum **x = NULL, **first = NULL, **second = NULL;
+	struct crypto_bignum **direct_first = NULL, **direct_second = NULL;
+	struct crypto_bignum **cold_first = NULL, **cold_second = NULL;
+	struct crypto_bignum **warm_first = NULL, **warm_second = NULL;
+	struct crypto_interpolation_cache_stats cold = { 0 }, warm = { 0 };
+	struct os_reltime start, end;
+	char directory[] = "/tmp/decoyauth-cache-test-XXXXXX";
+	const char *configured = getenv("DECOYAUTH_CACHE_PASSWORDS");
+	long direct_ms = -1, cold_ms = -1, warm_ms = -1;
+	int count = configured ? atoi(configured) : 512;
+	int ret = 1;
+
+	if (count < 2 || count > 512) {
+		printf("DECOYAUTH_CACHE_PASSWORDS must be between 2 and 512\n");
+		return 1;
+	}
+	ec = crypto_ec_init(19);
+	x = make_values(count, "password");
+	first = make_values(count, "first");
+	second = make_values(count, "second");
+	if (!ec || !x || !first || !second || !mkdtemp(directory))
+		goto out;
+
+	os_get_reltime(&start);
+	if (crypto_interpolate_pair(x, first, second, count, ec,
+				    &direct_first, &direct_second) < 0)
+		goto out;
+	os_get_reltime(&end);
+	direct_ms = elapsed_ms(&start, &end);
+
+	os_get_reltime(&start);
+	if (crypto_interpolate_pair_cached(
+		    x, first, second, count, ec, directory, 10,
+		    &cold_first, &cold_second, &cold) < 0)
+		goto out;
+	os_get_reltime(&end);
+	cold_ms = elapsed_ms(&start, &end);
+
+	os_get_reltime(&start);
+	if (crypto_interpolate_pair_cached(
+		    x, first, second, count, ec, directory, 10,
+		    &warm_first, &warm_second, &warm) < 0)
+		goto out;
+	os_get_reltime(&end);
+	warm_ms = elapsed_ms(&start, &end);
+
+	if (cold.hit || !warm.hit ||
+	    cold.bytes != (size_t) count * count * 32 ||
+	    warm.bytes != cold.bytes ||
+	    !coefficients_equal(direct_first, cold_first, count) ||
+	    !coefficients_equal(direct_second, cold_second, count) ||
+	    !coefficients_equal(direct_first, warm_first, count) ||
+	    !coefficients_equal(direct_second, warm_second, count)) {
+		printf("DecoyAuth cache result mismatch\n");
+		goto out;
+	}
+
+	printf("DecoyAuth cache benchmark: passwords=%d matrix_bytes=%zu "
+	       "direct_ms=%ld cold_ms=%ld warm_ms=%ld saved_ms=%ld "
+	       "cold_prepare_ms=%ld cold_weave_ms=%ld warm_weave_ms=%ld\n",
+	       count, warm.bytes, direct_ms, cold_ms, warm_ms,
+	       direct_ms - warm_ms, cold.prepare_ms, cold.weave_ms,
+	       warm.weave_ms);
+	ret = 0;
+out:
+	free_values(x, count);
+	free_values(first, count);
+	free_values(second, count);
+	free_values(direct_first, count);
+	free_values(direct_second, count);
+	free_values(cold_first, count);
+	free_values(cold_second, count);
+	free_values(warm_first, count);
+	free_values(warm_second, count);
+	crypto_ec_deinit(ec);
+	remove_cache_dir(directory);
+	return ret;
+}
-- 
2.53.0




More information about the Hostap mailing list