open62541-sys 0.6.1

Low-level, unsafe bindings for the C11 library open62541, an open source and free implementation of OPC UA (OPC Unified Architecture).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/* 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 2014-2020 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
 *    Copyright 2017 (c) Florian Palm
 *    Copyright 2017 (c) Stefan Profanter, fortiss GmbH
 *    Copyright 2017 (c) Mark Giraud, Fraunhofer IOSB
 */

#ifndef UA_SECURECHANNEL_H_
#define UA_SECURECHANNEL_H_

struct UA_SecureChannel;
typedef struct UA_SecureChannel UA_SecureChannel;

#include <open62541/util.h>
#include <open62541/types.h>
#include <open62541/plugin/log.h>
#include <open62541/plugin/securitypolicy.h>
#include <open62541/plugin/eventloop.h>
#include <open62541/transport_generated.h>

#include "open62541_queue.h"
#include "util/ua_util_internal.h"

_UA_BEGIN_DECLS

/* True for the ECC SecurityPolicy family, including the AEAD variants
 * (ECC_nistP256_AesGcm/ChaChaPoly, ECC_curve25519/448). Use this instead of
 * spelling out the policyType enum pair at each call site. */
static UA_INLINE UA_Boolean
UA_SecurityPolicy_isEcc(const UA_SecurityPolicy *policy) {
    return policy != NULL &&
        (policy->policyType == UA_SECURITYPOLICYTYPE_ECC ||
         policy->policyType == UA_SECURITYPOLICYTYPE_ECC_AEAD);
}

/* True for AEAD SecurityPolicies (AES-GCM / ChaCha20-Poly1305). These derive a
 * combined encryption key + IV (no separate signing key) and authenticate via
 * the cipher tag; the SecureChannel and EccEncryptedSecret code use this to
 * switch on the AEAD message layout. */
static UA_INLINE UA_Boolean
UA_SecurityPolicy_isAead(const UA_SecurityPolicy *policy) {
    return policy != NULL && policy->policyType == UA_SECURITYPOLICYTYPE_ECC_AEAD;
}

/* Forward-Declaration so the SecureChannel can point to a singly-linked list of
 * Sessions. This is only used in the server, not in the client. */
struct UA_Session;
typedef struct UA_Session UA_Session;

/* The message header of the OPC UA binary protocol is structured as follows:
 *
 * - MessageType (3 Byte)
 * - IsFinal (1 Byte)
 * - MessageSize (4 Byte)
 * *** UA_SECURECHANNEL_MESSAGEHEADER_LENGTH ***
 * - SecureChannelId (4 Byte)
 * *** UA_SECURECHANNEL_CHANNELHEADER_LENGTH ***
 * - SecurityHeader (4 Byte TokenId for symmetric, otherwise dynamic length)
 * - SequenceHeader (8 Byte)
 *   - SequenceNumber
 *   - RequestId
 */

#define UA_SECURECHANNEL_MESSAGEHEADER_LENGTH 8
#define UA_SECURECHANNEL_CHANNELHEADER_LENGTH 12
#define UA_SECURECHANNEL_SYMMETRIC_SECURITYHEADER_LENGTH 4
#define UA_SECURECHANNEL_SEQUENCEHEADER_LENGTH 8
#define UA_SECURECHANNEL_SYMMETRIC_HEADER_UNENCRYPTEDLENGTH \
    (UA_SECURECHANNEL_CHANNELHEADER_LENGTH +                \
     UA_SECURECHANNEL_SYMMETRIC_SECURITYHEADER_LENGTH)
#define UA_SECURECHANNEL_SYMMETRIC_HEADER_TOTALLENGTH   \
    (UA_SECURECHANNEL_CHANNELHEADER_LENGTH +            \
    UA_SECURECHANNEL_SYMMETRIC_SECURITYHEADER_LENGTH +  \
     UA_SECURECHANNEL_SEQUENCEHEADER_LENGTH)

