High Performance OPC UA Server SDK  1.3.1.248
Lesson 2: Adding Security

This lesson will extend the previous example client with the possibility to establish a secure channel with security, which is cryptographically encrypted and signed.

For creating a secure channel the server's certificate as well as its supported security policies are required. This could be hard coded, but the better way is to retrieve this information from the server itself using the OPC UA Discovery service GetEndpoints.

This example assumes that the server is offering a secure endpoint with the security policy http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256. The client logic will search for such an endpoint and use it when found. Otherwise it will not try to connect. This can be seen as an example to avoid down-grade attacks.

Discovery Process

Before extending the example application you need to understand some background about the OPC UA Discovery Process. To be able to connect with security you need information about the server's available endpoint URLs, the supported security policies, supported user identity tokens and the server certificate.

All this information can be retrieved using the OPC UA Discovery Service Set (see also Part 4, OPC UA Specification). For the client there are mainly two important services when connecting to a server on a machine:

  • FindServers: Returns a list of available servers on the particular machine.
  • GetEndpoints: Returns a list of supported endpoint descriptions with all the required information for establishing a secure connection.

The Discovery process is shown in the image below. The first step when connecting to a server is to enumerate the available servers on that machine. On a PC multiple servers may be running on different ports. The Local Discovery Server (LDS) is running on the default OPC UA port 4840 and answers the FindServers request. On a device with just one server normally the OPC UA server itself is running on port 4840 and answers this request. Every OPC UA server implements FindServers, but unlike the LDS it only returns information about itself. For the client this makes no difference, because the LDS and the OPC UA Server behave in the same way.

In the second step the client calls GetEndpoints to retrieve the list of available endpoints. This time the request is sent directly to the server, not to the LDS. The client selects one endpoint and uses this information to establish the real connection with security.

Discovery Process

The Connect process is handled completely by the Client SDK. So the programmer does not need to care about creating a secure channel and a session. Also reconnects of interrupted network connections are handled automatically. But the application gets information about the individual steps in the callbacks the you can register using ua_client_set_callbacks.

The Discovery process also needs a TCP connection with a secure channel, but no session. The secure channel for retrieving information does not need to be secured and uses security policy None. Creating this connection is also handled internally to simplify the Discovery process. See UA Discovery Module for more information on the discovery API.

Client Configuration

In this example we introduce two configuration defines to configure the security policy and message mode to use.

  • CONFIG_SECURITY_POLICY Configures the security policy to use.
  • CONFIG_MESSAGE_MODE: Configures the security message mode to use.

The code will discover the list of supported endpoints of the server and will choose the best matching endpoint from this list, based on the given configuration. If the server does not support the configured policy the client will not try to connect.

/* Simple client configuration: this is normally read out from a configuration file.
* This configures the security to use when connecting to the server.
*/
#if 1
# define CONFIG_SECURITY_POLICY UA_SECURITY_POLICY_BASIC256SHA256_STRING
# define CONFIG_MESSAGE_MODE UA_MESSAGESECURITYMODE_SIGNANDENCRYPT
#else
# define CONFIG_SECURITY_POLICY UA_SECURITY_POLICY_NONE_STRING
# define CONFIG_MESSAGE_MODE UA_MESSAGESECURITYMODE_NONE
#endif

By changing #if 1 to #if 0 you can disable security and connect with securiy policy none to demonstrate that this new connection procedure also works without security.

Extending the State Machine

It is necessary to call GetEndpoints before trying to connect. Therefor new states are introduced into the state machine. One new state is SAMPLE_CLIENT_STATE_DISCOVERING which is active while the client is waiting for the GetEndpoints response. SAMPLE_CLIENT_STATE_DISCOVERED is a transient state which is set as soon as the client has received the response. The client will immediately go into the next state and try to connect.

enum sample_client_states {
SAMPLE_CLIENT_STATE_INITIAL, /* Start state, nothing has been done yet */
SAMPLE_CLIENT_STATE_DISCOVERING, /* Client is discovering endpoints */
SAMPLE_CLIENT_STATE_DISCOVERED, /* Discovery is finished */
SAMPLE_CLIENT_STATE_CONNECTING, /* Client is currently trying to connect */
SAMPLE_CLIENT_STATE_CONNECTED, /* Connecting is finished */
SAMPLE_CLIENT_STATE_READING, /* Client is currently reading */
SAMPLE_CLIENT_STATE_READ_DONE, /* Reading is finished */
SAMPLE_CLIENT_STATE_BROWSING, /* Client is currently browsing */
SAMPLE_CLIENT_STATE_BROWSE_DONE, /* Browsing is finished */
SAMPLE_CLIENT_STATE_DISCONNECT, /* Client should disconnect */
SAMPLE_CLIENT_STATE_DISCONNECTING, /* Client is currently disconnecting */
SAMPLE_CLIENT_STATE_FINISHED /* All operations are finished */
};

The function sample_client_check_state needs to be extended as well to process this new states. As you can see the connect call has moved to the DISCOVERED state, and the INITIAL state now starts with discovering by calling the function sample_client_discover.

bool sample_client_check_state(struct sample_client *ctx)
{
switch (ctx->cur_state) {
case SAMPLE_CLIENT_STATE_INITIAL:
sample_client_discover(ctx);
break;
case SAMPLE_CLIENT_STATE_DISCOVERED:
sample_client_connect(ctx);
break;
case SAMPLE_CLIENT_STATE_CONNECTED:
sample_client_read(ctx);
break;
case SAMPLE_CLIENT_STATE_READ_DONE:
sample_client_browse(ctx);
break;
case SAMPLE_CLIENT_STATE_BROWSE_DONE:
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCONNECT;
break;
case SAMPLE_CLIENT_STATE_DISCONNECT:
sample_client_disconnect(ctx);
break;
case SAMPLE_CLIENT_STATE_FINISHED:
return false;
default:
break;;
}
return true;
}

