aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-18 23:09:43 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-18 23:09:43 +0100
commit8bfd0dc6b44438ba6c5d2844ce21fbc2adfe3f1a (patch)
tree8dc81d68d88652f2e4c7643c5cbfd17f24809366
parent74f18c6264618187386a5dc8b1152faa8727bf53 (diff)
downloadBobinkCOpcUa-8bfd0dc6b44438ba6c5d2844ce21fbc2adfe3f1a.tar.gz
BobinkCOpcUa-8bfd0dc6b44438ba6c5d2844ce21fbc2adfe3f1a.zip
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.
-rw-r--r--CMakeLists.txt7
-rw-r--r--src/client.c5
-rw-r--r--tests/cert_bootstrap/certs/Client/cert.derbin0 -> 913 bytes
-rw-r--r--tests/cert_bootstrap/certs/Client/key.derbin0 -> 1217 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerLDS/cert.derbin0 -> 922 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerLDS/key.derbin0 -> 1217 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerRegister/cert.derbin0 -> 937 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerRegister/key.derbin0 -> 1217 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerRegisterClient/cert.derbin0 -> 949 bytes
-rw-r--r--tests/cert_bootstrap/certs/ServerRegisterClient/key.derbin0 -> 1219 bytes
-rw-r--r--tests/cert_bootstrap/certs/trust/Client_cert.derbin0 -> 913 bytes
-rw-r--r--tests/cert_bootstrap/certs/trust/ServerLDS_cert.derbin0 -> 922 bytes
-rw-r--r--tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.derbin0 -> 949 bytes
-rw-r--r--tests/cert_bootstrap/certs/trust/ServerRegister_cert.derbin0 -> 937 bytes
-rw-r--r--tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.derbin0 -> 922 bytes
-rw-r--r--tests/cert_bootstrap/client.conf14
-rw-r--r--tests/cert_bootstrap/server_lds.conf13
-rw-r--r--tests/cert_bootstrap/server_register.conf12
-rw-r--r--tests/cert_bootstrap/server_register_client.conf13
-rwxr-xr-xtests/run_cert_bootstrap_test.sh166
20 files changed, 229 insertions, 1 deletions
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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/Client/cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/Client/key.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerLDS/cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerLDS/key.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerRegister/cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerRegister/key.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerRegisterClient/cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/ServerRegisterClient/key.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/trust/Client_cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/trust/ServerLDS_cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/trust/ServerRegisterClient_cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/trust/ServerRegister_cert.der
Binary files 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
--- /dev/null
+++ b/tests/cert_bootstrap/certs/trust_client/ServerLDS_cert.der
Binary files 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 <config_dir>
+#
+# Exit: 0 when all checks pass, 1 on any failure.
+# ---------------------------------------------------------------
+set -uo pipefail
+
+CONFIG_DIR="${1:?Usage: $0 <config_dir>}"
+
+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