/* Minimum length of a valid message (ERR message with an empty reason) */
#define UA_SECURECHANNEL_MESSAGE_MIN_LENGTH 16

/* For chunked requests */
typedef struct UA_Chunk {
    TAILQ_ENTRY(UA_Chunk) pointers;
    UA_ByteString bytes;
    UA_MessageType messageType;
    UA_ChunkType chunkType;
    UA_UInt32 requestId;
    UA_Boolean copied; /* Do the bytes point to a buffer from the network or was
                        * memory allocated for the chunk separately */
} UA_Chunk;

typedef TAILQ_HEAD(UA_ChunkQueue, UA_Chunk) UA_ChunkQueue;

typedef enum {
    UA_SECURECHANNELRENEWSTATE_NORMAL,

    /* Client has sent an OPN, but not received a response so far. */
    UA_SECURECHANNELRENEWSTATE_SENT,

    /* The server waits for the first request with the new token for the rollover.
     * The new token is stored in the altSecurityToken. The configured local and
     * remote symmetric encryption keys are the old ones. */
    UA_SECURECHANNELRENEWSTATE_NEWTOKEN_SERVER,

    /* The client already uses the new token. But he waits for the server to respond
     * with the new token to complete the rollover. The old token is stored in
     * altSecurityToken. The local symmetric encryption key is new. The remote
     * encryption key is the old one. */
    UA_SECURECHANNELRENEWSTATE_NEWTOKEN_CLIENT
} UA_SecureChannelRenewState;

struct UA_SecureChannel {
    UA_SecureChannelState state;
    UA_SecureChannelRenewState renewState;
    UA_MessageSecurityMode securityMode;
    UA_ShutdownReason shutdownReason;

    UA_ConnectionConfig config;
    UA_String endpointUrl;
    UA_String remoteAddress;

    /* Connection handling in the EventLoop */
    UA_ConnectionManager *connectionManager;
    uintptr_t connectionId;

    /* The namespace mapping translates namespace indices of NodeIds during
     * de/encoding (client only) */
    UA_NamespaceMapping *namespaceMapping;

    /* Linked lists (only used in the server) */
    TAILQ_ENTRY(UA_SecureChannel) serverEntry;
    TAILQ_ENTRY(UA_SecureChannel) componentEntry;

    /* Rules for revolving the token with a renew OPN request: The client is
     * allowed to accept messages with the old token until the OPN response has
     * arrived. The server accepts the old token until one message secured with
     * the new token has arrived.
     *
     * We recognize whether nextSecurityToken contains a valid next token if the
     * ChannelId is not 0. */
    UA_ChannelSecurityToken securityToken;    /* Also contains the channelId */
    UA_ChannelSecurityToken altSecurityToken; /* Alternative token for the rollover.
                                               * See the renewState. */

    /* The endpoint and context of the channel */
    UA_SecurityPolicy *securityPolicy;
    void *channelContext; /* For interaction with the security policy */

    /* Properties derived from the SecurityPolicy URI, cached when the policy is
     * set (it is fixed for the channel's lifetime). This avoids re-evaluating
     * UA_SecurityPolicy_isEnhancedSecurity / _useLegacySequenceNumbers (which
     * scan the policy URI) on the per-chunk send and receive paths. */
    UA_Boolean enhancedSecurity;      /* UA_SecurityPolicy_isEnhancedSecurity */
    UA_Boolean legacySequenceNumbers; /* UA_SecurityPolicy_useLegacySequenceNumbers */

    /* OPC UA Part 6 v1.05.07 ยง6.7.5 "ChannelThumbprint" for
     * SecurityPolicies with secureChannelEnhancements = true.
     * The first OPN request signature is appended to the data signed by
     * the OPN response (and vice versa). Set on send, consumed on
     * receive, then cleared. Never used on OPN renewals. */
    UA_ByteString firstRequestSignature;