Discovery: Calling GetEndpoints

The Client SDK supports an own discovery module with the functions ua_client_discovery_begin_find_servers and ua_client_discovery_begin_get_endpoints. This simplifies the process of calling discovery services because it implicitly creates the necessary connection to the server, so that you don't need to handle connection establishment for discovery in your application state machine.

The example gets now extended with the function sample_client_discover, which gets called by the state machine code of the previous section.

static void sample_client_discover(struct sample_client *ctx)
{
int ret;
static struct ua_client_discovery_settings settings;
settings.call_timeout = 3000;
ret = ua_client_discovery_begin_get_endpoints(&settings, ctx->url, &req, sample_discovery_endpoint_cb, ctx);
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: ua_client_begin_connect failed with errorcode=%i\n", __func__, ret);
ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
} else {
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCOVERING;
}
}

Like all service calls this function works asynchronously to avoid blocking while the application waits for the response. The SDK will call the specified callback when the response was received, or a timeout occurred, whatever happens first.

In the callback the client processes the response and selects the endpoint description with configured security policy and message mode. In this case http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 and message mode SignAndEncrypt. Instead of writing the full security policy URL you can use the define UA_SECURITY_POLICY_BASIC256SHA256_STRING.

static int sample_discovery_endpoint_cb(
int result,
struct ua_responseheader *res_header,
void *cb_data)
{
struct sample_client *ctx = cb_data;
const struct ua_endpointdescription *ep;
unsigned char *cert_id;
char scertid[PKI_STORE_CERT_ID_SIZE*2+1];
int ret;
if (result != UA_EGOOD) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "GetEndpoints failed with error 0x%08x\n", result);
ctx->result = result;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
return 0;
}
if (res_header->service_result != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "GetEndpoints return status code 0x%08x\n", res_header->service_result);
ctx->result = res_header->service_result;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
return 0;
}
/* print all available endpoints */
dump_endpoints(res->endpoints, res->num_endpoints);
/* search for matchin endpoint */
ep = find_endpoint_description(res->endpoints, res->num_endpoints, CONFIG_SECURITY_POLICY, CONFIG_MESSAGE_MODE);
/* configure Client SDK to use the selected endpoint configuration */
if (ep) {
cert_id = uaapplication_get_certificate_id(ctx->app, g_appconfig.client.certificate);
pki_store_sha1_to_string(cert_id, scertid);
printf("Configuring client security:\n");
printf(" Client PKI store = %u\n", g_appconfig.client.store);
printf(" Client certificate ID = %s\n", scertid);
&ctx->client, /* the client context */
g_appconfig.client.store, /* the configured client PKI store */
cert_id, /* the configured client certificate id */
0, /* number of CA certificates for cert_id */
NULL, /* array of CA certificates (DER encoded) */
ep); /* selected endpoint configuration */
if (ret == 0) {
ctx->cur_state = SAMPLE_CLIENT_STATE_DISCOVERED;
} else {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: ua_client_set_security_from_endpointdescription failed with error %i\n", __func__, ret);
ctx->result = ret;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
}
} else {
TRACE_ERROR(TRACE_FAC_USERAPPLICATION, "%s: The server does not support the configured security policy.\n", __func__);
ctx->result = UA_EBADINVALIDSTATE;
ctx->cur_state = SAMPLE_CLIENT_STATE_FINISHED;
}
return 0;
}

The first thing to do is checking the service result if the service call succeeded. In the next step the function prints all available endpoints to console using the helper function dump_endpoints. The second helper function find_endpoint_description searches for an endpoint which matches the given configuration. Then finally the function ua_client_set_security_from_endpointdescription is called to configure the Client SDK to use the configuration of this endpoint. On success the state is set to SAMPLE_CLIENT_STATE_DISCOVERED and the application will continue to connect like in lesson01, but now with security enabled.

Helper function find_endpoint_description:

static const struct ua_endpointdescription *find_endpoint_description(
const struct ua_endpointdescription *endpoints,
int num_endpoints,
const char *security_policy,
enum ua_messagesecuritymode message_mode)
{
const struct ua_endpointdescription *ep;
int i;
for (i = 0; i < num_endpoints; ++i) {
ep = &endpoints[i];
if (ua_string_compare_const(&ep->security_policy_uri, security_policy) == 0 &&
ep->security_mode == message_mode) {
return ep;
}
}
return NULL;
}

Helper function dump_endpoints:

static void dump_endpoints(
const struct ua_endpointdescription *endpoints,
int num_endpoints)
{
const struct ua_endpointdescription *ep;
const struct ua_usertokenpolicy *ut;
int i, j;
for (i = 0; i < num_endpoints; ++i) {
ep = &endpoints[i];
printf("endpoint[%u]:\n", i);
printf(" endpoint_url: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ep->endpoint_url));
printf(" security_mode: %u\n", ep->security_mode);
printf(" security_policy_uri: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ep->security_policy_uri));
printf(" transport_profile_uri: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ep->transport_profile_uri));
printf(" security_level: %u\n", ep->security_level);
for (j = 0; j < ep->num_user_identity_tokens; ++j) {
ut = &ep->user_identity_tokens[j];
printf(" usertoken[%u]:\n", j);
printf(" policyid: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ut->policy_id));
printf(" token_type: %u\n", ut->token_type);
printf(" issued_token_type: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ut->issued_token_type));
printf(" issuer_endpoint_url: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ut->issuer_endpoint_url));
printf(" security_policy_uri: %" UA_STRING_FMT "\n", UA_STRING_ARGS(&ut->security_policy_uri));
}
}
}