From 8bfd0dc6b44438ba6c5d2844ce21fbc2adfe3f1a Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Wed, 18 Feb 2026 23:09:43 +0100 Subject: Add TOFU certificate bootstrap integration test Make download-cert always use an unsecure client so it can connect to a server's None discovery endpoint without the server certificate in the trust store. Add a cert_bootstrap test that verifies the full Trust On First Use workflow: find-servers succeeds, get-endpoints fails (untrusted cert), download-cert retrieves the certificate via None, then get-endpoints and read-time both succeed. --- CMakeLists.txt | 7 + src/client.c | 5 +- tests/cert_bootstrap/certs/Client/cert.der | Bin 0 -> 913 bytes tests/cert_bootstrap/certs/Client/key.der | Bin 0 -> 1217 bytes tests/cert_bootstrap/certs/ServerLDS/cert.der | Bin 0 -> 922 bytes tests/cert_bootstrap/certs/ServerLDS/key.der | Bin 0 -> 1217 bytes tests/cert_bootstrap/certs/ServerRegister/cert.der | Bin 0 -> 937 bytes tests/cert_bootstrap/certs/ServerRegister/key.der | Bin 0 -> 1217 bytes .../certs/ServerRegisterClient/cert.der | Bin 0 -> 949 bytes .../certs/ServerRegisterClient/key.der | Bin 0 -> 1219 bytes tests/cert_bootstrap/certs/trust/Client_cert.der | Bin 0 -> 913 bytes .../cert_bootstrap/certs/trust/ServerLDS_cert.der | Bin 0 -> 922 bytes .../certs/trust/ServerRegisterClient_cert.der | Bin 0 -> 949 bytes .../certs/trust/ServerRegister_cert.der | Bin 0 -> 937 bytes .../certs/trust_client/ServerLDS_cert.der | Bin 0 -> 922 bytes tests/cert_bootstrap/client.conf | 14 ++ tests/cert_bootstrap/server_lds.conf | 13 ++ tests/cert_bootstrap/server_register.conf | 12 ++ tests/cert_bootstrap/server_register_client.conf | 13 ++ tests/run_cert_bootstrap_test.sh | 166 +++++++++++++++++++++ 20 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 tests/cert_bootstrap/certs/Client/cert.der create mode 100644 tests/cert_bootstrap/certs/Client/key.der create mode 100644 tests/cert_bootstrap/certs/ServerLDS/cert.der create mode 100644 tests/cert_bootstrap/certs/ServerLDS/key.der create mode 100644 tests/cert_bootstrap/certs/ServerRegister/cert.der create mode 100644 tests/cert_bootstrap/certs/ServerRegister/key.der create mode 100644 tests/cert_bootstrap/certs/ServerRegisterClient/cert.der create mode 100644 tests/cert_bootstrap/certs/ServerRegisterClient/key.der create mode 100644 tests/cert_bootstrap/certs/trust/Client_cert.der create mode 100644 tests/cert_bootstrap/certs/trust/ServerLDS_cert.der create mode 100644 tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.der create mode 100644 tests/cert_bootstrap/certs/trust/ServerRegister_cert.der create mode 100644 tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.der create mode 100644 tests/cert_bootstrap/client.conf create mode 100644 tests/cert_bootstrap/server_lds.conf create mode 100644 tests/cert_bootstrap/server_register.conf create mode 100644 tests/cert_bootstrap/server_register_client.conf create mode 100755 tests/run_cert_bootstrap_test.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b117b6..77d4ffb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,3 +84,10 @@ add_test(NAME "download_cert" "tests/secure_anonymous") set_tests_properties( "download_cert" PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + +add_test(NAME "cert_bootstrap" + COMMAND bash "${CMAKE_SOURCE_DIR}/tests/run_cert_bootstrap_test.sh" + "tests/cert_bootstrap") +set_tests_properties( + "cert_bootstrap" PROPERTIES WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT + 30) diff --git a/src/client.c b/src/client.c index 011792e..97a9289 100644 --- a/src/client.c +++ b/src/client.c @@ -339,7 +339,10 @@ main (int argc, char **argv) UA_Client *client = UA_Client_new (); UA_StatusCode retval; - if (sec.certPath) + if (op == OP_DOWNLOAD_CERT) + retval = createUnsecureClientConfig (UA_Client_getConfig (client), + applicationUri, NULL); + else if (sec.certPath) retval = createSecureClientConfig (UA_Client_getConfig (client), applicationUri, &sec, &auth); else diff --git a/tests/cert_bootstrap/certs/Client/cert.der b/tests/cert_bootstrap/certs/Client/cert.der new file mode 100644 index 0000000..84724c1 Binary files /dev/null and b/tests/cert_bootstrap/certs/Client/cert.der differ diff --git a/tests/cert_bootstrap/certs/Client/key.der b/tests/cert_bootstrap/certs/Client/key.der new file mode 100644 index 0000000..17403f0 Binary files /dev/null and b/tests/cert_bootstrap/certs/Client/key.der differ diff --git a/tests/cert_bootstrap/certs/ServerLDS/cert.der b/tests/cert_bootstrap/certs/ServerLDS/cert.der new file mode 100644 index 0000000..9983c3b Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerLDS/cert.der differ diff --git a/tests/cert_bootstrap/certs/ServerLDS/key.der b/tests/cert_bootstrap/certs/ServerLDS/key.der new file mode 100644 index 0000000..ca209fc Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerLDS/key.der differ diff --git a/tests/cert_bootstrap/certs/ServerRegister/cert.der b/tests/cert_bootstrap/certs/ServerRegister/cert.der new file mode 100644 index 0000000..9fb39f5 Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerRegister/cert.der differ diff --git a/tests/cert_bootstrap/certs/ServerRegister/key.der b/tests/cert_bootstrap/certs/ServerRegister/key.der new file mode 100644 index 0000000..62eaca2 Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerRegister/key.der differ diff --git a/tests/cert_bootstrap/certs/ServerRegisterClient/cert.der b/tests/cert_bootstrap/certs/ServerRegisterClient/cert.der new file mode 100644 index 0000000..937960e Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerRegisterClient/cert.der differ diff --git a/tests/cert_bootstrap/certs/ServerRegisterClient/key.der b/tests/cert_bootstrap/certs/ServerRegisterClient/key.der new file mode 100644 index 0000000..1bd17d2 Binary files /dev/null and b/tests/cert_bootstrap/certs/ServerRegisterClient/key.der differ diff --git a/tests/cert_bootstrap/certs/trust/Client_cert.der b/tests/cert_bootstrap/certs/trust/Client_cert.der new file mode 100644 index 0000000..84724c1 Binary files /dev/null and b/tests/cert_bootstrap/certs/trust/Client_cert.der differ diff --git a/tests/cert_bootstrap/certs/trust/ServerLDS_cert.der b/tests/cert_bootstrap/certs/trust/ServerLDS_cert.der new file mode 100644 index 0000000..9983c3b Binary files /dev/null and b/tests/cert_bootstrap/certs/trust/ServerLDS_cert.der differ diff --git a/tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.der b/tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.der new file mode 100644 index 0000000..937960e Binary files /dev/null and b/tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.der differ diff --git a/tests/cert_bootstrap/certs/trust/ServerRegister_cert.der b/tests/cert_bootstrap/certs/trust/ServerRegister_cert.der new file mode 100644 index 0000000..9fb39f5 Binary files /dev/null and b/tests/cert_bootstrap/certs/trust/ServerRegister_cert.der differ diff --git a/tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.der b/tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.der new file mode 100644 index 0000000..9983c3b Binary files /dev/null and b/tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.der differ diff --git a/tests/cert_bootstrap/client.conf b/tests/cert_bootstrap/client.conf new file mode 100644 index 0000000..8c54f04 --- /dev/null +++ b/tests/cert_bootstrap/client.conf @@ -0,0 +1,14 @@ +# Client — test: cert_bootstrap +# Uses a restricted trust store with only the LDS certificate. +# The ServerRegister certificate is NOT initially trusted. + +applicationUri = urn:localhost:bobink:Client + +certificate = tests/cert_bootstrap/certs/Client/cert.der +privateKey = tests/cert_bootstrap/certs/Client/key.der +trustStore = tests/cert_bootstrap/certs/trust_client + +securityMode = SignAndEncrypt +securityPolicy = Aes256_Sha256_RsaPss + +authMode = anonymous diff --git a/tests/cert_bootstrap/server_lds.conf b/tests/cert_bootstrap/server_lds.conf new file mode 100644 index 0000000..6cbfdcf --- /dev/null +++ b/tests/cert_bootstrap/server_lds.conf @@ -0,0 +1,13 @@ +# ServerLDS — test: cert_bootstrap +# Secured LDS with discovery-only None endpoint. + +port = 14840 +applicationUri = urn:localhost:bobink:ServerLDS + +certificate = tests/cert_bootstrap/certs/ServerLDS/cert.der +privateKey = tests/cert_bootstrap/certs/ServerLDS/key.der +trustStore = tests/cert_bootstrap/certs/trust + +authMode = anonymous + +cleanupTimeout = 60 diff --git a/tests/cert_bootstrap/server_register.conf b/tests/cert_bootstrap/server_register.conf new file mode 100644 index 0000000..b065f66 --- /dev/null +++ b/tests/cert_bootstrap/server_register.conf @@ -0,0 +1,12 @@ +# ServerRegister server config — test: cert_bootstrap + +port = 14841 +applicationUri = urn:localhost:bobink:ServerRegister + +certificate = tests/cert_bootstrap/certs/ServerRegister/cert.der +privateKey = tests/cert_bootstrap/certs/ServerRegister/key.der +trustStore = tests/cert_bootstrap/certs/trust + +authMode = anonymous + +registerInterval = 10 diff --git a/tests/cert_bootstrap/server_register_client.conf b/tests/cert_bootstrap/server_register_client.conf new file mode 100644 index 0000000..e1cff06 --- /dev/null +++ b/tests/cert_bootstrap/server_register_client.conf @@ -0,0 +1,13 @@ +# ServerRegister client config — test: cert_bootstrap +# Registers with the secured LDS over an encrypted channel. + +applicationUri = urn:localhost:bobink:ServerRegister + +certificate = tests/cert_bootstrap/certs/ServerRegisterClient/cert.der +privateKey = tests/cert_bootstrap/certs/ServerRegisterClient/key.der +trustStore = tests/cert_bootstrap/certs/trust + +securityMode = SignAndEncrypt +securityPolicy = Aes256_Sha256_RsaPss + +authMode = anonymous diff --git a/tests/run_cert_bootstrap_test.sh b/tests/run_cert_bootstrap_test.sh new file mode 100755 index 0000000..8d31783 --- /dev/null +++ b/tests/run_cert_bootstrap_test.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------- +# Integration test for certificate bootstrap (TOFU workflow). +# +# Demonstrates Trust On First Use: +# 1. Client finds servers via LDS (trusted) → success +# 2. Client get-endpoints on ServerRegister → FAILS (untrusted) +# 3. Client download-cert on ServerRegister via None → success +# 4. Client get-endpoints on ServerRegister → success (now trusted) +# 5. Client read-time on ServerRegister → success +# +# Usage: tests/run_cert_bootstrap_test.sh +# +# Exit: 0 when all checks pass, 1 on any failure. +# --------------------------------------------------------------- +set -uo pipefail + +CONFIG_DIR="${1:?Usage: $0 }" + +LDS_PORT=14840 +SR_PORT=14841 +LDS_PID="" +SR_PID="" +TMPFILE="" +DOWNLOADED_CERT="" +FAILURES=0 + +# ── cleanup ──────────────────────────────────────────────────── +cleanup() { + [ -n "$LDS_PID" ] && kill "$LDS_PID" 2>/dev/null && wait "$LDS_PID" 2>/dev/null + [ -n "$SR_PID" ] && kill "$SR_PID" 2>/dev/null && wait "$SR_PID" 2>/dev/null + [ -n "$TMPFILE" ] && rm -f "$TMPFILE" + [ -n "$DOWNLOADED_CERT" ] && rm -f "$DOWNLOADED_CERT" + rm -f "$CONFIG_DIR/certs/trust_client/ServerRegister_cert.der" +} +trap cleanup EXIT + +# ── helpers ──────────────────────────────────────────────────── +wait_for_port() { + local port="$1" pid="$2" label="$3" i=0 + while [ $i -lt 50 ]; do + if ! kill -0 "$pid" 2>/dev/null; then + echo "FAIL: $label exited prematurely" + exit 1 + fi + if ss -tlnp 2>/dev/null | grep -q ":${port} "; then + return 0 + fi + sleep 0.1 + i=$((i + 1)) + done + echo "FAIL: $label did not listen on port $port within 5 s" + exit 1 +} + +check() { + local label="$1" result="$2" + if [ "$result" -eq 0 ]; then + echo "PASS: $label" + else + echo "FAIL: $label" + FAILURES=$((FAILURES + 1)) + fi +} + +# ── idempotency guard ───────────────────────────────────────── +rm -f "$CONFIG_DIR/certs/trust_client/ServerRegister_cert.der" + +# ── port check ───────────────────────────────────────────────── +for port in $LDS_PORT $SR_PORT; do + if ss -tlnp 2>/dev/null | grep -q ":${port} "; then + echo "FAIL: port $port is already in use" + exit 1 + fi +done + +# ── start LDS ────────────────────────────────────────────────── +build/ServerLDS "$CONFIG_DIR/server_lds.conf" >/dev/null 2>&1 & +LDS_PID=$! +wait_for_port "$LDS_PORT" "$LDS_PID" "ServerLDS" + +# ── start ServerRegister ─────────────────────────────────────── +build/ServerRegister "$CONFIG_DIR/server_register.conf" "$CONFIG_DIR/server_register_client.conf" "opc.tcp://localhost:$LDS_PORT" >/dev/null 2>&1 & +SR_PID=$! +wait_for_port "$SR_PORT" "$SR_PID" "ServerRegister" + +TMPFILE=$(mktemp) + +# ── Step 1: FindServers on LDS (client trusts LDS) ──────────── +build/Client "$CONFIG_DIR/client.conf" find-servers "opc.tcp://localhost:$LDS_PORT" >"$TMPFILE" 2>&1 +FS_RC=$? +FS_OUTPUT=$(<"$TMPFILE") + +[ "$FS_RC" -eq 0 ] +check "find-servers exit code is 0 (got $FS_RC)" $? + +echo "$FS_OUTPUT" | grep -q "urn:localhost:bobink:ServerRegister" +check "find-servers contains urn:localhost:bobink:ServerRegister" $? + +# ── Step 2: GetEndpoints on ServerRegister (should FAIL) ────── +build/Client "$CONFIG_DIR/client.conf" get-endpoints "opc.tcp://localhost:$SR_PORT" >"$TMPFILE" 2>&1 +GE_FAIL_RC=$? +GE_FAIL_OUTPUT=$(<"$TMPFILE") + +[ "$GE_FAIL_RC" -ne 0 ] +check "get-endpoints FAILS without ServerRegister cert (exit code $GE_FAIL_RC)" $? + +# ── Step 3: download-cert from ServerRegister (via None) ────── +DOWNLOADED_CERT=$(mktemp --suffix=.der) + +build/Client "$CONFIG_DIR/client.conf" download-cert "opc.tcp://localhost:$SR_PORT" "$DOWNLOADED_CERT" >"$TMPFILE" 2>&1 +DC_RC=$? +DC_OUTPUT=$(<"$TMPFILE") + +[ "$DC_RC" -eq 0 ] +check "download-cert exit code is 0 (got $DC_RC)" $? + +echo "$DC_OUTPUT" | grep -q "Certificate saved to" +check "download-cert output contains 'Certificate saved to'" $? + +# ── Step 4: Verify downloaded cert matches original ─────────── +cmp -s "$DOWNLOADED_CERT" "$CONFIG_DIR/certs/ServerRegister/cert.der" +check "downloaded certificate matches ServerRegister cert.der" $? + +# ── Step 5: Install cert into trust_client ──────────────────── +cp "$DOWNLOADED_CERT" "$CONFIG_DIR/certs/trust_client/ServerRegister_cert.der" + +# ── Step 6: GetEndpoints on ServerRegister (should succeed) ─── +build/Client "$CONFIG_DIR/client.conf" get-endpoints "opc.tcp://localhost:$SR_PORT" >"$TMPFILE" 2>&1 +GE_RC=$? +GE_OUTPUT=$(<"$TMPFILE") + +[ "$GE_RC" -eq 0 ] +check "get-endpoints succeeds after cert install (exit code $GE_RC)" $? + +echo "$GE_OUTPUT" | grep -q "Aes256_Sha256_RsaPss" +check "get-endpoints contains Aes256_Sha256_RsaPss" $? + +# ── Step 7: ReadTime on ServerRegister (should succeed) ─────── +build/Client "$CONFIG_DIR/client.conf" read-time "opc.tcp://localhost:$SR_PORT" >"$TMPFILE" 2>&1 +RT_RC=$? +RT_OUTPUT=$(<"$TMPFILE") + +[ "$RT_RC" -eq 0 ] +check "read-time exit code is 0 (got $RT_RC)" $? + +echo "$RT_OUTPUT" | grep -q "date is:" +check "read-time output contains 'date is:'" $? + +# ── result ───────────────────────────────────────────────────── +if [ "$FAILURES" -ne 0 ]; then + echo "" + echo "--- find-servers output ---" + echo "$FS_OUTPUT" + echo "--- get-endpoints (expected fail) output ---" + echo "$GE_FAIL_OUTPUT" + echo "--- download-cert output ---" + echo "$DC_OUTPUT" + echo "--- get-endpoints (after install) output ---" + echo "$GE_OUTPUT" + echo "--- read-time output ---" + echo "$RT_OUTPUT" + echo "--- end ---" + exit 1 +fi +exit 0 -- cgit v1.2.3