    /* OPC UA Part 6 v1.05.07 ยง6.8.1 step 2 "Extract" IKM chaining
     * accumulator for SecurityPolicies with secureChannelEnhancements =
     * true. The IKM is the size of the policy's shared secret (e.g. 32
     * bytes for P-256). On each renewal, the policy's generateKey
     * wrapper XORs the new shared secret with this value and writes the
     * result back. The SecureChannel passes this value as a prefix of
     * the `secret` ByteString to the policy's generateKey, where the
     * policy strips and updates it. Zero-length on the first OPN. */
    UA_ByteString currentIKM;

    /* OPC UA Part 6 v1.05.07 ChannelThumbprint: the (verified) signature
     * of the first OpenSecureChannel *response*. Captured once on the
     * first OPN (client: on response verify; server: on response sign)
     * and preserved across renewals. Used to bind the CreateSession /
     * ActivateSession SignatureData to this SecureChannel. Only set for
     * SecurityPolicies with secureChannelEnhancements = true. */
    UA_ByteString channelThumbprint;

    /* Asymmetric encryption info */
    UA_ByteString remoteCertificate;
    UA_Byte remoteCertificateThumbprint[20]; /* The thumbprint of the remote certificate */

    /* Symmetric encryption nonces. These are used to generate the key material
     * and must not be reused once the keys are in place.
     *
     * Nonces are also used during the CreateSession / ActivateSession
     * handshake. These are not handled here, as the Session handling can
     * overlap with a RenewSecureChannel. */
    UA_ByteString remoteNonce;
    UA_ByteString localNonce;

    UA_UInt32 receiveSequenceNumber;
    UA_UInt32 sendSequenceNumber;

    /* Sessions that are bound to the SecureChannel (singly-linked list, only
     * used in the server) */
    UA_Session *sessions;

    /* (Decrypted) chunks waiting to be processed */
    UA_ChunkQueue chunks;
    size_t chunksCount;
    size_t chunksLength;

    /* Received buffer from which no chunks have been extracted so far */
    UA_ByteString unprocessed;
    size_t unprocessedOffset;
    UA_Boolean unprocessedCopied;
    UA_DelayedCallback unprocessedDelayed;

    void *processOPNHeaderApplication;
    UA_StatusCode (*processOPNHeader)(void *application, UA_SecureChannel *channel,
                                      const UA_AsymmetricAlgorithmSecurityHeader *asymHeader);
};

void UA_SecureChannel_init(UA_SecureChannel *channel);

/* Trigger the shutdown */
void UA_SecureChannel_shutdown(UA_SecureChannel *channel,
                               UA_ShutdownReason shutdownReason);

/* Eventual cleanup after the channel has closed. It is possible to call _init
 * on the channel afterwards to reset it to the fresh status. */
void UA_SecureChannel_clear(UA_SecureChannel *channel);

/* Process the remote configuration in the HEL/ACK handshake. The connection
 * config is initialized with the local settings. */
UA_StatusCode
UA_SecureChannel_processHELACK(UA_SecureChannel *channel,
                               const UA_TcpAcknowledgeMessage *remoteConfig);

UA_StatusCode
UA_SecureChannel_setSecurityPolicy(UA_SecureChannel *channel,
                                   UA_SecurityPolicy *securityPolicy,
                                   const UA_ByteString *remoteCertificate);

UA_StatusCode
UA_SecureChannel_setSecurityMode(UA_SecureChannel *channel,
                                 UA_MessageSecurityMode securityMode);

UA_Boolean
UA_SecureChannel_isConnected(UA_SecureChannel *channel);

/* Returns true if the channel has timed out. Performs the SecurityToken
 * rollover if required and possible. */
UA_Boolean
UA_SecureChannel_checkTimeout(UA_SecureChannel *channel,
                              UA_DateTime nowMonotonic);

