diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-18 21:44:17 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-18 21:44:17 +0100 |
| commit | deaabd1464784a6fddbfa9e1ac6cb0e1148a8c34 (patch) | |
| tree | 93b6614e554db2e8c7ac0becfb0b8129ab49e141 | |
| parent | 70381b3381d77845dbc04fd521b729b7098134a5 (diff) | |
| download | BobinkCOpcUa-deaabd1464784a6fddbfa9e1ac6cb0e1148a8c34.tar.gz BobinkCOpcUa-deaabd1464784a6fddbfa9e1ac6cb0e1148a8c34.zip | |
Add X509 certificate identity token authentication
Support authMode=cert alongside anonymous and user. The client
reuses its application certificate as the X509 identity token
(open62541 requires both to match). Server-side access control
advertises the certificate token policy automatically when
sessionPKI is configured.
| -rw-r--r-- | CMakeLists.txt | 4 | ||||
| -rw-r--r-- | readme.md | 9 | ||||
| -rw-r--r-- | src/client.c | 7 | ||||
| -rw-r--r-- | src/common.c | 26 | ||||
| -rw-r--r-- | src/common.h | 29 | ||||
| -rw-r--r-- | src/server_lds.c | 2 | ||||
| -rw-r--r-- | src/server_register.c | 22 | ||||
| -rw-r--r-- | tests/secure_cert/client.conf | 13 | ||||
| -rw-r--r-- | tests/secure_cert/server_lds.conf | 13 | ||||
| -rw-r--r-- | tests/secure_cert/server_register.conf | 13 | ||||
| -rw-r--r-- | tests/secure_cert/server_register_client.conf | 13 |
11 files changed, 126 insertions, 25 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 79c3c3d..b5da8b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,9 +67,9 @@ enable_testing() set(_test_script "${CMAKE_SOURCE_DIR}/tests/run_test.sh") -set(_test_names unsecure_anonymous secure_anonymous secure_user) +set(_test_names unsecure_anonymous secure_anonymous secure_user secure_cert) -set(_test_policies None Basic256Sha256 Basic256Sha256) +set(_test_policies None Basic256Sha256 Basic256Sha256 Basic256Sha256) foreach(_name _policy IN ZIP_LISTS _test_names _test_policies) add_test(NAME "${_name}" COMMAND bash "${_test_script}" "tests/${_name}" @@ -107,13 +107,14 @@ All three programs accept an optional log level as the last argument ## Tests -Integration tests exercise three combinations of security and authentication: +Integration tests exercise four combinations of security and authentication: | Test | Security | Auth | |------|----------|------| | `unsecure_anonymous` | None / None | anonymous | | `secure_anonymous` | SignAndEncrypt / Basic256Sha256 | anonymous | | `secure_user` | SignAndEncrypt / Basic256Sha256 | user/password | +| `secure_cert` | SignAndEncrypt / Basic256Sha256 | X509 certificate | Run all tests: @@ -143,3 +144,9 @@ cmake --build build --parallel Programs are configured through plain text files (`key = value`, one per line). Example configs are in `config/`. + +Three authentication modes are supported via the `authMode` key: + +- **anonymous** — no user identity +- **user** — username and password (requires `username` and `password` keys) +- **cert** — X509 certificate identity token (reuses the application certificate; requires encryption to be configured) diff --git a/src/client.c b/src/client.c index 3d22a4d..f2166a6 100644 --- a/src/client.c +++ b/src/client.c @@ -233,9 +233,12 @@ main (int argc, char **argv) /* ---- Auth config (read-time only) ---- */ const char *username = NULL, *password = NULL; + UA_Boolean certAuth = false; if (op == OP_READ_TIME - && parseAuthConfig (&cfg, "Client", NULL, &username, &password) != 0) + && parseAuthConfig (&cfg, "Client", NULL, &username, &password, + &certAuth) + != 0) { configFree (&cfg); return EXIT_FAILURE; @@ -287,7 +290,7 @@ main (int argc, char **argv) UA_StatusCode retval = createSecureClientConfig ( UA_Client_getConfig (client), applicationUri, certPath, keyPath, - trustPaths, trustSize, secMode, secPolUri); + trustPaths, trustSize, secMode, secPolUri, certAuth); if (retval != UA_STATUSCODE_GOOD) { UA_Client_delete (client); diff --git a/src/common.c b/src/common.c index 67ea135..865fc55 100644 --- a/src/common.c +++ b/src/common.c @@ -174,7 +174,7 @@ parseLogLevel (const char *name) int parseAuthConfig (const Config *cfg, const char *program, UA_Boolean *allowAnonymous, const char **username, - const char **password) + const char **password, UA_Boolean *certAuth) { const char *authMode = configRequire (cfg, "authMode", program); if (!authMode) @@ -182,6 +182,8 @@ parseAuthConfig (const Config *cfg, const char *program, *username = NULL; *password = NULL; + if (certAuth) + *certAuth = false; if (strcmp (authMode, "anonymous") == 0) { @@ -201,8 +203,18 @@ parseAuthConfig (const Config *cfg, const char *program, return 0; } + if (strcmp (authMode, "cert") == 0) + { + if (allowAnonymous) + *allowAnonymous = false; + if (certAuth) + *certAuth = true; + return 0; + } + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, - "%s: unknown auth mode '%s' (expected 'anonymous' or 'user')", + "%s: unknown auth mode '%s' " + "(expected 'anonymous', 'user', or 'cert')", program, authMode); return -1; } @@ -422,7 +434,7 @@ createSecureClientConfig (UA_ClientConfig *cc, const char *applicationUri, const char *certPath, const char *keyPath, char **trustPaths, size_t trustSize, UA_MessageSecurityMode securityMode, - const char *securityPolicyUri) + const char *securityPolicyUri, UA_Boolean certAuth) { UA_ByteString certificate = loadFile (certPath); UA_ByteString privateKey = loadFile (keyPath); @@ -435,6 +447,14 @@ createSecureClientConfig (UA_ClientConfig *cc, const char *applicationUri, UA_StatusCode retval = UA_ClientConfig_setDefaultEncryption ( cc, certificate, privateKey, trustList, trustSize, NULL, 0); + /* X509 identity token: reuse the application certificate. open62541 + requires that the identity cert matches the SecureChannel cert, so + a separate user cert cannot be used. Call before clearing the local + buffers since setAuthenticationCert makes its own copy. */ + if (retval == UA_STATUSCODE_GOOD && certAuth) + retval + = UA_ClientConfig_setAuthenticationCert (cc, certificate, privateKey); + UA_ByteString_clear (&certificate); UA_ByteString_clear (&privateKey); for (size_t i = 0; i < trustSize; i++) diff --git a/src/common.h b/src/common.h index a531fc9..aff6ff4 100644 --- a/src/common.h +++ b/src/common.h @@ -91,22 +91,25 @@ int parseLogLevel (const char *name); /** * @brief Parses the authMode key from a configuration file. * - * When authMode is "anonymous", sets *allowAnonymous to true and - * *username / *password to NULL. When authMode is "user", sets - * *allowAnonymous to false and loads the username/password keys. + * When authMode is "anonymous", sets *allowAnonymous to true and leaves + * *username / *password as NULL. When authMode is "user", sets + * *allowAnonymous to false and loads the username/password keys. When + * authMode is "cert", sets *allowAnonymous to false and *certAuth to true. * Logs errors internally. * * @param cfg Parsed configuration. * @param program Program name (for error messages). - * @param allowAnonymous Output: true for anonymous, false for user. + * @param allowAnonymous Output: true for anonymous, false otherwise. * May be NULL (ignored — useful for client callers). * @param username Output: username string (owned by cfg), or NULL. * @param password Output: password string (owned by cfg), or NULL. + * @param certAuth Output: true when authMode is "cert", false otherwise. + * May be NULL (ignored — useful for server callers). * @return 0 on success, -1 on error. */ int parseAuthConfig (const Config *cfg, const char *program, UA_Boolean *allowAnonymous, const char **username, - const char **password); + const char **password, UA_Boolean *certAuth); /** * @brief Parses a security mode name into the corresponding enum value. @@ -149,7 +152,9 @@ UA_StatusCode createUnsecureClientConfig (UA_ClientConfig *cc, * * The config must be zero-initialized by the caller before calling this * function. Loads the certificate, private key, and trustlist, then applies - * default encryption settings. + * default encryption settings. When @p certAuth is true, also configures + * X509 certificate identity-token authentication using the same application + * certificate (mutually exclusive with username/password authentication). * * @param cc Pointer to a zero-initialized UA_ClientConfig. * @param applicationUri OPC UA application URI. @@ -161,12 +166,16 @@ UA_StatusCode createUnsecureClientConfig (UA_ClientConfig *cc, * @param securityMode Requested message security mode. * @param securityPolicyUri Security policy URI string (e.g. * "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"). + * @param certAuth When true, use the application certificate as X509 identity + * token. * @return UA_STATUSCODE_GOOD on success, error code otherwise. */ -UA_StatusCode createSecureClientConfig ( - UA_ClientConfig *cc, const char *applicationUri, const char *certPath, - const char *keyPath, char **trustPaths, size_t trustSize, - UA_MessageSecurityMode securityMode, const char *securityPolicyUri); +UA_StatusCode +createSecureClientConfig (UA_ClientConfig *cc, const char *applicationUri, + const char *certPath, const char *keyPath, + char **trustPaths, size_t trustSize, + UA_MessageSecurityMode securityMode, + const char *securityPolicyUri, UA_Boolean certAuth); /** * @brief Logs a UA_ApplicationDescription (server info from FindServers). diff --git a/src/server_lds.c b/src/server_lds.c index 99c1e8c..3307073 100644 --- a/src/server_lds.c +++ b/src/server_lds.c @@ -99,7 +99,7 @@ main (int argc, char *argv[]) UA_Boolean allowAnonymous; const char *username = NULL, *password = NULL; if (parseAuthConfig (&cfg, "ServerLDS", &allowAnonymous, &username, - &password) + &password, NULL) != 0) { configFree (&cfg); diff --git a/src/server_register.c b/src/server_register.c index 8f23d1c..705fc18 100644 --- a/src/server_register.c +++ b/src/server_register.c @@ -50,6 +50,7 @@ typedef struct int logLevel; const char *username; const char *password; + UA_Boolean certAuth; } LdsClientParams; /** @@ -65,9 +66,9 @@ makeLdsClientConfig (UA_ClientConfig *cc, const LdsClientParams *p) UA_StatusCode rv; if (p->certPath) { - rv = createSecureClientConfig (cc, p->appUri, p->certPath, p->keyPath, - p->trustPaths, p->trustSize, - p->securityMode, p->securityPolicyUri); + rv = createSecureClientConfig ( + cc, p->appUri, p->certPath, p->keyPath, p->trustPaths, p->trustSize, + p->securityMode, p->securityPolicyUri, p->certAuth); } else { @@ -157,7 +158,7 @@ main (int argc, char **argv) UA_Boolean serverAllowAnonymous; const char *serverUsername = NULL, *serverPassword = NULL; if (parseAuthConfig (&serverCfg, "ServerRegister", &serverAllowAnonymous, - &serverUsername, &serverPassword) + &serverUsername, &serverPassword, NULL) != 0) goto cleanup; @@ -230,8 +231,9 @@ main (int argc, char **argv) } const char *clientUsername = NULL, *clientPassword = NULL; + UA_Boolean clientCertAuth = false; if (parseAuthConfig (&clientCfg, "ServerRegister", NULL, &clientUsername, - &clientPassword) + &clientPassword, &clientCertAuth) != 0) goto cleanup; @@ -254,13 +256,20 @@ main (int argc, char **argv) { retval = UA_AccessControl_default (serverConfig, true, NULL, 0, NULL); } - else + else if (serverUsername) { UA_UsernamePasswordLogin logins[1]; logins[0].username = UA_STRING ((char *)serverUsername); logins[0].password = UA_STRING ((char *)serverPassword); retval = UA_AccessControl_default (serverConfig, false, NULL, 1, logins); } + else + { + /* cert auth — sessionPKI.verifyCertificate is set by createServer + via setDefaultWithSecureSecurityPolicies, so UA_AccessControl_default + will automatically advertise the X509 certificate token policy. */ + retval = UA_AccessControl_default (serverConfig, false, NULL, 0, NULL); + } if (retval != UA_STATUSCODE_GOOD) goto cleanup; @@ -278,6 +287,7 @@ main (int argc, char **argv) .logLevel = logLevel, .username = clientUsername, .password = clientPassword, + .certAuth = clientCertAuth, }; /* Use run_startup + manual event loop (instead of UA_Server_run) so we diff --git a/tests/secure_cert/client.conf b/tests/secure_cert/client.conf new file mode 100644 index 0000000..0abd582 --- /dev/null +++ b/tests/secure_cert/client.conf @@ -0,0 +1,13 @@ +# Client — test: secure_cert +# Authenticates to ServerRegister with X509 certificate identity token. + +applicationUri = urn:localhost:bobink:Client + +certificate = certs/Client_cert.der +privateKey = certs/Client_key.der +trustStore = certs/trust/client + +securityMode = SignAndEncrypt +securityPolicy = Basic256Sha256 + +authMode = cert diff --git a/tests/secure_cert/server_lds.conf b/tests/secure_cert/server_lds.conf new file mode 100644 index 0000000..ca1f8a6 --- /dev/null +++ b/tests/secure_cert/server_lds.conf @@ -0,0 +1,13 @@ +# ServerLDS — test: secure_cert +# Secured LDS with discovery-only None endpoint. + +port = 14840 +applicationUri = urn:localhost:bobink:ServerLDS + +certificate = certs/ServerLDS_cert.der +privateKey = certs/ServerLDS_key.der +trustStore = certs/trust/server_lds + +authMode = anonymous + +cleanupTimeout = 60 diff --git a/tests/secure_cert/server_register.conf b/tests/secure_cert/server_register.conf new file mode 100644 index 0000000..ba6de55 --- /dev/null +++ b/tests/secure_cert/server_register.conf @@ -0,0 +1,13 @@ +# ServerRegister server config — test: secure_cert +# Requires X509 certificate identity token for session auth. + +port = 14841 +applicationUri = urn:localhost:bobink:ServerRegister + +certificate = certs/ServerRegister_cert.der +privateKey = certs/ServerRegister_key.der +trustStore = certs/trust/server_register + +authMode = cert + +registerInterval = 10 diff --git a/tests/secure_cert/server_register_client.conf b/tests/secure_cert/server_register_client.conf new file mode 100644 index 0000000..7542bdf --- /dev/null +++ b/tests/secure_cert/server_register_client.conf @@ -0,0 +1,13 @@ +# ServerRegister client config — test: secure_cert +# Registers with the LDS (anonymous — LDS does not require cert auth). + +applicationUri = urn:localhost:bobink:ServerRegister + +certificate = certs/ServerRegisterClient_cert.der +privateKey = certs/ServerRegisterClient_key.der +trustStore = certs/trust/server_register_client + +securityMode = SignAndEncrypt +securityPolicy = Basic256Sha256 + +authMode = anonymous |
