mirror of
https://github.com/open62541/open62541.git
synced 2026-10-28 20:59:28 +08:00
Add encryption_malformed_ecc_usertoken_length, which sends an ActivateSession request with a UserNameIdentityToken carrying an EccEncryptedSecret whose Length field makes endOfSecret smaller than the ECC_nistP384 signature size. Asserts the server responds with BadIdentityTokenInvalid instead of the process being killed while handling the request. Internal Vulnerability Advisory: open62541-SA-2026-0043 Co-Authored-By: X1AOxiang <X1AOxiang@users.noreply.github.com>
837 lines
31 KiB
C
837 lines
31 KiB
C
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* Copyright 2026 (c) o6 Automation GmbH (Author: Andreas Ebner)
|
|
*/
|
|
|
|
/* Consolidated ECC encryption integration tests.
|
|
* Tests all ECC security profiles added by o6 Automation:
|
|
* ECC_nistP384, ECC_brainpoolP256r1, ECC_brainpoolP384r1,
|
|
* ECC_curve25519, ECC_curve448
|
|
*
|
|
* Each curve is tested with DER and PEM certificate formats. */
|
|
|
|
#include <open62541/client.h>
|
|
#include <open62541/client_config_default.h>
|
|
#include <open62541/client_highlevel.h>
|
|
#include <open62541/plugin/securitypolicy.h>
|
|
#include <open62541/plugin/securitypolicy_default.h>
|
|
#include <open62541/plugin/certificategroup_default.h>
|
|
#include <open62541/plugin/accesscontrol_default.h>
|
|
#include <open62541/server.h>
|
|
#include <open62541/server_config_default.h>
|
|
|
|
#include "client/ua_client_internal.h"
|
|
#include "ua_server_internal.h"
|
|
#include "ua_securechannel.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "test_helpers.h"
|
|
#include "certificates.h"
|
|
#include "check.h"
|
|
#include "testing_clock.h"
|
|
#include "testing_networklayers.h"
|
|
#include "thread_wrapper.h"
|
|
|
|
/* --- Curve parameter table --- */
|
|
|
|
/* Constructor for the deprecated (non-AEAD) ECC SecurityPolicies. They are no
|
|
* longer part of the default policy set (OPC UA Part 7), so the test adds them
|
|
* explicitly. NULL for the AEAD curves (curve25519/curve448) that remain in the
|
|
* default set. */
|
|
typedef UA_StatusCode (*EccPolicyCtor)(UA_SecurityPolicy *,
|
|
const UA_ApplicationType,
|
|
const UA_ByteString,
|
|
const UA_ByteString,
|
|
const UA_Logger *);
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
const char *policyUri;
|
|
UA_Byte *certDer; size_t certDerLen;
|
|
UA_Byte *keyDer; size_t keyDerLen;
|
|
UA_Byte *certPem; size_t certPemLen;
|
|
UA_Byte *keyPem; size_t keyPemLen;
|
|
EccPolicyCtor deprecatedCtor; /* non-NULL for the deprecated curves */
|
|
} EccCurveTestData;
|
|
|
|
static EccCurveTestData eccCurves[] = {
|
|
{"ECC_nistP384",
|
|
"http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384",
|
|
CERT_P384_DER_DATA, CERT_P384_DER_LENGTH,
|
|
KEY_P384_DER_DATA, KEY_P384_DER_LENGTH,
|
|
CERT_P384_PEM_DATA, CERT_P384_PEM_LENGTH,
|
|
KEY_P384_PEM_DATA, KEY_P384_PEM_LENGTH,
|
|
UA_SecurityPolicy_EccNistP384},
|
|
{"ECC_brainpoolP256r1",
|
|
"http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1",
|
|
CERT_BP256R1_DER_DATA, CERT_BP256R1_DER_LENGTH,
|
|
KEY_BP256R1_DER_DATA, KEY_BP256R1_DER_LENGTH,
|
|
CERT_BP256R1_PEM_DATA, CERT_BP256R1_PEM_LENGTH,
|
|
KEY_BP256R1_PEM_DATA, KEY_BP256R1_PEM_LENGTH,
|
|
UA_SecurityPolicy_EccBrainpoolP256r1},
|
|
{"ECC_brainpoolP384r1",
|
|
"http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1",
|
|
CERT_BP384R1_DER_DATA, CERT_BP384R1_DER_LENGTH,
|
|
KEY_BP384R1_DER_DATA, KEY_BP384R1_DER_LENGTH,
|
|
CERT_BP384R1_PEM_DATA, CERT_BP384R1_PEM_LENGTH,
|
|
KEY_BP384R1_PEM_DATA, KEY_BP384R1_PEM_LENGTH,
|
|
UA_SecurityPolicy_EccBrainpoolP384r1},
|
|
#ifdef UA_ENABLE_ENCRYPTION_OPENSSL
|
|
{"ECC_curve25519",
|
|
"http://opcfoundation.org/UA/SecurityPolicy#ECC_curve25519",
|
|
CERT_ED25519_DER_DATA, CERT_ED25519_DER_LENGTH,
|
|
KEY_ED25519_DER_DATA, KEY_ED25519_DER_LENGTH,
|
|
CERT_ED25519_PEM_DATA, CERT_ED25519_PEM_LENGTH,
|
|
KEY_ED25519_PEM_DATA, KEY_ED25519_PEM_LENGTH,
|
|
NULL},
|
|
{"ECC_curve448",
|
|
"http://opcfoundation.org/UA/SecurityPolicy#ECC_curve448",
|
|
CERT_ED448_DER_DATA, CERT_ED448_DER_LENGTH,
|
|
KEY_ED448_DER_DATA, KEY_ED448_DER_LENGTH,
|
|
CERT_ED448_PEM_DATA, CERT_ED448_PEM_LENGTH,
|
|
KEY_ED448_PEM_DATA, KEY_ED448_PEM_LENGTH,
|
|
NULL}
|
|
#endif
|
|
};
|
|
|
|
/* Add the active curve's deprecated SecurityPolicy to a server config (no-op
|
|
* for the AEAD curves that are part of the default set). */
|
|
static void
|
|
addDeprecatedServerPolicy(UA_ServerConfig *config, EccCurveTestData *c,
|
|
UA_ByteString cert, UA_ByteString key) {
|
|
if(!c->deprecatedCtor)
|
|
return;
|
|
config->securityPolicies = (UA_SecurityPolicy *)
|
|
UA_realloc(config->securityPolicies,
|
|
sizeof(UA_SecurityPolicy) * (config->securityPoliciesSize + 1));
|
|
c->deprecatedCtor(&config->securityPolicies[config->securityPoliciesSize],
|
|
UA_APPLICATIONTYPE_SERVER, cert, key, config->logging);
|
|
config->securityPoliciesSize++;
|
|
UA_ServerConfig_addAllEndpoints(config);
|
|
}
|
|
|
|
/* Add the active curve's deprecated SecurityPolicy to a client config. */
|
|
static void
|
|
addDeprecatedClientPolicy(UA_ClientConfig *cc, EccCurveTestData *c,
|
|
UA_ByteString cert, UA_ByteString key) {
|
|
if(!c->deprecatedCtor)
|
|
return;
|
|
cc->securityPolicies = (UA_SecurityPolicy *)
|
|
UA_realloc(cc->securityPolicies,
|
|
sizeof(UA_SecurityPolicy) * (cc->securityPoliciesSize + 1));
|
|
c->deprecatedCtor(&cc->securityPolicies[cc->securityPoliciesSize],
|
|
UA_APPLICATIONTYPE_CLIENT, cert, key, cc->logging);
|
|
cc->securityPoliciesSize++;
|
|
}
|
|
|
|
#define NUM_ECC_CURVES (sizeof(eccCurves) / sizeof(eccCurves[0]))
|
|
|
|
/* --- Shared state --- */
|
|
|
|
static UA_Server *server;
|
|
static UA_Boolean running;
|
|
static THREAD_HANDLE server_thread;
|
|
static size_t activeCurve;
|
|
|
|
THREAD_CALLBACK(serverloop) {
|
|
while(running)
|
|
UA_Server_run_iterate(server, true);
|
|
return 0;
|
|
}
|
|
|
|
static void setup_common(void) {
|
|
running = true;
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certDerLen;
|
|
certificate.data = c->certDer;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyDerLen;
|
|
privateKey.data = c->keyDer;
|
|
|
|
server = UA_Server_newForUnitTestWithSecurityPolicies(
|
|
4840, &certificate, &privateKey, NULL, 0, NULL, 0, NULL, 0);
|
|
ck_assert(server != NULL);
|
|
|
|
UA_ServerConfig *config = UA_Server_getConfig(server);
|
|
UA_CertificateGroup_AcceptAll(&config->secureChannelPKI);
|
|
UA_CertificateGroup_AcceptAll(&config->sessionPKI);
|
|
|
|
UA_String_clear(&config->applicationDescription.applicationUri);
|
|
config->applicationDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
addDeprecatedServerPolicy(config, c, certificate, privateKey);
|
|
|
|
/* Add a writable test variable */
|
|
UA_VariableAttributes attr = UA_VariableAttributes_default;
|
|
UA_Int32 initValue = 42;
|
|
UA_Variant_setScalar(&attr.value, &initValue, &UA_TYPES[UA_TYPES_INT32]);
|
|
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
|
|
UA_StatusCode addRes =
|
|
UA_Server_addVariableNode(server, UA_NODEID_NUMERIC(1, 50000),
|
|
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
|
|
UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
|
|
UA_QUALIFIEDNAME(1, "TestVar"),
|
|
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
|
|
attr, NULL, NULL);
|
|
ck_assert_uint_eq(addRes, UA_STATUSCODE_GOOD);
|
|
|
|
UA_Server_run_startup(server);
|
|
THREAD_CREATE(server_thread, serverloop);
|
|
}
|
|
|
|
/* Per-curve setup functions (set activeCurve before calling common setup) */
|
|
#define DEFINE_CURVE_SETUP(N) \
|
|
static void setup_curve_##N(void) { activeCurve = N; setup_common(); }
|
|
|
|
DEFINE_CURVE_SETUP(0)
|
|
DEFINE_CURVE_SETUP(1)
|
|
DEFINE_CURVE_SETUP(2)
|
|
#ifdef UA_ENABLE_ENCRYPTION_OPENSSL
|
|
DEFINE_CURVE_SETUP(3)
|
|
DEFINE_CURVE_SETUP(4)
|
|
#endif
|
|
|
|
static void (*curveSetups[])(void) = {
|
|
setup_curve_0, setup_curve_1, setup_curve_2,
|
|
#ifdef UA_ENABLE_ENCRYPTION_OPENSSL
|
|
setup_curve_3, setup_curve_4
|
|
#endif
|
|
};
|
|
|
|
static void teardown(void) {
|
|
running = false;
|
|
THREAD_JOIN(server_thread);
|
|
UA_Server_run_shutdown(server);
|
|
UA_Server_delete(server);
|
|
}
|
|
|
|
static void pauseServer(void) {
|
|
running = false;
|
|
THREAD_JOIN(server_thread);
|
|
}
|
|
|
|
static void runServer(void) {
|
|
running = true;
|
|
THREAD_CREATE(server_thread, serverloop);
|
|
}
|
|
|
|
/* Helper: create an encrypted client for the active curve using DER certs */
|
|
static UA_Client *
|
|
createEncryptedClient(void) {
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certDerLen;
|
|
certificate.data = c->certDer;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyDerLen;
|
|
privateKey.data = c->keyDer;
|
|
|
|
UA_Client *client = UA_Client_newForUnitTest();
|
|
UA_ClientConfig *cc = UA_Client_getConfig(client);
|
|
UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
|
|
NULL, 0, NULL, 0);
|
|
cc->certificateVerification.clear(&cc->certificateVerification);
|
|
UA_CertificateGroup_AcceptAll(&cc->certificateVerification);
|
|
addDeprecatedClientPolicy(cc, c, certificate, privateKey);
|
|
cc->securityPolicyUri = UA_STRING_ALLOC(c->policyUri);
|
|
|
|
UA_String_clear(&cc->clientDescription.applicationUri);
|
|
cc->clientDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
return client;
|
|
}
|
|
|
|
/* --- Test: connect with DER certificate --- */
|
|
|
|
START_TEST(encryption_connect) {
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certDerLen;
|
|
certificate.data = c->certDer;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyDerLen;
|
|
privateKey.data = c->keyDer;
|
|
|
|
/* Discover endpoints */
|
|
UA_Client *client = UA_Client_newForUnitTest();
|
|
ck_assert(client != NULL);
|
|
|
|
UA_EndpointDescription *endpointArray = NULL;
|
|
size_t endpointArraySize = 0;
|
|
UA_StatusCode retval =
|
|
UA_Client_getEndpoints(client, "opc.tcp://localhost:4840",
|
|
&endpointArraySize, &endpointArray);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
ck_assert(endpointArraySize > 0);
|
|
UA_Array_delete(endpointArray, endpointArraySize,
|
|
&UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
|
|
UA_Client_delete(client);
|
|
|
|
/* Encrypted connect */
|
|
client = UA_Client_newForUnitTest();
|
|
UA_ClientConfig *cc = UA_Client_getConfig(client);
|
|
UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
|
|
NULL, 0, NULL, 0);
|
|
cc->certificateVerification.clear(&cc->certificateVerification);
|
|
UA_CertificateGroup_AcceptAll(&cc->certificateVerification);
|
|
addDeprecatedClientPolicy(cc, c, certificate, privateKey);
|
|
cc->securityPolicyUri = UA_STRING_ALLOC(c->policyUri);
|
|
|
|
UA_String_clear(&cc->clientDescription.applicationUri);
|
|
cc->clientDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: connect with PEM certificate --- */
|
|
|
|
START_TEST(encryption_connect_pem) {
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certPemLen;
|
|
certificate.data = c->certPem;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyPemLen;
|
|
privateKey.data = c->keyPem;
|
|
|
|
/* Discover endpoints */
|
|
UA_Client *client = UA_Client_newForUnitTest();
|
|
ck_assert(client != NULL);
|
|
|
|
UA_EndpointDescription *endpointArray = NULL;
|
|
size_t endpointArraySize = 0;
|
|
UA_StatusCode retval =
|
|
UA_Client_getEndpoints(client, "opc.tcp://localhost:4840",
|
|
&endpointArraySize, &endpointArray);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
ck_assert(endpointArraySize > 0);
|
|
UA_Array_delete(endpointArray, endpointArraySize,
|
|
&UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
|
|
UA_Client_delete(client);
|
|
|
|
/* Encrypted connect */
|
|
client = UA_Client_newForUnitTest();
|
|
UA_ClientConfig *cc = UA_Client_getConfig(client);
|
|
UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
|
|
NULL, 0, NULL, 0);
|
|
cc->certificateVerification.clear(&cc->certificateVerification);
|
|
UA_CertificateGroup_AcceptAll(&cc->certificateVerification);
|
|
|
|
UA_String_clear(&cc->clientDescription.applicationUri);
|
|
cc->clientDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
addDeprecatedClientPolicy(cc, c, certificate, privateKey);
|
|
cc->securityPolicyUri = UA_STRING_ALLOC(c->policyUri);
|
|
|
|
retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: write and read back over encrypted channel --- */
|
|
|
|
START_TEST(encryption_connect_write) {
|
|
UA_Client *client = createEncryptedClient();
|
|
ck_assert(client != NULL);
|
|
|
|
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Write a value to the test variable */
|
|
UA_NodeId testVarId = UA_NODEID_NUMERIC(1, 50000);
|
|
UA_Int32 writeValue = 1234;
|
|
UA_Variant val;
|
|
UA_Variant_setScalar(&val, &writeValue, &UA_TYPES[UA_TYPES_INT32]);
|
|
retval = UA_Client_writeValueAttribute(client, testVarId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Read back and verify */
|
|
UA_Variant readVal;
|
|
UA_Variant_init(&readVal);
|
|
retval = UA_Client_readValueAttribute(client, testVarId, &readVal);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
ck_assert(readVal.type == &UA_TYPES[UA_TYPES_INT32]);
|
|
ck_assert_int_eq(*(UA_Int32 *)readVal.data, 1234);
|
|
UA_Variant_clear(&readVal);
|
|
|
|
/* Write a different value */
|
|
writeValue = 5678;
|
|
UA_Variant_setScalar(&val, &writeValue, &UA_TYPES[UA_TYPES_INT32]);
|
|
retval = UA_Client_writeValueAttribute(client, testVarId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Read back again */
|
|
UA_Variant_init(&readVal);
|
|
retval = UA_Client_readValueAttribute(client, testVarId, &readVal);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
ck_assert_int_eq(*(UA_Int32 *)readVal.data, 5678);
|
|
UA_Variant_clear(&readVal);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: reconnect session over new SecureChannel --- */
|
|
|
|
START_TEST(encryption_reconnect) {
|
|
UA_Client *client = createEncryptedClient();
|
|
ck_assert(client != NULL);
|
|
|
|
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Initial read */
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
/* Remember the session token */
|
|
UA_NodeId oldAuthToken = client->authenticationToken;
|
|
|
|
/* Close the SecureChannel without closing the session */
|
|
UA_Client_disconnectSecureChannel(client);
|
|
|
|
/* Reconnect — reuses the previous session */
|
|
retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Read again — reactivates the session */
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
/* Session token must be the same */
|
|
ck_assert(UA_NodeId_equal(&oldAuthToken, &client->authenticationToken));
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: SecureChannel token renewal under encryption --- */
|
|
|
|
START_TEST(encryption_renew) {
|
|
UA_Client *client = createEncryptedClient();
|
|
ck_assert(client != NULL);
|
|
|
|
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_UInt32 channelId = client->channel.securityToken.channelId;
|
|
|
|
/* Pause the server thread so we can manually step client/server */
|
|
pauseServer();
|
|
|
|
/* Advance time to 80% of token lifetime to trigger renewal */
|
|
UA_fakeSleep((UA_UInt32)(client->channel.securityToken.revisedLifetime * 0.8));
|
|
UA_Client_run_iterate(client, 1);
|
|
UA_Server_run_iterate(server, false);
|
|
UA_Client_run_iterate(client, 1);
|
|
UA_Server_run_iterate(server, false);
|
|
|
|
/* Advance again to trigger a second renewal */
|
|
UA_fakeSleep((UA_UInt32)(client->channel.securityToken.revisedLifetime * 0.8));
|
|
UA_Client_run_iterate(client, 1);
|
|
UA_Server_run_iterate(server, false);
|
|
UA_Client_run_iterate(client, 1);
|
|
UA_Server_run_iterate(server, false);
|
|
|
|
/* Still the same channel */
|
|
ck_assert_uint_eq(channelId, client->channel.securityToken.channelId);
|
|
|
|
/* Resume the server thread */
|
|
runServer();
|
|
|
|
/* Read a value to verify the channel still works after renewal */
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: connect with Sign-only mode (exercises AEAD sign path
|
|
* for curve25519/448 and HMAC sign path for NIST/Brainpool) --- */
|
|
|
|
START_TEST(encryption_connect_sign_only) {
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certDerLen;
|
|
certificate.data = c->certDer;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyDerLen;
|
|
privateKey.data = c->keyDer;
|
|
|
|
UA_Client *client = UA_Client_newForUnitTest();
|
|
UA_ClientConfig *cc = UA_Client_getConfig(client);
|
|
UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
|
|
NULL, 0, NULL, 0);
|
|
cc->certificateVerification.clear(&cc->certificateVerification);
|
|
UA_CertificateGroup_AcceptAll(&cc->certificateVerification);
|
|
addDeprecatedClientPolicy(cc, c, certificate, privateKey);
|
|
cc->securityPolicyUri = UA_STRING_ALLOC(c->policyUri);
|
|
cc->securityMode = UA_MESSAGESECURITYMODE_SIGN;
|
|
|
|
UA_String_clear(&cc->clientDescription.applicationUri);
|
|
cc->clientDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* Read a value to exercise the signed channel */
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE);
|
|
retval = UA_Client_readValueAttribute(client, nodeId, &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
/* Write to exercise sign path for larger payloads */
|
|
UA_NodeId testVarId = UA_NODEID_NUMERIC(1, 50000);
|
|
UA_Int32 writeValue = 9999;
|
|
UA_Variant wval;
|
|
UA_Variant_setScalar(&wval, &writeValue, &UA_TYPES[UA_TYPES_INT32]);
|
|
retval = UA_Client_writeValueAttribute(client, testVarId, &wval);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: CSR generation for ECC policies --- */
|
|
|
|
START_TEST(encryption_csr_generation) {
|
|
UA_ServerConfig *config = UA_Server_getConfig(server);
|
|
|
|
/* Find the active curve's security policy on the server */
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
UA_SecurityPolicy *sp = NULL;
|
|
for(size_t i = 0; i < config->securityPoliciesSize; i++) {
|
|
UA_String policyUri = UA_STRING((char *)(uintptr_t)c->policyUri);
|
|
if(UA_String_equal(&config->securityPolicies[i].policyUri, &policyUri)) {
|
|
sp = &config->securityPolicies[i];
|
|
break;
|
|
}
|
|
}
|
|
ck_assert_ptr_ne(sp, NULL);
|
|
ck_assert(sp->createSigningRequest != NULL);
|
|
|
|
/* Generate a CSR without regenerating the private key */
|
|
UA_ByteString csr = UA_BYTESTRING_NULL;
|
|
UA_String subjectName = UA_STRING("CN=TestECC O=open62541");
|
|
UA_StatusCode retval = sp->createSigningRequest(sp, &subjectName,
|
|
NULL, NULL,
|
|
&csr, NULL);
|
|
/* Curve25519/448 may not support standard CSR generation */
|
|
if(retval != UA_STATUSCODE_GOOD) {
|
|
UA_ByteString_clear(&csr);
|
|
return;
|
|
}
|
|
ck_assert(csr.length > 0);
|
|
UA_ByteString_clear(&csr);
|
|
|
|
/* Generate a CSR with a new private key */
|
|
UA_ByteString newPrivKey = UA_BYTESTRING_NULL;
|
|
retval = sp->createSigningRequest(sp, &subjectName,
|
|
NULL, NULL,
|
|
&csr, &newPrivKey);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
ck_assert(csr.length > 0);
|
|
ck_assert(newPrivKey.length > 0);
|
|
UA_ByteString_clear(&csr);
|
|
UA_ByteString_clear(&newPrivKey);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: update certificate and private key on active ECC policy --- */
|
|
|
|
START_TEST(encryption_update_certificate) {
|
|
UA_ServerConfig *config = UA_Server_getConfig(server);
|
|
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
UA_SecurityPolicy *sp = NULL;
|
|
for(size_t i = 0; i < config->securityPoliciesSize; i++) {
|
|
UA_String policyUri = UA_STRING((char *)(uintptr_t)c->policyUri);
|
|
if(UA_String_equal(&config->securityPolicies[i].policyUri, &policyUri)) {
|
|
sp = &config->securityPolicies[i];
|
|
break;
|
|
}
|
|
}
|
|
ck_assert_ptr_ne(sp, NULL);
|
|
ck_assert(sp->updateCertificate != NULL);
|
|
|
|
/* Update with the same cert and key (should succeed) */
|
|
UA_ByteString certificate;
|
|
certificate.length = c->certDerLen;
|
|
certificate.data = c->certDer;
|
|
|
|
UA_ByteString privateKey;
|
|
privateKey.length = c->keyDerLen;
|
|
privateKey.data = c->keyDer;
|
|
|
|
UA_StatusCode retval = sp->updateCertificate(sp, certificate, privateKey);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
/* The policy should still be functional after update — connect and read */
|
|
UA_Client *client = createEncryptedClient();
|
|
retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_Variant val;
|
|
UA_Variant_init(&val);
|
|
retval = UA_Client_readValueAttribute(client,
|
|
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE), &val);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
UA_Variant_clear(&val);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test: malformed EccEncryptedSecret user-token length (P-384) --- */
|
|
|
|
/* Same as setup_curve_0, but also registers a UserName login so the ECC
|
|
* endpoint advertises a UserName token policy - the malformed-length code
|
|
* path in decryptUserTokenEcc is only reached via that token type. */
|
|
static void setup_p384_usertoken(void) {
|
|
running = true;
|
|
activeCurve = 0;
|
|
EccCurveTestData *c = &eccCurves[activeCurve];
|
|
|
|
UA_ByteString certificate = {c->certDerLen, c->certDer};
|
|
UA_ByteString privateKey = {c->keyDerLen, c->keyDer};
|
|
|
|
server = UA_Server_newForUnitTestWithSecurityPolicies(
|
|
4840, &certificate, &privateKey, NULL, 0, NULL, 0, NULL, 0);
|
|
ck_assert(server != NULL);
|
|
|
|
UA_ServerConfig *config = UA_Server_getConfig(server);
|
|
UA_CertificateGroup_AcceptAll(&config->secureChannelPKI);
|
|
UA_CertificateGroup_AcceptAll(&config->sessionPKI);
|
|
|
|
UA_String_clear(&config->applicationDescription.applicationUri);
|
|
config->applicationDescription.applicationUri =
|
|
UA_STRING_ALLOC("urn:unconfigured:application");
|
|
|
|
addDeprecatedServerPolicy(config, c, certificate, privateKey);
|
|
|
|
UA_UsernamePasswordLogin login = {UA_STRING_STATIC("user"), UA_STRING_STATIC("pass")};
|
|
UA_AccessControl_default(config, true, NULL, 1, &login);
|
|
|
|
UA_Server_run_startup(server);
|
|
THREAD_CREATE(server_thread, serverloop);
|
|
}
|
|
|
|
/* Replicates the client's ActivateSession signature computation so the
|
|
* server accepts the (otherwise malformed) request up to where it parses
|
|
* the token. */
|
|
static UA_StatusCode
|
|
computeClientSignatureForTest(UA_Client *client, UA_SignatureData *sd) {
|
|
UA_SecureChannel *channel = &client->channel;
|
|
const UA_SecurityPolicy *sp = channel->securityPolicy;
|
|
void *cc = channel->channelContext;
|
|
const UA_SecurityPolicySignatureAlgorithm *signAlg = &sp->asymSignatureAlgorithm;
|
|
|
|
UA_StatusCode retval = UA_STATUSCODE_GOOD;
|
|
if(!UA_SecurityPolicy_isEnhancedSecurity(sp))
|
|
retval = UA_String_copy(&signAlg->uri, &sd->algorithm);
|
|
if(retval != UA_STATUSCODE_GOOD)
|
|
return retval;
|
|
|
|
retval = UA_ByteString_allocBuffer(&sd->signature, signAlg->getLocalSignatureSize(sp, cc));
|
|
if(retval != UA_STATUSCODE_GOOD)
|
|
return retval;
|
|
|
|
if(UA_SecurityPolicy_isEnhancedSecurity(sp)) {
|
|
UA_ByteString signData = UA_BYTESTRING_NULL;
|
|
retval = UA_SecureChannel_buildActivateSessionSignatureData(
|
|
channel, &client->serverSessionNonce, &client->clientSessionNonce,
|
|
&channel->remoteCertificate, &channel->remoteCertificate,
|
|
&sp->localCertificate, &signData);
|
|
if(retval != UA_STATUSCODE_GOOD)
|
|
return retval;
|
|
retval = signAlg->sign(sp, cc, &signData, &sd->signature);
|
|
UA_ByteString_clear(&signData);
|
|
return retval;
|
|
}
|
|
|
|
UA_ByteString leaf = getLeafCertificate(channel->remoteCertificate);
|
|
UA_ByteString signData;
|
|
signData.length = leaf.length + client->serverSessionNonce.length;
|
|
signData.data = (UA_Byte *)UA_malloc(signData.length);
|
|
if(!signData.data)
|
|
return UA_STATUSCODE_BADOUTOFMEMORY;
|
|
memcpy(signData.data, leaf.data, leaf.length);
|
|
memcpy(signData.data + leaf.length, client->serverSessionNonce.data,
|
|
client->serverSessionNonce.length);
|
|
retval = signAlg->sign(sp, cc, &signData, &sd->signature);
|
|
UA_ByteString_clear(&signData);
|
|
return retval;
|
|
}
|
|
|
|
/* Builds a structurally-consistent EccEncryptedSecret whose Length field (80)
|
|
* makes endOfSecret = 89, less than the P-384 signature size (96). Before the
|
|
* fix, decryptUserTokenEcc computes endOfSecret - sigLen for the
|
|
* signature-verify length, underflowing to ~2^64 and reading off the end of
|
|
* the buffer. After the fix, the length is rejected before that subtraction. */
|
|
static void
|
|
buildMalformedEccSecret(UA_ByteString *out) {
|
|
static const unsigned char typeId[] = {0x01, 0x00, 0x8A, 0x44}; /* i=17546 */
|
|
const char *policyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384";
|
|
size_t uriLen = strlen(policyUri);
|
|
|
|
size_t off = 0;
|
|
size_t total = 89;
|
|
UA_ByteString_allocBuffer(out, total);
|
|
memcpy(out->data + off, typeId, 4); off += 4;
|
|
out->data[off++] = 0x01; /* encodingMask */
|
|
out->data[off++] = 80; out->data[off++] = 0; out->data[off++] = 0; out->data[off++] = 0;
|
|
out->data[off++] = (UA_Byte)(uriLen & 0xFF); out->data[off++] = (UA_Byte)((uriLen >> 8) & 0xFF);
|
|
out->data[off++] = 0; out->data[off++] = 0;
|
|
memcpy(out->data + off, policyUri, uriLen); off += uriLen;
|
|
out->data[off++] = 0xFF; out->data[off++] = 0xFF; out->data[off++] = 0xFF; out->data[off++] = 0xFF;
|
|
memset(out->data + off, 0, 8); off += 8;
|
|
out->data[off++] = 0; out->data[off++] = 0;
|
|
memset(out->data + off, 0, total - off);
|
|
}
|
|
|
|
START_TEST(encryption_malformed_ecc_usertoken_length) {
|
|
UA_Client *client = createEncryptedClient();
|
|
UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_ActivateSessionRequest req;
|
|
UA_ActivateSessionRequest_init(&req);
|
|
UA_NodeId_copy(&client->authenticationToken, &req.requestHeader.authenticationToken);
|
|
|
|
retval = computeClientSignatureForTest(client, &req.clientSignature);
|
|
ck_assert_uint_eq(retval, UA_STATUSCODE_GOOD);
|
|
|
|
UA_UserNameIdentityToken *unit = UA_UserNameIdentityToken_new();
|
|
unit->policyId =
|
|
UA_STRING_ALLOC("open62541-username-policy-sign+encrypt#ECC_nistP384");
|
|
unit->userName = UA_STRING_ALLOC("attacker");
|
|
buildMalformedEccSecret(&unit->password);
|
|
req.userIdentityToken.encoding = UA_EXTENSIONOBJECT_DECODED;
|
|
req.userIdentityToken.content.decoded.type = &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN];
|
|
req.userIdentityToken.content.decoded.data = unit;
|
|
|
|
UA_ActivateSessionResponse resp;
|
|
UA_ActivateSessionResponse_init(&resp);
|
|
__UA_Client_Service(client, &req, &UA_TYPES[UA_TYPES_ACTIVATESESSIONREQUEST],
|
|
&resp, &UA_TYPES[UA_TYPES_ACTIVATESESSIONRESPONSE]);
|
|
|
|
/* The malformed length must be rejected with a defined StatusCode. Prior
|
|
* to the fix, the server crashes while processing this request instead
|
|
* of returning a response. */
|
|
ck_assert_uint_eq(resp.responseHeader.serviceResult, UA_STATUSCODE_BADIDENTITYTOKENINVALID);
|
|
|
|
UA_ActivateSessionRequest_clear(&req);
|
|
UA_ActivateSessionResponse_clear(&resp);
|
|
|
|
UA_Client_disconnect(client);
|
|
UA_Client_delete(client);
|
|
}
|
|
END_TEST
|
|
|
|
/* --- Test suite: one TCase per curve --- */
|
|
|
|
static Suite* testSuite_encryption(void) {
|
|
Suite *s = suite_create("Encryption ECC");
|
|
for(size_t i = 0; i < NUM_ECC_CURVES; i++) {
|
|
TCase *tc = tcase_create(eccCurves[i].name);
|
|
tcase_add_checked_fixture(tc, curveSetups[i], teardown);
|
|
#ifdef UA_ENABLE_ENCRYPTION
|
|
tcase_add_test(tc, encryption_connect);
|
|
tcase_add_test(tc, encryption_connect_pem);
|
|
tcase_add_test(tc, encryption_connect_sign_only);
|
|
tcase_add_test(tc, encryption_csr_generation);
|
|
|
|
/* These exercise generic SecureChannel/Session behavior and certificate
|
|
* update handling. Running them for every ECC curve adds many expensive
|
|
* full handshakes without increasing curve-specific coverage. */
|
|
if(i == 0) {
|
|
tcase_add_test(tc, encryption_connect_write);
|
|
tcase_add_test(tc, encryption_reconnect);
|
|
tcase_add_test(tc, encryption_renew);
|
|
tcase_add_test(tc, encryption_update_certificate);
|
|
}
|
|
#endif /* UA_ENABLE_ENCRYPTION */
|
|
suite_add_tcase(s, tc);
|
|
}
|
|
|
|
#ifdef UA_ENABLE_ENCRYPTION
|
|
/* Separate TCase: needs a UserName login registered on the server, unlike
|
|
* the plain per-curve fixture used above. */
|
|
TCase *tcUserToken = tcase_create("ECC_nistP384 malformed user token");
|
|
tcase_add_checked_fixture(tcUserToken, setup_p384_usertoken, teardown);
|
|
tcase_add_test(tcUserToken, encryption_malformed_ecc_usertoken_length);
|
|
suite_add_tcase(s, tcUserToken);
|
|
#endif /* UA_ENABLE_ENCRYPTION */
|
|
|
|
return s;
|
|
}
|
|
|
|
int main(void) {
|
|
Suite *s = testSuite_encryption();
|
|
SRunner *sr = srunner_create(s);
|
|
srunner_set_fork_status(sr, CK_NOFORK);
|
|
srunner_run_all(sr, CK_NORMAL);
|
|
int number_failed = srunner_ntests_failed(sr);
|
|
srunner_free(sr);
|
|
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|