/* Remove (partially) received unprocessed chunks */
void
UA_SecureChannel_deleteBuffered(UA_SecureChannel *channel);

/* Wrapper function for generating a local nonce for the supplied channel. Uses
 * the random generator of the channels security policy to allocate and generate
 * a nonce with the specified length. */
UA_StatusCode
UA_SecureChannel_generateLocalNonce(UA_SecureChannel *channel);

UA_StatusCode
UA_SecureChannel_generateLocalKeys(UA_SecureChannel *channel);

UA_StatusCode
generateRemoteKeys(UA_SecureChannel *channel);

/* OPC UA Part 6 v1.05.07 (secureChannelEnhancements): build the
 * channel-bound CreateSession ServerSignature data
 *   channelThumbprint | clientNonce | H(serverChannelCert) |
 *   H(clientChannelCert) | serverNonce
 * Both the server (sign) and client (verify) call this with the same
 * logical values. `out` is allocated by the callee. */
UA_StatusCode
UA_SecureChannel_buildCreateSessionSignatureData(
    const UA_SecureChannel *channel, const UA_ByteString *clientNonce,
    const UA_ByteString *serverNonce, const UA_ByteString *serverChannelCert,
    const UA_ByteString *clientChannelCert, UA_ByteString *out);

/* OPC UA Part 6 v1.05.07: build the channel-bound ActivateSession
 * ClientSignature data
 *   channelThumbprint | serverNonce | H(serverAppCert) |
 *   H(serverChannelCert) | H(clientChannelCert) | clientNonce
 * `out` is allocated by the callee. */
UA_StatusCode
UA_SecureChannel_buildActivateSessionSignatureData(
    const UA_SecureChannel *channel, const UA_ByteString *serverNonce,
    const UA_ByteString *clientNonce, const UA_ByteString *serverAppCert,
    const UA_ByteString *serverChannelCert, const UA_ByteString *clientChannelCert,
    UA_ByteString *out);

/* OPC UA Part 6 v1.05.07: build the channel-bound ActivateSession
 * user-token (X.509) signature data
 *   channelThumbprint | serverNonce | H(serverAppCert) | H(serverChannelCert) |
 *   H(clientAppCert) | H(clientChannelCert) | clientNonce
 * This is the ClientSignature layout with an extra H(clientAppCert) - the
 * CLIENT's APPLICATION instance certificate - inserted before
 * H(clientChannelCert). NOTE: the user/X509-token certificate does NOT appear
 * in the signed data (it is conveyed and validated separately); the signature
 * only proves possession of the user key over the channel-bound data. */
UA_StatusCode
UA_SecureChannel_buildUserTokenSignatureData(
    const UA_SecureChannel *channel, const UA_ByteString *serverNonce,
    const UA_ByteString *clientNonce, const UA_ByteString *serverAppCert,
    const UA_ByteString *serverChannelCert, const UA_ByteString *clientAppCert,
    const UA_ByteString *clientChannelCert, UA_ByteString *out);

/**
 * Sending Messages
 * ---------------- */

/* When a fatal error occurs the Server shall send an Error Message to the
 * Client and close the socket. When a Client encounters one of these errors, it
 * shall also close the socket but does not send an Error Message. After the
 * socket is closed a Client shall try to reconnect automatically using the
 * mechanisms described in [...]. */
void
UA_SecureChannel_sendERR(UA_SecureChannel *channel, UA_TcpErrorMessage *error);

UA_StatusCode
UA_SecureChannel_sendOPN(UA_SecureChannel *channel, UA_UInt32 requestId,
                         const void *content, const UA_DataType *contentType);

UA_StatusCode
UA_SecureChannel_sendMSG(UA_SecureChannel *channel, UA_UInt32 requestId,
                         void *payload, const UA_DataType *payloadType);

UA_StatusCode
UA_SecureChannel_sendCLO(UA_SecureChannel *channel, UA_UInt32 requestId,
                         UA_CloseSecureChannelRequest *req);

