aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-18 22:17:30 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-18 22:17:30 +0100
commit77e70beff33d89f30082f3e5d513cd657fa529ea (patch)
tree2943ddf1eb2709c8dc4414f93e4e8461d889cea5 /src
parent95f40458a9dd927fba35624564b64b5f973dd9fe (diff)
downloadBobinkCOpcUa-77e70beff33d89f30082f3e5d513cd657fa529ea.tar.gz
BobinkCOpcUa-77e70beff33d89f30082f3e5d513cd657fa529ea.zip
Add download-cert client operation with integration test
Retrieves the server's DER certificate via GetEndpoints and writes it to a local file. The test starts a secure ServerLDS, downloads its certificate, and verifies it matches the original.
Diffstat (limited to 'src')
-rw-r--r--src/client.c125
1 files changed, 118 insertions, 7 deletions
diff --git a/src/client.c b/src/client.c
index ed8b12a..011792e 100644
--- a/src/client.c
+++ b/src/client.c
@@ -2,10 +2,11 @@
* @file client.c
* @brief Unified OPC UA client for discovery and server interaction.
*
- * Supports three operations selected via CLI:
+ * Supports four operations selected via CLI:
* find-servers — queries a server's FindServers service
* get-endpoints — queries a server's GetEndpoints service
* read-time — connects to a server and reads the current time
+ * download-cert — downloads a server's certificate to a local file
*
* Encryption is optional: when certificate, privateKey, and trustStore are
* provided, the client uses the configured security policy; otherwise it
@@ -19,6 +20,7 @@
#include <open62541/client_highlevel.h>
#include <open62541/plugin/log_stdout.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -31,6 +33,7 @@ typedef enum
OP_FIND_SERVERS,
OP_GET_ENDPOINTS,
OP_READ_TIME,
+ OP_DOWNLOAD_CERT,
OP_INVALID
} Operation;
@@ -43,6 +46,8 @@ parseOperation (const char *name)
return OP_GET_ENDPOINTS;
if (strcmp (name, "read-time") == 0)
return OP_READ_TIME;
+ if (strcmp (name, "download-cert") == 0)
+ return OP_DOWNLOAD_CERT;
return OP_INVALID;
}
@@ -154,6 +159,80 @@ opReadTime (UA_Client *client, const char *url)
return rc;
}
+/**
+ * Downloads the server's certificate via GetEndpoints and writes it to a file.
+ *
+ * Picks the first endpoint that carries a non-empty serverCertificate.
+ *
+ * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise.
+ */
+static int
+opDownloadCert (UA_Client *client, const char *url, const char *outputPath)
+{
+ size_t arraySize = 0;
+ UA_EndpointDescription *array = NULL;
+
+ UA_StatusCode retval
+ = UA_Client_getEndpoints (client, url, &arraySize, &array);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "GetEndpoints failed: %s", UA_StatusCode_name (retval));
+ return EXIT_FAILURE;
+ }
+
+ UA_ByteString *cert = NULL;
+ for (size_t i = 0; i < arraySize; i++)
+ {
+ if (array[i].serverCertificate.length > 0)
+ {
+ cert = &array[i].serverCertificate;
+ break;
+ }
+ }
+
+ if (!cert)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "No endpoint returned a server certificate");
+ UA_Array_delete (array, arraySize,
+ &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ return EXIT_FAILURE;
+ }
+
+ FILE *fp = fopen (outputPath, "wb");
+ if (!fp)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not open output file: %s", outputPath);
+ UA_Array_delete (array, arraySize,
+ &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ return EXIT_FAILURE;
+ }
+
+ size_t written = fwrite (cert->data, 1, cert->length, fp);
+ fclose (fp);
+
+ int rc;
+ if (written != cert->length)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Failed to write certificate (%zu of %zu bytes)", written,
+ cert->length);
+ rc = EXIT_FAILURE;
+ }
+ else
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Certificate saved to %s (%zu bytes)", outputPath,
+ cert->length);
+ rc = EXIT_SUCCESS;
+ }
+
+ UA_Array_delete (array, arraySize, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ return rc;
+}
+
/* ========================================================================
* Main
* ======================================================================== */
@@ -161,13 +240,16 @@ opReadTime (UA_Client *client, const char *url)
int
main (int argc, char **argv)
{
- if (argc < 4 || argc > 5)
+ if (argc < 4)
{
UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
"Usage: %s <config-file> <operation> <endpoint-url> "
"[log-level]\n"
- "Operations: find-servers, get-endpoints, read-time",
- argv[0]);
+ " %s <config-file> download-cert <endpoint-url> "
+ "<output-file> [log-level]\n"
+ "Operations: find-servers, get-endpoints, read-time, "
+ "download-cert",
+ argv[0], argv[0]);
return EXIT_FAILURE;
}
@@ -175,15 +257,41 @@ main (int argc, char **argv)
if (op == OP_INVALID)
{
UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
- "Unknown operation: %s "
- "(expected find-servers, get-endpoints, read-time)",
+ "Unknown operation: %s (expected find-servers, "
+ "get-endpoints, read-time, download-cert)",
argv[2]);
return EXIT_FAILURE;
}
const char *endpointUrl = argv[3];
+ const char *outputPath = NULL;
+ const char *logLevelStr = "info";
- const char *logLevelStr = (argc == 5) ? argv[4] : "info";
+ if (op == OP_DOWNLOAD_CERT)
+ {
+ if (argc < 5 || argc > 6)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s <config-file> download-cert "
+ "<endpoint-url> <output-file> [log-level]",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+ outputPath = argv[4];
+ logLevelStr = (argc == 6) ? argv[5] : "info";
+ }
+ else
+ {
+ if (argc > 5)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s <config-file> <operation> <endpoint-url> "
+ "[log-level]",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+ logLevelStr = (argc == 5) ? argv[4] : "info";
+ }
int logLevel = parseLogLevel (logLevelStr);
if (logLevel < 0)
{
@@ -262,6 +370,9 @@ main (int argc, char **argv)
case OP_READ_TIME:
rc = opReadTime (client, endpointUrl);
break;
+ case OP_DOWNLOAD_CERT:
+ rc = opDownloadCert (client, endpointUrl, outputPath);
+ break;
default:
rc = EXIT_FAILURE;
break;