/* The MessageContext is forwarded into the encoding layer so that we can send
 * chunks before continuing to encode. This lets us reuse a fixed chunk-sized
 * messages buffer. */
typedef struct {
    UA_SecureChannel *channel;
    UA_UInt32 requestId;
    UA_UInt32 messageType;

    UA_UInt16 chunksSoFar;
    size_t messageSizeSoFar;

    UA_ByteString messageBuffer;
    UA_Byte *buf_pos;
    const UA_Byte *buf_end;

    UA_Boolean final;
} UA_MessageContext;

/* Start the context of a new symmetric message. */
UA_StatusCode
UA_MessageContext_begin(UA_MessageContext *mc, UA_SecureChannel *channel,
                        UA_UInt32 requestId, UA_MessageType messageType);

/* Encode the content and send out full chunks. If the return code is good, then
 * the ChunkInfo contains encoded content that has not been sent. If the return
 * code is bad, then the ChunkInfo has been cleaned up internally. */
UA_StatusCode
UA_MessageContext_encode(UA_MessageContext *mc, const void *content,
                         const UA_DataType *contentType);

/* Sends a symmetric message already encoded in the context. The context is
 * cleaned up, also in case of errors. */
UA_StatusCode
UA_MessageContext_finish(UA_MessageContext *mc);

/* To be used when a failure occures when a MessageContext is open. Note that
 * the _encode and _finish methods will clean up internally. _abort can be run
 * on a MessageContext that has already been cleaned up before. */
void
UA_MessageContext_abort(UA_MessageContext *mc);

/**
 * Receive Message
 * --------------- */

/* Process a received buffer. This always has these three steps:
 *
 * 1. loadBuffer: The chunks in the SecureChannel are cut into chunks.
 *    The chunks can still point to the buffer.
 * 2. getCompleteMessage: Assemble chunks into a complete message. This is
 *    repeated until an error occours or an empty message is returned.
 * 3. persistBuffer: Make a copy of the remaining unpprocessed bytestring. So
 *    that the NetworkManager can reuse or free the packet memory.
 *
 * Note that only MSG and CLO messages are decrypted. HEL/ACK/OPN/... are
 * forwarded verbatim to the application. */
UA_StatusCode
UA_SecureChannel_loadBuffer(UA_SecureChannel *channel, const UA_ByteString buffer);

UA_StatusCode
UA_SecureChannel_getCompleteMessage(UA_SecureChannel *channel,
                                    UA_MessageType *messageType, UA_UInt32 *requestId,
                                    UA_ByteString *payload, UA_Boolean *copied,
                                    UA_DateTime nowMonotonic);

UA_StatusCode
UA_SecureChannel_persistBuffer(UA_SecureChannel *channel);

/* Internal methods in ua_securechannel_crypto.h */

void
hideBytesAsym(const UA_SecureChannel *channel, UA_Byte **buf_start,
              const UA_Byte **buf_end);

/* Decrypt and verify via the signature. The chunk buffer is reused to hold the
 * decrypted data after the MessageHeader and SecurityHeader. The chunk length
 * is reduced by the signature, padding and encryption overhead.
 *
 * The offset argument points to the start of the encrypted content (beginning
 * with the SequenceHeader).*/
UA_StatusCode
decryptAndVerifyChunk(UA_SecureChannel *channel,
                      const UA_SecurityPolicySignatureAlgorithm *signatureAlgorithm,
                      const UA_SecurityPolicyEncryptionAlgorithm *encryptionAlgorithm,
                      UA_MessageType messageType, UA_ByteString *chunk, size_t offset);

size_t
calculateAsymAlgSecurityHeaderLength(const UA_SecureChannel *channel);

UA_StatusCode
prependHeadersAsym(UA_SecureChannel *const channel, UA_Byte *header_pos,
                   const UA_Byte *buf_end, size_t totalLength,
                   size_t securityHeaderLength, UA_UInt32 requestId,
                   size_t *const finalLength);

/* Advance and return the next SequenceNumber to put into an outgoing chunk
 * (asymmetric OPN and symmetric MSG/CLO alike). Part 6 ยง6.7.2.4: the legacy
 * policies start at 1 and roll over to 1; the v1.05.07 enhanced/ECC policies
 * (LegacySequenceNumbers = false) start at 0 and roll over to 0. */
UA_UInt32
UA_SecureChannel_nextSequenceNumber(UA_SecureChannel *channel);

void
setBufPos(UA_MessageContext *mc);

UA_StatusCode
checkSymHeader(UA_SecureChannel *channel, const UA_UInt32 tokenId,
               UA_DateTime nowMonotonic);

UA_StatusCode
checkAsymHeader(UA_SecureChannel *channel,
                const UA_AsymmetricAlgorithmSecurityHeader *asymHeader);

void
padChunk(UA_SecureChannel *channel,
         const UA_SecurityPolicySignatureAlgorithm *signatureAlgorithm,
         const UA_SecurityPolicyEncryptionAlgorithm *encryptionAlgorithm,
         const UA_Byte *start, UA_Byte **pos);

UA_StatusCode
signAndEncryptAsym(UA_SecureChannel *channel, size_t preSignLength,
                   UA_ByteString *buf, size_t securityHeaderLength,
                   size_t totalLength);

UA_StatusCode
signAndEncryptSym(UA_MessageContext *messageContext,
                  size_t preSigLength, size_t totalLength);

/**
 * Log Helper
 * ----------
 * C99 requires at least one element for the variadic argument. If the log
 * statement has no variable arguments, supply an additional NULL. It will be
 * ignored by printf.
 *
 * We have to jump through some hoops to enable the use of format strings
 * without arguments since (pedantic) C99 does not allow variadic macros with
 * zero arguments. So we add a dummy argument that is not printed (%.0s is
 * string of length zero). */

#define UA_LOG_CHANNEL_INTERNAL(LOGGER, LEVEL, CHANNEL, MSG, ...)       \
    do {                                                                \
    if(UA_LOGLEVEL <= UA_LOGLEVEL_##LEVEL) {                            \
        UA_LOG_##LEVEL(LOGGER, UA_LOGCATEGORY_SECURECHANNEL,            \
                       "TCP %lu\t| SC %" PRIu32 "\t| " MSG "%.0s", \
                       (long unsigned)(CHANNEL)->connectionId,          \
                       (CHANNEL)->securityToken.channelId, __VA_ARGS__); \
    } \
    } while (0)

#define UA_LOG_TRACE_CHANNEL(LOGGER, CHANNEL, ...)                      \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, TRACE, CHANNEL, __VA_ARGS__, ""))
#define UA_LOG_DEBUG_CHANNEL(LOGGER, CHANNEL, ...)                      \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, DEBUG, CHANNEL, __VA_ARGS__, ""))
#define UA_LOG_INFO_CHANNEL(LOGGER, CHANNEL, ...)                       \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, INFO, CHANNEL, __VA_ARGS__, ""))
#define UA_LOG_WARNING_CHANNEL(LOGGER, CHANNEL, ...)                    \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, WARNING, CHANNEL, __VA_ARGS__, ""))
#define UA_LOG_ERROR_CHANNEL(LOGGER, CHANNEL, ...)                      \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, ERROR, CHANNEL, __VA_ARGS__, ""))
#define UA_LOG_FATAL_CHANNEL(LOGGER, CHANNEL, ...)                      \
    UA_MACRO_EXPAND(UA_LOG_CHANNEL_INTERNAL(LOGGER, FATAL, CHANNEL, __VA_ARGS__, ""))

_UA_END_DECLS

#endif /* UA_SECURECHANNEL_H_ */