diff --git a/examples/coap-server.c b/examples/coap-server.c index b4243e43d8..f3fe02bea9 100644 --- a/examples/coap-server.c +++ b/examples/coap-server.c @@ -162,6 +162,12 @@ static coap_upa_abbrev_t abbrev_mappings[] = { static coap_dtls_pki_t *setup_pki(coap_context_t *ctx, coap_dtls_role_t role, char *sni); +static uint8_t *read_file_mem(const char *file, size_t *length); + +static char *oscore_make_credential_file_name(const coap_bin_const_t *rcpkey_id, + const coap_bin_const_t *ctxkey_id, + int seq_file); + typedef struct psk_sni_def_t { char *sni_match; coap_bin_const_t *new_key; @@ -310,6 +316,176 @@ hnd_get_index(coap_resource_t *resource, (const uint8_t *)INDEX, NULL, NULL); } +/** Store an active echo challange, only one available at a time */ +static struct { + uint8_t recipient_id[10]; + uint8_t recipient_id_len; + uint8_t context_id[10]; + uint8_t context_id_len; + uint8_t echo_value[8]; +} active_echo_value; + +/* compare recipient_id and ctxkey_id (optional) with the active active_echo_value */ +static int +coap_oscore_active_echo_match(const coap_bin_const_t *recipient_id, + const coap_bin_const_t *ctxkey_id) { + if (recipient_id == NULL) + return 0; + + /* validate recipient id */ + if (active_echo_value.recipient_id_len != recipient_id->length || + memcmp(active_echo_value.recipient_id, recipient_id->s, recipient_id->length) != 0) { + return 0; + } + + if (ctxkey_id == NULL && active_echo_value.context_id_len == 0) + return 1; + + if (ctxkey_id == NULL || ctxkey_id->s == NULL || ctxkey_id->length == 0) + return active_echo_value.context_id_len == 0; + + return active_echo_value.context_id_len == ctxkey_id->length && + memcmp(active_echo_value.context_id, ctxkey_id->s, ctxkey_id->length) == 0; +} + +static int +update_seq_num_handler(const coap_session_t *session, + const coap_bin_const_t *rcpkey_id, + const coap_bin_const_t *ctxkey_id, + uint64_t sender_seq_num, + uint64_t seq_num_window) { + FILE *fp; + char *seq_file = oscore_make_credential_file_name(rcpkey_id, ctxkey_id, 1); + int ret; + + (void)session; + + if (seq_file == NULL) + return 0; + + fp = fopen(seq_file, "w"); + if (fp == NULL) { + coap_free_type(COAP_STRING, seq_file); + return 0; + } + + ret = fprintf(fp, "%" PRIu64 " %" PRIu64 "\n", sender_seq_num, seq_num_window); + fclose(fp); + coap_free_type(COAP_STRING, seq_file); + return ret > 0; +} + +static int +update_echo_handler(const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id, + const uint8_t echo_value[8]) { + (void)session; + + if (echo_value == NULL) { + /* clear the active echo value */ + active_echo_value.recipient_id_len = 0; + active_echo_value.context_id_len = 0; + memset(active_echo_value.echo_value, 0, sizeof(active_echo_value.echo_value)); + return 1; + } + + active_echo_value.context_id_len = ctxkey_id.length ? (uint8_t)min(ctxkey_id.length, + sizeof(active_echo_value.context_id)) : 0; + if (ctxkey_id.length) { + memcpy(active_echo_value.context_id, ctxkey_id.s, active_echo_value.context_id_len); + } + + active_echo_value.recipient_id_len = (uint8_t)rcpkey_id.length; + memcpy(active_echo_value.recipient_id, rcpkey_id.s, active_echo_value.recipient_id_len); + memcpy(active_echo_value.echo_value, echo_value, sizeof(active_echo_value.echo_value)); + return 1; +} + +static int +load_oscore_storage_seq_file(const coap_bin_const_t *rcpkey_id, + const coap_bin_const_t *ctxkey_id, + uint64_t *sender_seq_num, + uint64_t *seq_num_window) { + char *seq_file = oscore_make_credential_file_name(rcpkey_id, ctxkey_id, 1); + FILE *fp; + int ret; + + if (seq_file == NULL) + return 0; + + fp = fopen(seq_file, "r"); + if (fp == NULL) { + coap_free_type(COAP_STRING, seq_file); + return 0; + } + + ret = fscanf(fp, "%" SCNu64 " %" SCNu64 "\n", sender_seq_num, + seq_num_window); + fclose(fp); + coap_free_type(COAP_STRING, seq_file); + return ret == 2; +} + +/** + * Load the config from file system if file exists. + * Demonstrates external credential storage, this implementation is + * only for demonstration and not meant for production use. + * The approach is not efficient. + */ +static coap_oscore_conf_t * +coap_oscore_ctx_find(const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id) { + size_t length; + uint8_t *buf = NULL; + char *cred_file; + coap_oscore_conf_t *t_oscore_conf = NULL; + uint64_t last_seq = 0; + uint64_t seq_window = 0; + int has_seq; + int has_echo; + coap_str_const_t built_conf; + + (void)session; + + cred_file = oscore_make_credential_file_name(&rcpkey_id, &ctxkey_id, 0); + + buf = read_file_mem(cred_file, &length); + if (buf == NULL) { + coap_log_info("OSCORE Credentials file '%s' not found\n", cred_file); + goto exit; + } + + /* Load persisted sequence state before building the config buffer. */ + has_seq = load_oscore_storage_seq_file(&rcpkey_id, &ctxkey_id, + &last_seq, &seq_window); + has_echo = coap_oscore_active_echo_match(&rcpkey_id, &ctxkey_id); + + built_conf.length = length; + built_conf.s = buf; + t_oscore_conf = coap_new_oscore_conf(built_conf, NULL, NULL, 0); + if (t_oscore_conf == NULL) { + coap_log_info("Failed to load credentials file %s\n", cred_file); + goto exit; + } + + if (has_seq) { + coap_oscore_recipient_set_latest_seq(t_oscore_conf, &rcpkey_id, last_seq, seq_window); + } + if (has_echo) { + coap_oscore_recipient_set_echo(t_oscore_conf, &rcpkey_id, + active_echo_value.echo_value); + } + + coap_log_debug("Used credentials file %s\n", cred_file); + +exit: + coap_free_type(COAP_STRING, cred_file); + coap_free(buf); + return t_oscore_conf; +} + static void hnd_get_fetch_time(coap_resource_t *resource, coap_session_t *session, @@ -1681,7 +1857,7 @@ usage(const char *program, const char *version) { "\t\t[-x] [-y rec_secs] [-z scheme://addr[:port][/resource[?query]]]\n" "\t\t[-A address] [-B resource[:check]] [-E oscore_conf_file[,seq_file]]\n" "\t\t[-G group_if] [-I rate_limit_ppm] [-K interval] [-L value] [-N]\n" - "\t\t[-P scheme://address[:port],[name1[,name2..]]]\n" + "\t\t[-O oscore_cred_dir] [-P scheme://address[:port],[name1[,name2..]]]\n" "\t\t[-T max_token_size] [-U type] [-V num] [-X size] [-Z fail] [-3]\n" "\t\t[[-h hint] [-i match_identity_file] [-k key]\n" "\t\t[-s match_psk_sni_file] [-u user] [-2]]\n" @@ -1689,6 +1865,8 @@ usage(const char *program, const char *version) { "\t\t[-J pkcs11_pin] [-M rpk_file] [-R trust_casfile]\n" "\t\t[-S match_pki_sni_file] [-Y]]\n" "General Options\n" + , program); + fprintf(stderr, "\t-a priority\tSend logging output to syslog at priority (0-7) level\n" "\t-b max_block_size\n" "\t \t\tMaximum block size server supports (16, 32, 64,\n" @@ -1742,8 +1920,7 @@ usage(const char *program, const char *version) { "\t \t\tScheme is one of coap, coaps, coap+tcp and coaps+tcp.\n" "\t \t\tIf resource and query (or queries) are defined, then this\n" "\t \t\tis sent to the call-home client using a PUT request\n" - "\t-A address\tInterface address to bind to\n" - , program); + "\t-A address\tInterface address to bind to\n"); fprintf(stderr, "\t \t\tExamples:\n" #if COAP_IPV4_SUPPORT @@ -1775,6 +1952,8 @@ usage(const char *program, const char *version) { "\t \t\tcoap-oscore-conf(5) for definitions.\n" "\t \t\tOptional seq_file is used to save the current transmit\n" "\t \t\tsequence number, so on restart sequence numbers continue\n" + ); + fprintf(stderr, "\t-G group_if\tUse this interface for listening for the multicast\n" "\t \t\tgroup. This can be different from the implied interface\n" "\t \t\tif the -A option is used\n" @@ -1790,6 +1969,20 @@ usage(const char *program, const char *version) { "\t-N \t\tMake \"observe\" responses NON-confirmable. Even if set\n" "\t \t\tevery fifth response will still be sent as a confirmable\n" "\t \t\tresponse (RFC 7641 requirement)\n" + "\t-O oscore_cred_dir\n" + "\t \t\tDirectory containing OSCORE credential files to be\n" + "\t \t\tdynamically loaded named as _.txt where\n" + "\t \t\t and are hex-encoded byte strings, but can\n" + "\t \t\tbe 0 length. and are determined from the\n" + "\t \t\trequesting CoAP OSCORE option and map to recipient_id and\n" + "\t \t\tid_context respectively.\n" + "\t \t\tThese files use coap-oscore-conf(5) definitions.\n" + "\t \t\tDemonstrates the use of an external OSCORE credential store.\n" + "\t \t\tA __seq.txt file contains the recipient\n" + "\t \t\tsequence counter and window. To enforce a new echo challenge\n" + "\t \t\tthe __seq.txt file can be deleted, to remove\n" + "\t \t\tthe credentials, the _.txt file can be\n" + "\t \t\tdeleted. Can be combined with -E for a default configuration\n" "\t-P scheme://address[:port] | allow-mcast,[name1[,name2[,name3..]]]\n" "\t \t\tScheme, address, optional port of how to connect to the\n" "\t \t\tnext proxy server and zero or more names (comma\n" @@ -2218,6 +2411,7 @@ cmdline_read_user(char *arg, unsigned char **buf, size_t maxlen) { static FILE *oscore_seq_num_fp = NULL; static const char *oscore_conf_file = NULL; static const char *oscore_seq_save_file = NULL; +static const char *oscore_cred_dir = NULL; static int oscore_save_seq_num(uint64_t sender_seq_num, void *param COAP_UNUSED) { @@ -2229,6 +2423,43 @@ oscore_save_seq_num(uint64_t sender_seq_num, void *param COAP_UNUSED) { return 1; } + +/* + * Build "${oscore_cred_dir}/_[_seq].txt". + * Caller must free with coap_free(). + */ +static char * +oscore_make_credential_file_name(const coap_bin_const_t *rcpkey_id, + const coap_bin_const_t *ctxkey_id, int seq_file) { + size_t rcp_len = rcpkey_id ? rcpkey_id->length : 0; + size_t ctx_len = ctxkey_id ? ctxkey_id->length : 0; + size_t dir_len = oscore_cred_dir ? strlen(oscore_cred_dir) + 1 : 0; /* "dir/" */ + size_t buf_len = dir_len + (2 * ctx_len) + 1 + (2 * rcp_len) + + 9; /* hex + "_" + hex + "[_seq].txt\0" */ + char *result = coap_malloc_type(COAP_STRING, buf_len); + char *p = result; + char *end = result + buf_len; + + if (!result) + return NULL; + + if (oscore_cred_dir && strlen(oscore_cred_dir)) { + p += snprintf(p, end - p, "%s", oscore_cred_dir); + if (oscore_cred_dir[strlen(oscore_cred_dir)-1] != '/') { + p += snprintf(p, end - p, "%s", "/"); + } + } + for (size_t i = 0; i < rcp_len; i++) + p += snprintf(p, end - p, "%02x", rcpkey_id->s[i]); + *p++ = '_'; + for (size_t i = 0; i < ctx_len; i++) + p += snprintf(p, end - p, "%02x", ctxkey_id->s[i]); + if (seq_file) + p += snprintf(p, end - p, "_seq"); + snprintf(p, end - p, ".txt"); + return result; +} + static coap_oscore_conf_t * get_oscore_conf(coap_context_t *context) { uint8_t *buf; @@ -2678,7 +2909,7 @@ main(int argc, char **argv) { clock_offset = time(NULL); while ((opt = getopt(argc, argv, - "a:b:c:d:ef:g:h:i:j:k:l:mnop:q:rs:tu:v:w:xy:z:A:B:C:E:G:I:J:K:L:M:NP:R:S:T:U:V:X:YZ:23")) != -1) { + "a:b:c:d:ef:g:h:i:j:k:l:mnop:q:rs:tu:v:w:xy:z:A:B:C:E:G:I:J:K:L:M:NP:O:R:S:T:U:V:X:YZ:23")) != -1) { switch (opt) { #ifndef _WIN32 case 'a': @@ -2795,6 +3026,13 @@ main(int argc, char **argv) { case 'N': resource_flags = COAP_RESOURCE_FLAGS_NOTIFY_NON; break; + case 'O': + if (!coap_oscore_is_supported()) { + fprintf(stderr, "OSCORE support not enabled\n"); + goto failed; + } + oscore_cred_dir = optarg; + break; case 'o': shutdown_no_observe = 1; break; @@ -2966,6 +3204,13 @@ main(int argc, char **argv) { if (get_oscore_conf(ctx) == NULL) goto failed; } + if (oscore_cred_dir) { + /* register example local OSCORE credential storage */ + coap_oscore_register_external_handlers(ctx, + coap_oscore_ctx_find, + update_seq_num_handler, + update_echo_handler); + } #if COAP_PROXY_SUPPORT if (reverse_proxy.entry_count) { proxy_dtls_setup(ctx, &reverse_proxy); diff --git a/include/coap3/coap_net_internal.h b/include/coap3/coap_net_internal.h index 575b709fc1..d7f63715c5 100644 --- a/include/coap3/coap_net_internal.h +++ b/include/coap3/coap_net_internal.h @@ -20,6 +20,7 @@ #include "coap_subscribe.h" #include "coap_resource.h" +#include "coap_oscore.h" #ifdef __cplusplus extern "C" { @@ -102,6 +103,11 @@ struct coap_context_t { #endif /* RIOT_VERSION */ #if COAP_OSCORE_SUPPORT struct oscore_ctx_t *p_osc_ctx; /**< primary oscore context */ + coap_oscore_find_handler_t oscore_find_cb; /**< Optional override for oscore_find_context() */ + coap_oscore_update_echo_handler_t + oscore_update_echo_cb; /**< Optional function to call to update echo values */ + coap_oscore_update_seq_num_handler_t + oscore_update_seq_num_cb; /**< Optional function to call to update sequence number and window values */ #endif /* COAP_OSCORE_SUPPORT */ #if COAP_CLIENT_SUPPORT diff --git a/include/coap3/coap_oscore.h b/include/coap3/coap_oscore.h index a9ab442595..69e8311d40 100644 --- a/include/coap3/coap_oscore.h +++ b/include/coap3/coap_oscore.h @@ -32,6 +32,82 @@ extern "C" { * @{ */ +/** + * Callback function type for overriding oscore_find_context(). + * + * If set via coap_oscore_register_external_handlers(), this function is + * called before the internal oscore_find_context() to locate the OSCORE + * recipient and security context for an incoming request. + * + * The implementation of this function should be combined with + * @ref coap_oscore_update_seq_num_handler_t and @ref coap_oscore_update_echo_handler_t + * if echo challenge and sequence counter management is required. + * Otherwise, libcoap will lose those values and echo challenge + * and replay protection will not work properly. + * + * @param session The active CoAP session receiving the request from. + * @param rcpkey_id The Recipient Key ID (KID). + * @param ctxkey_id The ID Context to match. + * + * @return The OSCORE config retrieved from the custom OSCORE storage or NULL if not found. + * Will fallback to libcoap internal credential storage lookup. + */ +typedef coap_oscore_conf_t *(*coap_oscore_find_handler_t)( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id +); + +/** + * Callback function type for persisting the OSCORE receiver sequence number + * and anti-replay sliding window to an external storage. + * + * Called by the library whenever the Receiver Sequence Number or the + * anti-replay window is updated, giving the application the opportunity to + * store both values for external OSCORE credential management. Required + * to provide those values via the coap_oscore_find_handler_t callback, since + * the values will otherwise be lost. + * + * @param session The active CoAP session receiving the request from. + * @param rcpkey_id The Recipient ID for which the sequence number and window applies. + * @param ctxkey_id The ID Context for which the sequence number and window applies. + * @param receiver_seq_num The receiver sequence number. + * @param seq_num_window The 64-bit anti-replay sliding window bitmask. + * @return @c 1 if persisted successfully, else @c 0. + */ +typedef int (*coap_oscore_update_seq_num_handler_t)( + const coap_session_t *session, + const coap_bin_const_t *rcpkey_id, + const coap_bin_const_t *ctxkey_id, + uint64_t receiver_seq_num, + uint64_t seq_num_window +); + +/** + * Callback function type for persisting the OSCORE Echo challenge value in + * an external storage (write-only). + * + * Called by the library when the Echo option value is set or cleared. + * Pass a non-NULL @p echo_value to store a fresh 8-byte challenge, or NULL + * to clear any previously stored challenge for this recipient. + * Loading the challenge back at session-resume time is handled automatically + * via the @ref coap_oscore_find_handler_t callback, which should embed the + * stored value inside the @c complex_recipient block it returns. + * + * @param session The active CoAP session receiving the request from. + * @param rcpkey_id The Recipient ID for which the Echo value applies. + * @param ctxkey_id The ID Context for which the Echo value applies + * (or zero-length if not used). + * @param echo_value 8-byte Echo challenge to persist, or NULL to clear. + * @return @c 1 if the Echo value was successfully persisted/cleared, else @c 0. + */ +typedef int (*coap_oscore_update_echo_handler_t)( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id, + const uint8_t echo_value[8] +); + /** * Creates a new client session to the designated server, protecting the data * using OSCORE. @@ -238,6 +314,7 @@ COAP_API coap_session_t *coap_new_client_session_oscore_pki3(coap_context_t *ctx void *app_data, coap_app_data_free_callback_t callback, coap_str_const_t *ws_host); + /** * Set the context's default OSCORE configuration for a server. * @@ -288,17 +365,51 @@ coap_oscore_conf_t *coap_new_oscore_conf(coap_str_const_t conf_mem, * * @param oscore_conf The OSCORE configuration structure to release. * - * @return @c 1 Successfully removed, else @c 0 not found. + * @return @c 1 Successfully released, else @c 0 if not valid. */ int coap_delete_oscore_conf(coap_oscore_conf_t *oscore_conf); +/** + * Register external storage handlers for OSCORE session state. + * + * Allows providing a custom OSCORE credential storage for + * persistence and optimized for the needs of the application. + * Expands the built-in OSCORE context lookup and enables management + * of persistent OSCORE data (sequence numbers and Echo challenges) + * from within the application. + * + * @param context The CoAP context to configure. + * @param find_handler Inject a customized OSCORE config-lookup function + * to return temporary oscore credentials managed by + * an external credential store (see coap_oscore_find_handler_t), + * or @c NULL to only use the built-in oscore_find_context(). + * @param update_seq_num_handler Called whenever the Sender Sequence Number + * or anti-replay window changes. Use this + * to synchronize values with the external + * credential storage. @c NULL to disable. + * If find_handler is set, then it is recommended that + * update_seq_num_handler is set. + * @param update_echo_handler Called whenever the Echo challenge value is + * set or cleared. Use this to persist the + * value to non-volatile storage. @c NULL to + * disable. If find_handler is set, then it is recommended + * that update_echo_handler is set. + */ +COAP_API void coap_oscore_register_external_handlers( + coap_context_t *context, + coap_oscore_find_handler_t find_handler, + coap_oscore_update_seq_num_handler_t update_seq_num_handler, + coap_oscore_update_echo_handler_t update_echo_handler); + /** * Add in the specific Recipient ID into the OSCORE context (server only). * Note: This is only added to the OSCORE context as first defined by * coap_new_client_session_oscore*() or coap_context_oscore_server(). * - * @param context The CoAP context to add the OSCORE recipient_id to. - * @param recipient_id The Recipient ID to add. + * @param context The CoAP context to add the OSCORE recipient_id to. + * @param recipient_id The Recipient ID to add. Ownership of memory moves into the function + * and will be freed off when the context is freed or if the + * function fails. * * @return @c 1 Successfully added, else @c 0 there is an issue. */ @@ -307,11 +418,11 @@ COAP_API int coap_new_oscore_recipient(coap_context_t *context, /** * Release all the information associated for the specific Recipient ID - * (and hence and stop any further OSCORE protection for this Recipient). + * (and hence stop any further OSCORE protection for this Recipient). * Note: This is only removed from the OSCORE context as first defined by * coap_new_client_session_oscore*() or coap_context_oscore_server(). * - * @param context The CoAP context holding the OSCORE recipient_id to. + * @param context The CoAP context holding the OSCORE recipient_id to be removed. * @param recipient_id The Recipient ID to remove. * * @return @c 1 Successfully removed, else @c 0 not found. @@ -319,6 +430,37 @@ COAP_API int coap_new_oscore_recipient(coap_context_t *context, COAP_API int coap_delete_oscore_recipient(coap_context_t *context, coap_bin_const_t *recipient_id); +/** + * Set the latest sequence number and sliding window for the specified recipient + * id in the compiled configuration file. + * + * @param oscore_conf The compiled configuration file. + * @param recipient_id The Recipient ID to update in @p oscore_conf. + * @param last_seq The sequence number to update the recipient id with. + * @param seq_window The sliding window to update the recipient id with. + * + * @return @c 1 Successfully updated, else @c 0 recipient id not found. + */ +COAP_API int coap_oscore_recipient_set_latest_seq(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t last_seq, + uint64_t seq_window); + +/** + * Set the echo key value to use for the specified recipient id in the compiled + * configuration file. + * + * @param oscore_conf The compiled configuraiton file. + * @param recipient_id The Recipient ID to update. + * @param echo_value The 8 byte echo value to update the recipient id with. + * + * @return @c 1 Successfully updated, else @c 0 recipient id not found. + */ +COAP_API int coap_oscore_recipient_set_echo(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint8_t echo_value[8]); + + /** @} */ #ifdef __cplusplus diff --git a/include/coap3/coap_oscore_internal.h b/include/coap3/coap_oscore_internal.h index 9eff57081f..505f3107de 100644 --- a/include/coap3/coap_oscore_internal.h +++ b/include/coap3/coap_oscore_internal.h @@ -48,6 +48,13 @@ struct coap_oscore_rcp_conf_t { coap_bin_const_t *recipient_id; /**< Recipient ID (i.e. local our id) */ /* Silent Server */ int silent_server; /**< 1 if server is likely to be silent else 0 */ + + /* SSN handling for rfc8613 B.1.2 */ + uint8_t echo_value[8]; /** Inject an echo value to use for the oscore credentials */ + uint8_t window_initialized; /**< Contains if the sliding window is initialized + @c 1 if initialized, @c 0 otherwise */ + uint64_t last_seq; /**< Highest sequence number used for this recipient */ + uint64_t sliding_window; /**< bitfield sequence counter window */ }; /** @@ -145,6 +152,33 @@ struct coap_pdu_t *coap_oscore_decrypt_pdu(coap_session_t *session, */ void coap_delete_all_oscore(coap_context_t *context); +/** + * Attach the OSCORE recipient context information to the session. + * + * @param session The session to attach the recipient information to. + * @param recipient_ctx The recipient information to attach. + */ +void coap_oscore_session_set_recipient_ctx(coap_session_t *session, + oscore_recipient_ctx_t *recipient_ctx); + +/** + * Set the recipient context of an association. + * + * @param association The association to set @p recipient for. + * @param recipient_ctx For which the reference counter will be increased. + */ +void coap_oscore_association_set_recipient_ctx(oscore_association_t *association, + oscore_recipient_ctx_t *recipient_ctx); + +/** + * Verify if the OSCORE context is attached to the @p c_context . + * + * @param c_context The context to check for the OSCORE context. + * @param oscore_ctx The OSCORE context to check for. + * @return @c 1 if the OSCORE context is attached to the @p c_context , else @c 0. + */ +int coap_oscore_is_attached(coap_context_t *c_context, oscore_ctx_t *oscore_ctx); + /** * Cleanup all allocated OSCORE association information. * @@ -275,6 +309,17 @@ coap_session_t *coap_new_client_session_oscore3_lkd(coap_context_t *ctx, coap_app_data_free_callback_t callback, coap_str_const_t *ws_host); +/** + * Initializes an OSCORE context from the given configuration. + * + * @param oscore_conf The OSCORE configuration information to use to create + * the OSCORE context. + * Will alwatys be freed by this call. + * + * @return The created OSCORE context or NULL on failure. + */ +oscore_ctx_t *coap_init_oscore_context_from_conf(coap_oscore_conf_t *oscore_conf); + /** * Creates a new client session to the designated server, with PKI credentials * protecting the data using OSCORE, along with app_data information (as per @@ -378,6 +423,39 @@ coap_session_t *coap_new_client_session_oscore_psk3_lkd(coap_context_t *ctx, int coap_new_oscore_recipient_lkd(coap_context_t *context, coap_bin_const_t *recipient_id); +/** + * Set the latest sequence number and sliding window for the specified recipient + * id in the compiled configuration file. + * + * Note: This function must be called in the locked state. + * + * @param oscore_conf The compiled configuration file. + * @param recipient_id The Recipient ID to update in @p oscore_conf. + * @param last_seq The sequence number to update the recipient id with. + * @param seq_window The sliding window to update the recipient id with. + * + * @return @c 1 Successfully updated, else @c 0 recipient id not found. + */ +int coap_oscore_recipient_set_latest_seq_lkd(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t last_seq, uint64_t seq_window); + +/** + * Set the echo key value to use for the specified recipient id in the compiled + * configuration file. + * + * Note: This function must be called in the locked state. + * + * @param oscore_conf The compiled configuraiton file. + * @param recipient_id The Recipient ID to update. + * @param echo_value The 8 byte echo value to update the recipient id with. + * + * @return @c 1 Successfully updated, else @c 0 recipient id not found. + */ +int coap_oscore_recipient_set_echo_lkd(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint8_t echo_value[8]); + /** @} */ #ifdef __cplusplus diff --git a/include/oscore/oscore_context.h b/include/oscore/oscore_context.h index 9f3d69aaf7..524bcfb9ea 100644 --- a/include/oscore/oscore_context.h +++ b/include/oscore/oscore_context.h @@ -117,8 +117,9 @@ struct oscore_sender_ctx_t { }; struct oscore_recipient_ctx_t { - /* This field allows recipient chaining */ - oscore_recipient_ctx_t *next_recipient; + unsigned ref; /**< Reference counter to keep track of linked associations + / active sessions */ + oscore_recipient_ctx_t *next_recipient; /**< This field allows recipient chaining */ oscore_ctx_t *osc_ctx; /* RFC8613 3.1 */ coap_bin_const_t *recipient_id; @@ -174,6 +175,15 @@ struct oscore_association_t { oscore_ctx_t *oscore_derive_ctx(coap_context_t *c_context, coap_oscore_conf_t *oscore_conf); +/** + * oscore_derive_ctx_from_conf - derive a osc_ctx from oscore_conf information + * + * @param oscore_conf The OSCORE configuration to use. + * + * @return NULL if failure or derived OSCORE context. + */ +oscore_ctx_t *oscore_derive_ctx_from_conf(coap_oscore_conf_t *oscore_conf); + /** * oscore_duplicate_ctx - duplicate a osc_ctx * @@ -206,6 +216,16 @@ void oscore_free_contexts(coap_context_t *c_context); int oscore_remove_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx); +int oscore_add_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx); + +/** + * Check if oscore context is attached to a the provided context. + * + * @param osc_ctx The OSCORE context to check for being attached. + * @return @c 1 if @p osc_ctx is not attached to any context, @c 0 otherwise. + */ +int oscore_is_context_attached(const oscore_ctx_t *osc_ctx); + /** * oscore_add_recipient - add in recipient information * @@ -224,6 +244,22 @@ int oscore_delete_recipient(oscore_ctx_t *osc_ctx, coap_bin_const_t *rid); void oscore_free_sender(oscore_sender_ctx_t *snd_ctx); +/** + * Cleanup recipient context, including releasing the oscore context if the + * oscore context referenced is not attached to the coap_context_t. + * + * @param recipient_ctx The recipient context to cleanup. Will set the pointer + * to NULL after cleanup. + */ +void oscore_release_recipient_ctx(oscore_recipient_ctx_t **recipient_ctx); + +/** + * Increment the recipient context reference count. + * + * @param recipient_ctx The recipient context to increment the reference count. + */ +void oscore_reference_recipient_ctx(oscore_recipient_ctx_t *recipient_ctx); + uint8_t oscore_bytes_equal(uint8_t *a_ptr, uint8_t a_len, uint8_t *b_ptr, @@ -247,14 +283,14 @@ void oscore_log_char_value(coap_log_t level, const char *name, * oscore_find_context - Locate recipient context (and hence OSCORE context) * * @param session The CoAP session to search. - * @param rcpkey_id The Recipient kid. + * @param rcpkey_id The Recipient kid (can be 0 length). * @param ctxkey_id The ID Context to match (or NULL if no check). * @param oscore_r2 Partial id_context to match against or NULL. - * @param recipient_ctx The recipient context to update. + * @param recipient_ctx Updated with the found recipient context. * * return The OSCORE context and @p recipient_ctx updated, or NULL is error. */ -oscore_ctx_t *oscore_find_context(const coap_session_t *session, +oscore_ctx_t *oscore_find_context(coap_session_t *session, const coap_bin_const_t rcpkey_id, const coap_bin_const_t *ctxkey_id, uint8_t *oscore_r2, diff --git a/libcoap-3.map b/libcoap-3.map index 1646dee075..860dbf5c1b 100644 --- a/libcoap-3.map +++ b/libcoap-3.map @@ -208,6 +208,9 @@ global: coap_option_iterator_init; coap_option_next; coap_oscore_is_supported; + coap_oscore_recipient_set_echo; + coap_oscore_recipient_set_latest_seq; + coap_oscore_register_external_handlers; coap_package_build; coap_package_name; coap_package_version; diff --git a/libcoap-3.sym b/libcoap-3.sym index 7f71b7ae38..b667731de9 100644 --- a/libcoap-3.sym +++ b/libcoap-3.sym @@ -206,6 +206,9 @@ coap_option_filter_unset coap_option_iterator_init coap_option_next coap_oscore_is_supported +coap_oscore_recipient_set_echo +coap_oscore_recipient_set_latest_seq +coap_oscore_register_external_handlers coap_package_build coap_package_name coap_package_version diff --git a/man/Makefile.am b/man/Makefile.am index 8bd202b82a..9bc92ca00f 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -135,6 +135,10 @@ install-man: install-man3 install-man5 install-man7 @echo ".so man3/coap_context.3" > coap_context_set_session_reconnect_time2.3 @echo ".so man3/coap_context.3" > coap_context_rate_limit_ppm.3 @echo ".so man3/coap_context.3" > coap_register_option.3 + @echo ".so man3/coap_oscore.3" > coap_new_client_session_oscore3.3 + @echo ".so man3/coap_oscore.3" > coap_new_client_session_oscore_pki3.3 + @echo ".so man3/coap_oscore.3" > coap_new_client_session_oscore_psk3.3 + @echo ".so man3/coap_oscore.3" > coap_context_oscore_server.3 @echo ".so man3/coap_deprecated.3" > coap_new_client_session_oscore_psk.3 @echo ".so man3/coap_deprecated.3" > coap_new_client_session_psk.3 @echo ".so man3/coap_deprecated.3" > coap_new_client_session_psk2.3 diff --git a/man/coap-oscore-conf.txt.in b/man/coap-oscore-conf.txt.in index 44671be8a2..29894ed5ed 100644 --- a/man/coap-oscore-conf.txt.in +++ b/man/coap-oscore-conf.txt.in @@ -15,7 +15,7 @@ coap-oscore-conf DESCRIPTION ----------- -The OSCORE configuration file is read in when using the _*-E* oscore_conf_file_ +The OSCORE configuration file is read in when using the *-E oscore_conf_file* option for the *coap-client*(5) or *coap-server*(5) executables. This then allows a client or server to use OSCORE to protect the CoAP information between endpoints (https://rfc-editor.org/rfc/rfc8613[RFC8613]). @@ -53,6 +53,9 @@ The possible encodings are: *integer* :: The value is encoded as an integer number. +*unsigned64* :: + The value is encoded as an unsigned 64-bit integer number. + *text* :: The string value is mapped and then encoded as an integer number. This string can optionally be enclosed in _"_. A subset of the Names from @@ -189,6 +192,25 @@ OSCORE COMPLEX RECIPIENT (*bool*) (*Optional*) (Default is false) + The remote server normally does not send any response if set. +The following 3 entries are used for external credential stores to restore +replay-protection state across restarts as per RFC8613 Appenxix B. +Setting either *last_seq* or *sliding_window* marks the replay window as +initialized. + +*last_seq* :: + (*unsigned64*) (*Optional*) (Default is 0) + + The highest sequence number observed for this recipient. + +*sliding_window* :: + (*unsigned64*) (*Optional*) (Default is 0) + + The sliding replay-window bitmask for this recipient. + Used together with *last_seq* to restore the full replay-protection state. + +*echo_value* :: + (*hex*) (*Optional*) (No default) + + An 8-byte Echo challenge value for this recipient. + Exactly 8 bytes required. + GENERAL ------- diff --git a/man/coap-server.txt.in b/man/coap-server.txt.in index a4c023158a..4c5919578d 100644 --- a/man/coap-server.txt.in +++ b/man/coap-server.txt.in @@ -26,7 +26,8 @@ SYNOPSIS [*-y* rec_secs] [*-z* scheme://addr[:port][/resource[?query]]] [*-A* address] [*-B* resource[:check] [*-E* oscore_conf_file[,seq_file]] - [*-G* group_if] [*-I* rate_limit_ppm] [*-K* interval] [*-L* value] [*-N*] + [*-G* group_if] [*-I* rate_limit_ppm] [*-K* interval] [*-L* value] + [*-N*] [*-O* oscore_cred_dir] [*-P* scheme://addr[:port] | allow-mcast,[name1[,name2..]]] [*-T* max_token_size] [*-U* type] [*-V* num] [*-X* size] [*-Z* fail] [*-3*] @@ -175,6 +176,18 @@ OPTIONS - General fifth response will still be sent as a confirmable response (RFC 7641 requirement). +*-O* oscore_cred_dir:: + Directory containing OSCORE credential files to be dynamically loaded named + as _.txt where and are hex-encoded + byte strings, but can be 0 length. and are determined + from the requesting CoAP OSCORE option and map to recipient_id and + id_context respectively. These files use coap-oscore-conf(5) definitions. + Demonstrates the use of an external OSCORE credential store. + A __seq.txt file contains the recipient sequence counter + and window. To enforce a new echo challenge the __seq.txt + file can be deleted, to remove the credentials, the _.txt + file can be deleted. Can be combined with -E for a default configuration. + *-P* scheme://address[:port],[name1[,name2[,name3..]]] :: Scheme, address, optional port of how to connect to the next proxy server and zero or more names (comma separated) that this proxy server is known by. diff --git a/man/coap_oscore.txt.in b/man/coap_oscore.txt.in index d18b24234e..e606386324 100644 --- a/man/coap_oscore.txt.in +++ b/man/coap_oscore.txt.in @@ -13,8 +13,11 @@ NAME coap_oscore, coap_new_oscore_conf, coap_delete_oscore_conf, +coap_oscore_register_external_handlers, coap_new_oscore_recipient, coap_delete_oscore_recipient, +coap_oscore_recipient_set_echo, +coap_oscore_recipient_set_latest_seq, coap_new_client_session_oscore3, coap_new_client_session_oscore_pki3, coap_new_client_session_oscore_psk3, @@ -31,12 +34,24 @@ void *_save_seq_num_func_param_, uint64_t _start_seq_num_);* *int coap_delete_oscore_conf(coap_oscore_conf_t *_oscore_conf_);* +*void coap_oscore_register_external_handlers(coap_context_t *_context_, +coap_oscore_find_handler_t _find_handler_, +coap_oscore_update_seq_num_handler_t _update_seq_num_handler_, +coap_oscore_update_echo_handler_t _update_echo_handler_);* + *int coap_new_oscore_recipient(coap_context_t *_context_, coap_bin_const_t *_recipient_id_);* *int coap_delete_oscore_recipient(coap_context_t *_context_, coap_bin_const_t *_recipient_id_);* +*int coap_oscore_recipient_set_echo(coap_oscore_conf_t *_oscore_conf_, +const coap_bin_const_t *_recipient_id_, uint8_t _echo_value_[8]);* + +*int coap_oscore_recipient_set_latest_seq(coap_oscore_conf_t *_oscore_conf_, +const coap_bin_const_t *_recipient_id_, uint64_t _last_seq_, +uint64_t _seq_window_);* + *coap_session_t *coap_new_client_session_oscore3(coap_context_t *_context_, const coap_address_t *_local_if_, const coap_address_t *_server_, coap_proto_t _proto_, coap_oscore_conf_t *_oscore_conf_, void *_app_data_, @@ -76,6 +91,69 @@ manipulate information. CALLBACK HANDLER ---------------- +*Callback Type: coap_oscore_find_handler_t* + +The coap_oscore_find_handler_t method handler function prototype is defined as: +[source, c] +---- +/** + * Callback function type for overriding oscore_find_context(). + */ +typedef coap_oscore_conf_t *(*coap_oscore_find_handler_t)( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id +); +---- + +If this callback is registered with +*coap_oscore_register_external_handlers*(), libcoap calls it when +incoming OSCORE traffic needs a context lookup. The callback can return +temporary credentials sourced from an external store (for example a file or +database keyed by Recipient ID and Context ID). + +*Callback Type: coap_oscore_update_seq_num_handler_t* + +The coap_oscore_update_seq_num_handler_t method handler function prototype is +defined as: +[source, c] +---- +/** + * Callback function type for persisting OSCORE sequence and replay state. + */ +typedef int (*coap_oscore_update_seq_num_handler_t)( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id, + uint64_t receiver_seq_num, + uint64_t seq_num_window +); +---- + +This callback can be used to keep external storage synchronized whenever +sequence/replay state changes. + +*Callback Type: coap_oscore_update_echo_handler_t* + +The coap_oscore_update_echo_handler_t method handler function prototype is +defined as: +[source, c] +---- +/** + * Callback function type for persisting the OSCORE Echo challenge value. + */ +typedef int (*coap_oscore_update_echo_handler_t)( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id, + const uint8_t echo_value[8], + int store_value +); +---- + +This callback allows the application to save and restore Echo values in +external storage. + *Callback Type: coap_oscore_save_seq_num_t* The coap_oscore_save_seq_num_t method handler function prototype is defined as: @@ -140,6 +218,22 @@ The *coap_delete_oscore_recipient*() is used to remove the _recipient_id_ from the OSCORE information associated with _context_. OSCORE Traffic continuing to use _recipient_id_ will then fail. +*Function: coap_oscore_recipient_set_echo()* + +The *coap_oscore_recipient_set_echo*() is used to set the _echo_value_ to be +used for initial validation of the OSCORE session setup in the conmpiled +compiled configuration _oscore_conf_ for _recipient_id_. If not set, an +_echo_value_ will get randomly generated. + +*Function: coap_oscore_recipient_set_latest_seq()* + +The *coap_oscore_recipient_set_latest_seq*() is used to set the _last_seq_ +and _sliding_window_ to be used following an application restart for the +continuation of the same OSCORE session. _last_seq_ and _sliding_window_ +are set in the compiled _oscore_conf_ for _recipient_id_. +If not set, _last_seq_ and _sliding_window_ are 0, used for starting a new +OSCORE session. + *Function: coap_delete_oscore_conf()* The *coap_delete_oscore_conf*() function is used to free off the @@ -147,6 +241,18 @@ coap_oscore_conf_t structure _oscore_conf_ as returned by *coap_new_oscore_conf*(). Normally this function never needs to be called as _oscore_conf_ is freed off by the call the client or server setup functions. +*Function: coap_oscore_register_external_handlers()* + +The *coap_oscore_register_external_handlers*() function registers callbacks +for custom OSCORE credential/state storage. It can be used to: + +* override context lookup (_find_handler_) +* persist sequence/replay state (_update_seq_num_handler_) +* persist Echo state (_update_echo_handler_) + +When no handler is required, pass NULL for that callback. If _find_handler_ is +set, then _update_seq_num_handler_ and _update_echo_handler_ should also be defined. + *Function: coap_new_client_session_oscore3()* The *coap_new_client_session_oscore3*() is basically the same as @@ -239,6 +345,9 @@ or NULL on failure. *coap_new_oscore_recipient*() and *coap_delete_oscore_recipient*() return 0 on failure, 1 on success. +*coap_oscore_recipient_set_echo*() and *coap_oscore_recipient_set_latest_seq*() +return 1 on successful update, else 0 if _recipient_id_ was not found. + EXAMPLES -------- *Client Setup* diff --git a/man/examples-code-check.c b/man/examples-code-check.c index 40d873f1a3..ff590005a8 100644 --- a/man/examples-code-check.c +++ b/man/examples-code-check.c @@ -120,6 +120,8 @@ const char *pointer_list[] = { "coap_context_t ", "coap_fixed_point_t ", "coap_oscore_conf_t ", + "coap_oscore_rcp_conf_t ", + "coap_oscore_snd_conf_t ", "coap_optlist_t ", "coap_session_t ", "coap_string_t ", diff --git a/src/coap_net.c b/src/coap_net.c index 8396396aa7..ca9a7961ab 100644 --- a/src/coap_net.c +++ b/src/coap_net.c @@ -871,10 +871,6 @@ coap_free_context_lkd(coap_context_t *context) { coap_delete_all_async(context); #endif /* COAP_ASYNC_SUPPORT */ -#if COAP_OSCORE_SUPPORT - coap_delete_all_oscore(context); -#endif /* COAP_OSCORE_SUPPORT */ - #if COAP_SERVER_SUPPORT coap_cache_entry_t *cp, *ctmp; coap_endpoint_t *ep, *tmp; @@ -899,6 +895,10 @@ coap_free_context_lkd(coap_context_t *context) { } #endif /* COAP_CLIENT_SUPPORT */ +#if COAP_OSCORE_SUPPORT + coap_delete_all_oscore(context); +#endif /* COAP_OSCORE_SUPPORT */ + if (context->dtls_context) coap_dtls_free_context(context->dtls_context); #ifdef COAP_EPOLL_SUPPORT @@ -1010,7 +1010,8 @@ coap_option_check_critical(coap_session_t *session, case COAP_OPTION_OSCORE: /* Valid critical if doing OSCORE */ #if COAP_OSCORE_SUPPORT - if (ctx->p_osc_ctx) + /* Generally configured or has coap oscore enabled helper function */ + if (ctx->p_osc_ctx || ctx->oscore_find_cb) break; #endif /* COAP_OSCORE_SUPPORT */ /* Fall Through */ diff --git a/src/coap_oscore.c b/src/coap_oscore.c index 64eaf20d8f..c489d05c67 100644 --- a/src/coap_oscore.c +++ b/src/coap_oscore.c @@ -59,7 +59,7 @@ coap_oscore_initiate(coap_session_t *session, coap_oscore_conf_t *oscore_conf) { if (osc_ctx == NULL) { return 0; } - session->recipient_ctx = osc_ctx->recipient_chain; + coap_oscore_session_set_recipient_ctx(session, osc_ctx->recipient_chain); session->oscore_encryption = 1; } return 1; @@ -781,7 +781,7 @@ coap_oscore_new_pdu_encrypted_lkd(coap_session_t *session, coap_new_bin_const(cose->partial_iv.s, cose->partial_iv.length); if (association->partial_iv == NULL) goto error; - association->recipient_ctx = rcp_ctx; + coap_oscore_association_set_recipient_ctx(association, rcp_ctx); coap_delete_pdu_lkd(association->sent_pdu); if (session->b_2_step != COAP_OSCORE_B_2_NONE || association->just_set_up) { size_t size; @@ -877,6 +877,23 @@ build_and_send_error_pdu(coap_session_t *session, return; } +oscore_ctx_t * +coap_init_oscore_context_from_conf(coap_oscore_conf_t *oscore_conf) { + oscore_ctx_t *oscore_ctx = oscore_derive_ctx_from_conf(oscore_conf); + + if (oscore_ctx == NULL) { + goto error; + } + + /* As all is stored in osc_ctx, oscore_conf is no longer needed */ + coap_free_type(COAP_STRING, oscore_conf); + + return oscore_ctx; +error: + coap_delete_oscore_conf(oscore_conf); + return NULL; +} + /* pdu contains incoming message with encrypted COSE ciphertext payload * function returns decrypted message * and verifies signature, if present @@ -919,7 +936,7 @@ coap_oscore_decrypt_pdu(coap_session_t *session, if (opt == NULL) return NULL; - if (session->context->p_osc_ctx == NULL) { + if (session->context->p_osc_ctx == NULL && session->context->oscore_find_cb == NULL) { coap_log_warn("OSCORE: Not enabled\n"); if (!coap_request) coap_handle_event_lkd(session->context, @@ -1101,7 +1118,7 @@ coap_oscore_decrypt_pdu(coap_session_t *session, goto error_no_ack; } /* to be used for encryption of returned response later */ - session->recipient_ctx = rcp_ctx; + coap_oscore_session_set_recipient_ctx(session, rcp_ctx); snd_ctx = osc_ctx->sender_context; /* @@ -1127,6 +1144,13 @@ coap_oscore_decrypt_pdu(coap_session_t *session, incoming_seq = coap_decode_var_bytes8(cose->partial_iv.s, cose->partial_iv.length); rcp_ctx->last_seq = incoming_seq; + } else if (session->context->oscore_update_seq_num_cb != NULL) { + /* notify custom credential storage */ + coap_lock_callback(session->context->oscore_update_seq_num_cb(session, + rcp_ctx->recipient_id, + osc_ctx->id_context, + rcp_ctx->last_seq, + rcp_ctx->sliding_window)); } } else { /* !coap_request */ /* @@ -1284,7 +1308,7 @@ coap_oscore_decrypt_pdu(coap_session_t *session, association->aad = coap_new_bin_const(cose->aad.s, cose->aad.length); if (association->aad == NULL) goto error; - association->recipient_ctx = rcp_ctx; + coap_oscore_association_set_recipient_ctx(association, rcp_ctx); /* So association is not released when handling decrypt */ association = NULL; } else if (!oscore_new_association(session, @@ -1579,6 +1603,24 @@ coap_oscore_decrypt_pdu(coap_session_t *session, NULL, 0); goto error_no_ack; + } else { + if (session->context->oscore_update_seq_num_cb != NULL) { + /* notify echo challenge changed */ + coap_lock_callback(session->context->oscore_update_seq_num_cb(session, + rcp_ctx->recipient_id, + osc_ctx->id_context, + rcp_ctx->last_seq, + rcp_ctx->sliding_window)); + } + + /* reset echo challenge value to prevent duplicate reuse */ + if (session->context->oscore_update_echo_cb != NULL) { + coap_lock_callback(session->context->oscore_update_echo_cb(session, + cose->key_id, + cose->kid_context, + NULL + )); + } } } else goto error; @@ -1588,8 +1630,24 @@ coap_oscore_decrypt_pdu(coap_session_t *session, session->b_2_step = COAP_OSCORE_B_2_NONE; coap_log_oscore("Appendix B.2 server finished\n"); } - coap_prng_lkd(rcp_ctx->echo_value, sizeof(rcp_ctx->echo_value)); + + /* Use echo_value loaded from conf; generate fresh if unset */ + { + static const uint8_t zero[8] = {0}; + + if (memcmp(rcp_ctx->echo_value, zero, sizeof(zero)) == 0) + coap_prng_lkd(rcp_ctx->echo_value, sizeof(rcp_ctx->echo_value)); + } + if (session->context->oscore_update_echo_cb != NULL) { + coap_lock_callback(session->context->oscore_update_echo_cb( + session, + cose->key_id, + cose->kid_context, + rcp_ctx->echo_value + )); + } coap_log_oscore("Appendix B.1.2 server plain response\n"); + build_and_send_error_pdu(session, pdu, COAP_RESPONSE_CODE(401), @@ -1753,6 +1811,7 @@ typedef enum { COAP_ENC_INTEGER = 0x400, COAP_ENC_TEXT = 0x800, COAP_ENC_BOOL = 0x1000, + COAP_ENC_UNSIGNED64 = 0x2000, COAP_ENC_LAST } coap_oscore_coding_t; @@ -1768,6 +1827,7 @@ static struct coap_oscore_encoding_t { TEXT_MAPPING(hex, COAP_ENC_HEX), TEXT_MAPPING(config, COAP_ENC_CONFIG), TEXT_MAPPING(integer, COAP_ENC_INTEGER), + TEXT_MAPPING(unsigned64, COAP_ENC_UNSIGNED64), TEXT_MAPPING(text, COAP_ENC_TEXT), TEXT_MAPPING(bool, COAP_ENC_BOOL), {{0, NULL}, COAP_ENC_LAST} @@ -1778,6 +1838,7 @@ typedef struct { const char *encoding_name; union { int value_int; + uint64_t value_int64; coap_bin_const_t *value_bin; coap_str_const_t value_str; } u; @@ -1972,6 +2033,10 @@ get_split_entry(const char **start, case COAP_ENC_INTEGER: value->u.value_int = atoi(begin); break; + case COAP_ENC_UNSIGNED64: { + value->u.value_int64 = strtoull(begin, NULL, 10); + break; + } case COAP_ENC_TEXT: value->u.value_str.s = (const uint8_t *)begin; value->u.value_str.length = end - begin; @@ -2070,6 +2135,9 @@ static oscore_config_t oscore_snd_config[] = { static oscore_config_t oscore_rcp_config[] = { CONFIG_RCP_ENTRY(recipient_id, COAP_ENC_HEX | COAP_ENC_ASCII, NULL), CONFIG_RCP_ENTRY(silent_server, COAP_ENC_BOOL, NULL), + CONFIG_DUMMY_ENTRY(last_seq, COAP_ENC_UNSIGNED64, NULL), + CONFIG_DUMMY_ENTRY(sliding_window, COAP_ENC_UNSIGNED64, NULL), + CONFIG_DUMMY_ENTRY(echo_value, COAP_ENC_HEX, NULL), }; @@ -2185,6 +2253,7 @@ coap_parse_oscore_snd_conf_mem(coap_bin_const_t conf_mem) { } break; case COAP_ENC_CONFIG: + case COAP_ENC_UNSIGNED64: case COAP_ENC_LAST: default: assert(0); @@ -2242,56 +2311,74 @@ coap_parse_oscore_rcp_conf_mem(coap_bin_const_t conf_mem) { for (i = 0; i < sizeof(oscore_rcp_config) / sizeof(oscore_rcp_config[0]); i++) { if (coap_string_equal(&oscore_rcp_config[i].str_keyword, &keyword) != 0 && value.encoding & oscore_rcp_config[i].encoding) { - coap_bin_const_t *unused_check; - - switch (value.encoding) { - case COAP_ENC_HEX: - case COAP_ENC_ASCII: - memcpy(&unused_check, - &(((char *)rcp_conf)[oscore_rcp_config[i].offset]), - sizeof(unused_check)); - if (unused_check != NULL) { - coap_log_warn("oscore_rcp_conf: Keyword '%.*s' duplicated\n", - (int)keyword.length, - (const char *)keyword.s); + if (coap_string_equal(coap_make_str_const("last_seq"), &keyword)) { + rcp_conf->last_seq = value.u.value_int64; + rcp_conf->window_initialized = 1; + } else if (coap_string_equal(coap_make_str_const("sliding_window"), &keyword)) { + rcp_conf->sliding_window = value.u.value_int64; + rcp_conf->window_initialized = 1; + } else if (coap_string_equal(coap_make_str_const("echo_value"), &keyword)) { + if (value.u.value_bin->length != sizeof(rcp_conf->echo_value)) { + coap_log_warn("oscore_rcp_conf: echo_value must be 8 bytes\n"); coap_delete_bin_const(value.u.value_bin); goto error; } - memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), - &value.u.value_bin, - sizeof(value.u.value_bin)); - break; - case COAP_ENC_INTEGER: - case COAP_ENC_BOOL: - memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), - &value.u.value_int, - sizeof(value.u.value_int)); - break; - case COAP_ENC_TEXT: - for (j = 0; oscore_rcp_config[i].text_mapping[j].text.s != NULL; j++) { - if (memcmp(value.u.value_str.s, - oscore_rcp_config[i].text_mapping[j].text.s, - value.u.value_str.length) == 0) { - memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), - &oscore_rcp_config[i].text_mapping[j].value, - sizeof(oscore_rcp_config[i].text_mapping[j].value)); - break; + memcpy(rcp_conf->echo_value, value.u.value_bin->s, + sizeof(rcp_conf->echo_value)); + coap_delete_bin_const(value.u.value_bin); + } else { + coap_bin_const_t *unused_check; + + switch (value.encoding) { + case COAP_ENC_HEX: + case COAP_ENC_ASCII: + memcpy(&unused_check, + &(((char *)rcp_conf)[oscore_rcp_config[i].offset]), + sizeof(unused_check)); + if (unused_check != NULL) { + coap_log_warn("oscore_rcp_conf: Keyword '%.*s' duplicated\n", + (int)keyword.length, + (const char *)keyword.s); + coap_delete_bin_const(value.u.value_bin); + goto error; } + memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), + &value.u.value_bin, + sizeof(value.u.value_bin)); + break; + case COAP_ENC_INTEGER: + case COAP_ENC_BOOL: + memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), + &value.u.value_int, + sizeof(value.u.value_int)); + break; + case COAP_ENC_TEXT: + for (j = 0; oscore_rcp_config[i].text_mapping[j].text.s != NULL; j++) { + if (memcmp(value.u.value_str.s, + oscore_rcp_config[i].text_mapping[j].text.s, + value.u.value_str.length) == 0) { + memcpy(&(((char *)rcp_conf)[oscore_rcp_config[i].offset]), + &oscore_rcp_config[i].text_mapping[j].value, + sizeof(oscore_rcp_config[i].text_mapping[j].value)); + break; + } + } + if (oscore_rcp_config[i].text_mapping[j].text.s == NULL) { + coap_log_warn("oscore_rcp_conf: Keyword '%.*s': value '%.*s' unknown\n", + (int)keyword.length, + (const char *)keyword.s, + (int)value.u.value_str.length, + (const char *)value.u.value_str.s); + goto error; + } + break; + case COAP_ENC_CONFIG: + case COAP_ENC_UNSIGNED64: + case COAP_ENC_LAST: + default: + assert(0); + break; } - if (oscore_rcp_config[i].text_mapping[j].text.s == NULL) { - coap_log_warn("oscore_rcp_conf: Keyword '%.*s': value '%.*s' unknown\n", - (int)keyword.length, - (const char *)keyword.s, - (int)value.u.value_str.length, - (const char *)value.u.value_str.s); - goto error; - } - break; - case COAP_ENC_CONFIG: - case COAP_ENC_LAST: - default: - assert(0); - break; } break; } @@ -2469,6 +2556,7 @@ coap_parse_oscore_conf_mem(coap_str_const_t conf_mem) { goto error; } break; + case COAP_ENC_UNSIGNED64: case COAP_ENC_LAST: default: assert(0); @@ -2555,6 +2643,47 @@ coap_delete_oscore_associations(coap_session_t *session) { oscore_delete_server_associations(session); } +void +coap_oscore_session_set_recipient_ctx(coap_session_t *session, + oscore_recipient_ctx_t *recipient_ctx) { + oscore_recipient_ctx_t *old_ctx; + + if (session == NULL || recipient_ctx == NULL) + return; + + if (session->recipient_ctx == recipient_ctx) { + /* already attached */ + return; + } + + old_ctx = session->recipient_ctx; + + /* attach recipient context */ + session->recipient_ctx = recipient_ctx; + session->recipient_ctx->ref++; + + if (old_ctx != NULL) { + oscore_release_recipient_ctx(&old_ctx); + } +} + +void +coap_oscore_association_set_recipient_ctx(oscore_association_t *association, + oscore_recipient_ctx_t *recipient_ctx) { + if (association == NULL) + return; + + if (association->recipient_ctx == recipient_ctx) + return; + + if (association->recipient_ctx != NULL) { + association->recipient_ctx->ref--; + } + + association->recipient_ctx = recipient_ctx; + association->recipient_ctx->ref++; +} + coap_oscore_conf_t * coap_new_oscore_conf(coap_str_const_t conf_mem, coap_oscore_save_seq_num_t save_seq_num_func, @@ -2632,6 +2761,7 @@ int coap_new_oscore_recipient_lkd(coap_context_t *context, coap_bin_const_t *recipient_id) { coap_oscore_rcp_conf_t *rcp_conf; + oscore_recipient_ctx_t *rcp_ctx; coap_lock_check_locked(); @@ -2647,7 +2777,8 @@ coap_new_oscore_recipient_lkd(coap_context_t *context, memset(rcp_conf, 0, sizeof(coap_oscore_rcp_conf_t)); rcp_conf->recipient_id = recipient_id; /* rcp_conf is released in oscore_add_recipient() */ - if (oscore_add_recipient(context->p_osc_ctx, rcp_conf, 0) == NULL) + rcp_ctx = oscore_add_recipient(context->p_osc_ctx, rcp_conf, 0); + if (rcp_ctx == NULL) return 0; return 1; } @@ -2674,6 +2805,89 @@ coap_delete_oscore_recipient_lkd(coap_context_t *context, return oscore_delete_recipient(context->p_osc_ctx, recipient_id); } +void +coap_oscore_register_external_handlers(coap_context_t *context, + coap_oscore_find_handler_t find_handler, + coap_oscore_update_seq_num_handler_t update_seq_num_handler, + coap_oscore_update_echo_handler_t update_echo_handler) { + if (context == NULL) + return; + + coap_lock_lock(return); + context->oscore_find_cb = find_handler; + context->oscore_update_seq_num_cb = update_seq_num_handler; + context->oscore_update_echo_cb = update_echo_handler; + coap_lock_unlock(); +} + +static coap_oscore_rcp_conf_t * +coap_oscore_get_recipient_conf(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *rid) { + coap_oscore_rcp_conf_t *next = oscore_conf->recipient_chain; + + while (next) { + if (coap_binary_equal(next->recipient_id, rid)) { + return next; + } + next = next->next_recipient; + } + return NULL; +} + + +COAP_API int +coap_oscore_recipient_set_latest_seq(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t last_seq, uint64_t seq_window) { + int ret; + + coap_lock_lock(return 0); + ret = coap_oscore_recipient_set_latest_seq_lkd(oscore_conf, recipient_id, last_seq, + seq_window); + coap_lock_unlock(); + return ret; +} + +int +coap_oscore_recipient_set_latest_seq_lkd(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t last_seq, uint64_t seq_window) { + coap_oscore_rcp_conf_t *rcp_conf; + + rcp_conf = coap_oscore_get_recipient_conf(oscore_conf, recipient_id); + if (rcp_conf) { + rcp_conf->last_seq = last_seq; + rcp_conf->sliding_window = seq_window; + return 1; + } + return 0; +} + +COAP_API int +coap_oscore_recipient_set_echo(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint8_t echo_value[8]) { + int ret; + + coap_lock_lock(return 0); + ret = coap_oscore_recipient_set_echo_lkd(oscore_conf, recipient_id, echo_value); + coap_lock_unlock(); + return ret; +} + +COAP_API int +coap_oscore_recipient_set_echo_lkd(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint8_t echo_value[8]) { + coap_oscore_rcp_conf_t *rcp_conf; + + rcp_conf = coap_oscore_get_recipient_conf(oscore_conf, recipient_id); + if (rcp_conf) { + memcpy(rcp_conf->echo_value, echo_value, sizeof(rcp_conf->echo_value)); + } + return 0; +} + /** @} */ #else /* !COAP_OSCORE_SUPPORT */ @@ -2834,4 +3048,45 @@ coap_delete_oscore_recipient(coap_context_t *context, return 0; } +void +coap_oscore_register_external_handlers(coap_context_t *context, + coap_oscore_find_handler_t find_handler, + coap_oscore_update_seq_num_handler_t update_seq_num_handler, + coap_oscore_update_echo_handler_t update_echo_handler) { + (void)context; + (void)find_handler; + (void)update_seq_num_handler; + (void)update_echo_handler; +} + +COAP_API int +coap_oscore_recipient_set_last_seq(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t last_seq) { + (void)oscore_conf; + (void)recipient_id; + (void)last_seq; + return 0; +} + +COAP_API int +coap_oscore_recipient_set_seq_window(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint64_t seq_window) { + (void)oscore_conf; + (void)recipient_id; + (void)seq_window; + return 0; +} + +COAP_API int +coap_oscore_recipient_set_echo(coap_oscore_conf_t *oscore_conf, + const coap_bin_const_t *recipient_id, + uint8_t echo_value[8]) { + (void)oscore_conf; + (void)recipient_id; + (void)echo_value; + return 0; +} + #endif /* !COAP_OSCORE_SUPPORT */ diff --git a/src/coap_session.c b/src/coap_session.c index 579940f5da..44844df27f 100644 --- a/src/coap_session.c +++ b/src/coap_session.c @@ -622,6 +622,7 @@ coap_session_mfree(coap_session_t *session) { #endif /* COAP_SERVER_SUPPORT */ #if COAP_OSCORE_SUPPORT coap_delete_oscore_associations(session); + oscore_release_recipient_ctx(&session->recipient_ctx); #endif /* COAP_OSCORE_SUPPORT */ #if COAP_WS_SUPPORT coap_free_type(COAP_STRING, session->ws); @@ -1249,6 +1250,9 @@ coap_make_addr_hash(coap_addr_hash_t *addr_hash, coap_proto_t proto, addr_hash->proto = proto; } +/* Aids debugging when session is created */ +static coap_session_t *debug_session; + coap_session_t * coap_endpoint_get_session(coap_endpoint_t *endpoint, const coap_packet_t *packet, coap_tick_t now) { @@ -1426,6 +1430,8 @@ coap_endpoint_get_session(coap_endpoint_t *endpoint, &addr_hash, &packet->addr_info.local, &packet->addr_info.remote, packet->ifindex, endpoint->context, endpoint); + /* Aids debugging endpoint created session */ + debug_session = session; if (session) { session->last_rx_tx = now; memcpy(session->sock.lfunc, endpoint->sock.lfunc, diff --git a/src/oscore/oscore_context.c b/src/oscore/oscore_context.c index a02e925bde..f8eafa3cf9 100644 --- a/src/oscore/oscore_context.c +++ b/src/oscore/oscore_context.c @@ -52,6 +52,8 @@ /* Move ptr from b to a, and then clear b */ #define OSC_MOVE_PTR(a,b) do { (a) = (b); (b) = NULL; } while(0) +static int oscore_context_release_recipients(oscore_ctx_t *osc_ctx); + static size_t compose_info(uint8_t *buffer, size_t buf_size, @@ -128,28 +130,34 @@ oscore_enter_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx) { } static void -oscore_free_recipient(oscore_recipient_ctx_t *recipient) { - if (recipient == NULL) +oscore_free_recipient_ctx(oscore_recipient_ctx_t *rcp_ctx) { + if (rcp_ctx == NULL) + return; + + assert(rcp_ctx->ref > 0); + if (--rcp_ctx->ref > 0) { return; + } + /* remove recipient from oscore context chain if attached */ - if (recipient->osc_ctx) { - if (recipient->osc_ctx->recipient_chain == recipient) { - recipient->osc_ctx->recipient_chain = recipient->next_recipient; + if (rcp_ctx->osc_ctx) { + if (rcp_ctx->osc_ctx->recipient_chain == rcp_ctx) { + rcp_ctx->osc_ctx->recipient_chain = rcp_ctx->next_recipient; } else { - oscore_recipient_ctx_t *prev = recipient->osc_ctx->recipient_chain; + oscore_recipient_ctx_t *prev = rcp_ctx->osc_ctx->recipient_chain; - while (prev && prev->next_recipient != recipient) { + while (prev && prev->next_recipient != rcp_ctx) { prev = prev->next_recipient; } if (prev) { - prev->next_recipient = recipient->next_recipient; + prev->next_recipient = rcp_ctx->next_recipient; } } } - coap_delete_bin_const(recipient->recipient_key); - coap_delete_bin_const(recipient->recipient_id); - coap_free_type(COAP_OSCORE_REC, recipient); + coap_delete_bin_const(rcp_ctx->recipient_key); + coap_delete_bin_const(rcp_ctx->recipient_id); + coap_free_type(COAP_OSCORE_REC, rcp_ctx); } void @@ -172,7 +180,7 @@ oscore_free_context(oscore_ctx_t *osc_ctx) { while (osc_ctx->recipient_chain) { oscore_recipient_ctx_t *next = osc_ctx->recipient_chain->next_recipient; - oscore_free_recipient(osc_ctx->recipient_chain); + oscore_free_recipient_ctx(osc_ctx->recipient_chain); osc_ctx->recipient_chain = next; } @@ -200,10 +208,66 @@ oscore_free_contexts(coap_context_t *c_context) { } osc_ctx->next = NULL; - oscore_free_context(osc_ctx); + /* + * Verify if all recipients can be released, if not + * defer freeing of context until later when all recipients are released. + * The oscore context is flagged as ready to be freed by removing + * the oscore context from the coap context. + */ + if (oscore_context_release_recipients(osc_ctx) > 0) { + oscore_free_context(osc_ctx); + } } } +/** + * Function checks attached recipients, if any + * attached recipient has a reference counter higher then 1, + * it is referenced by another active session or association and can not be freed yet, + * otherwise will release the recipient. + * + * @param osc_ctx The OSCORE context to check attached recipients for. + * + * @return @c 1 if all recipients are released, + * @c 0 if some recipients are still referenced externally and can not + * be released yet, + */ +static int +oscore_context_release_recipients(oscore_ctx_t *osc_ctx) { + int ok = 1; + + oscore_recipient_ctx_t *prev = NULL; + oscore_recipient_ctx_t *next = osc_ctx->recipient_chain; + + while (next != NULL) { + /* if reference is 1 or lower, ready to be freed */ + if (next->ref <= 1) { + oscore_recipient_ctx_t *to_free = next; + next = next->next_recipient; + oscore_free_recipient_ctx(to_free); + continue; + } + + /* + * Recipient is still attached to an oscore association or session, + * can not be freed yet. Decrease ref counter and keep in chain. + */ + next->ref--; + ok = 0; + + /* keep recipient in chain, since still referenced by other + session or association */ + if (prev != NULL) { + prev->next_recipient = next; + } else { + osc_ctx->recipient_chain = next; + } + prev = next; + next = next->next_recipient; + } + return ok; +} + int oscore_remove_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx) { oscore_ctx_t *head = c_context->p_osc_ctx; @@ -228,6 +292,17 @@ oscore_remove_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx) { c_context->p_osc_ctx = next->next; } next->next = NULL; + /* + * Only free context if no recipient is still referenced by + * an association or session. Otherwise defer context to be + * freed after all references are resolved. + */ + if (oscore_context_release_recipients(osc_ctx) <= 0) { + return 2; + } + + /* all related recipient ctx attachments are released */ + osc_ctx->recipient_chain = NULL; oscore_free_context(osc_ctx); return 1; } @@ -243,11 +318,63 @@ oscore_remove_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx) { * Updates recipient_ctx. */ oscore_ctx_t * -oscore_find_context(const coap_session_t *session, +oscore_find_context(coap_session_t *session, const coap_bin_const_t rcpkey_id, const coap_bin_const_t *ctxkey_id, uint8_t *oscore_r2, oscore_recipient_ctx_t **recipient_ctx) { + if (session->context->oscore_find_cb != NULL && ctxkey_id != NULL) { + oscore_ctx_t *tmp_ctx = NULL; + coap_oscore_conf_t *oscore_conf; + + if (session->recipient_ctx) { + if (coap_binary_equal(session->recipient_ctx->recipient_id, &rcpkey_id)) { + *recipient_ctx = session->recipient_ctx; + return session->recipient_ctx->osc_ctx; + } + } + coap_lock_callback_ret(oscore_conf, session->context->oscore_find_cb(session, + rcpkey_id, + *ctxkey_id)); + + if (oscore_conf) { + tmp_ctx = coap_init_oscore_context_from_conf(oscore_conf); + } + + if (tmp_ctx != NULL) { + /* + * find matching recipient and remove others if multiple recipients + * have been provided. ensures unneeded memory is freed early + * to prevent memory leaks and unnecessary memory usage. + */ + if (tmp_ctx->recipient_chain != NULL && + tmp_ctx->recipient_chain->next_recipient != NULL) { + oscore_recipient_ctx_t *rcp_ctx = tmp_ctx->recipient_chain; + oscore_recipient_ctx_t *ref_ctx = NULL; + while (rcp_ctx != NULL) { + ref_ctx = rcp_ctx; + rcp_ctx = rcp_ctx->next_recipient; + if (coap_binary_equal(ref_ctx->recipient_id, &rcpkey_id)) { + ref_ctx->next_recipient = NULL; + *recipient_ctx = ref_ctx; + } else { + coap_delete_bin_const(ref_ctx->recipient_key); + coap_delete_bin_const(ref_ctx->recipient_id); + coap_free_type(COAP_OSCORE_REC, (void *)ref_ctx); + } + } + } else { + *recipient_ctx = tmp_ctx->recipient_chain; + } + tmp_ctx->recipient_chain = *recipient_ctx; + coap_oscore_session_set_recipient_ctx(session, *recipient_ctx); + /* Remove the tmp_ctx reference */ + if (*recipient_ctx) + (*recipient_ctx)->ref--; + return tmp_ctx; + } + } + oscore_ctx_t *pt = session->context->p_osc_ctx; *recipient_ctx = NULL; @@ -602,7 +729,7 @@ oscore_duplicate_ctx(coap_context_t *c_context, } oscore_ctx_t * -oscore_derive_ctx(coap_context_t *c_context, coap_oscore_conf_t *oscore_conf) { +oscore_derive_ctx_from_conf(coap_oscore_conf_t *oscore_conf) { oscore_ctx_t *osc_ctx = NULL; oscore_sender_ctx_t *sender_ctx = NULL; coap_oscore_rcp_conf_t *rcp_conf; @@ -702,16 +829,30 @@ oscore_derive_ctx(coap_context_t *c_context, coap_oscore_conf_t *oscore_conf) { goto error; oscore_log_context(osc_ctx, "Common context"); - - oscore_enter_context(c_context, osc_ctx); - return osc_ctx; - error: oscore_free_context(osc_ctx); return NULL; } +int +oscore_add_context(coap_context_t *c_context, oscore_ctx_t *osc_ctx) { + oscore_enter_context(c_context, osc_ctx); + return 1; +} + +int +oscore_is_context_attached(const oscore_ctx_t *osc_ctx) { + return osc_ctx->next != NULL; +} + +oscore_ctx_t * +oscore_derive_ctx(coap_context_t *c_context, coap_oscore_conf_t *oscore_conf) { + oscore_ctx_t *osc_ctx = oscore_derive_ctx_from_conf(oscore_conf); + oscore_enter_context(c_context, osc_ctx); + return osc_ctx; +} + oscore_recipient_ctx_t * oscore_add_recipient(oscore_ctx_t *osc_ctx, coap_oscore_rcp_conf_t *rcp_conf, uint32_t break_key) { @@ -762,6 +903,11 @@ oscore_add_recipient(oscore_ctx_t *osc_ctx, coap_oscore_rcp_conf_t *rcp_conf, } rcp_ctx->silent_server = rcp_conf->silent_server; OSC_MOVE_PTR(rcp_ctx->recipient_id, rcp_conf->recipient_id); + memcpy(rcp_ctx->echo_value, rcp_conf->echo_value, sizeof(rcp_ctx->echo_value)); + if (rcp_conf->window_initialized) { + rcp_ctx->last_seq = rcp_conf->last_seq; + rcp_ctx->sliding_window = rcp_conf->sliding_window; + } rcp_ctx->initial_state = 1; rcp_ctx->osc_ctx = osc_ctx; @@ -769,7 +915,8 @@ oscore_add_recipient(oscore_ctx_t *osc_ctx, coap_oscore_rcp_conf_t *rcp_conf, rcp_chain = osc_ctx->recipient_chain; rcp_ctx->next_recipient = rcp_chain; osc_ctx->recipient_chain = rcp_ctx; - /* Just free rcp_conf as all configured values are now in rcp_ctx */ + oscore_reference_recipient_ctx(rcp_ctx); + /* All configured values are now in rcp_ctx */ coap_free_type(COAP_STRING, rcp_conf); return rcp_ctx; @@ -790,7 +937,7 @@ oscore_delete_recipient(oscore_ctx_t *osc_ctx, coap_bin_const_t *rid) { prev->next_recipient = next->next_recipient; else osc_ctx->recipient_chain = next->next_recipient; - oscore_free_recipient(next); + oscore_free_recipient_ctx(next); return 1; } prev = next; @@ -799,9 +946,36 @@ oscore_delete_recipient(oscore_ctx_t *osc_ctx, coap_bin_const_t *rid) { return 0; } +void +oscore_reference_recipient_ctx(oscore_recipient_ctx_t *recipient_ctx) { + recipient_ctx->ref++; +} + +void +oscore_release_recipient_ctx(oscore_recipient_ctx_t **recipient_ctx) { + oscore_ctx_t *osc_ctx; + + if (recipient_ctx == NULL || *recipient_ctx == NULL) + return; + + /* ensure oscore context is freed if not attached to coap context */ + osc_ctx = (*recipient_ctx)->osc_ctx; + oscore_free_recipient_ctx(*recipient_ctx); + /* + * Free temporary oscore context if not attached to a coap context + * and no recipients attached anymore. + */ + if (!oscore_is_context_attached(osc_ctx) && osc_ctx->recipient_chain == NULL) { + oscore_free_context(osc_ctx); + } + *recipient_ctx = NULL; +} + void oscore_free_association(oscore_association_t *association) { if (association) { + oscore_release_recipient_ctx(&association->recipient_ctx); + coap_delete_pdu_lkd(association->sent_pdu); coap_delete_bin_const(association->token); coap_delete_bin_const(association->aad); @@ -828,7 +1002,7 @@ oscore_new_association(coap_session_t *session, return 0; memset(association, 0, sizeof(oscore_association_t)); - association->recipient_ctx = recipient_ctx; + coap_oscore_association_set_recipient_ctx(association, recipient_ctx); association->is_observe = is_observe; association->just_set_up = 1; diff --git a/tests/test_oscore.c b/tests/test_oscore.c index 28558de962..d589536777 100644 --- a/tests/test_oscore.c +++ b/tests/test_oscore.c @@ -1028,6 +1028,897 @@ t_oscore_c_8_2(void) { coap_free(session); } +/************************************************************************ + ** OSCORE credential storage tests + ************************************************************************/ + +static void +t_convert_1(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "master_salt,hex,\"9e7ca92223786340\"\n" + "sender_id,ascii,\"server\"\n" + "recipient_id,ascii,\"client\"\n" + "recipient_id,ascii,\"client1\"\n" + "recipient_id,ascii,\"client2\"\n" + "replay_window,integer,30\n" + "aead_alg,integer,10\n" + "hkdf_alg,integer,-10\n"; + const coap_str_const_t conf = { sizeof(conf_data)-1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp; + int rcp_count; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + CU_ASSERT(coap_context_oscore_server(ctx, oscore_conf) == 1); + + osc_ctx = ctx->p_osc_ctx; + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx->sender_context); + + /* sender_id == "server" */ + CU_ASSERT(osc_ctx->sender_context->sender_id->length == 6); + CU_ASSERT(memcmp(osc_ctx->sender_context->sender_id->s, "server", 6) == 0); + + /* 3 recipients in chain */ + rcp_count = 0; + for (rcp = osc_ctx->recipient_chain; rcp; rcp = rcp->next_recipient) + rcp_count++; + CU_ASSERT(rcp_count == 3); + + CU_ASSERT(osc_ctx->aead_alg == 10); + CU_ASSERT(osc_ctx->hkdf_alg == -10); + CU_ASSERT(osc_ctx->replay_window_size == 30); + +fail: + oscore_free_contexts(ctx); +} + +/* Common config for ref-counting tests */ +#define REF_CONF_DATA \ + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" \ + "master_salt,hex,\"9e7ca92223786340\"\n" \ + "sender_id,ascii,\"server\"\n" \ + "recipient_id,ascii,\"client\"\n" + +/* + * t_ref_1: Context attached to coap_context. + * Recipient ref is managed by session attach/release. + * After all sessions release, recipient is freed but osc_ctx stays. + */ +static void +t_ref_1(void) { + static const char conf_data[] = REF_CONF_DATA; + const coap_str_const_t conf = { sizeof(conf_data)-1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + CU_ASSERT(coap_context_oscore_server(ctx, oscore_conf) == 1); + + osc_ctx = ctx->p_osc_ctx; + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + + /* After enter_context, recipient ref == 1 */ + rcp_ctx = osc_ctx->recipient_chain; + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + CU_ASSERT(rcp_ctx->ref == 1); + + /* Simulate session attaching recipient: ref becomes 2 */ + rcp_ctx->ref++; + CU_ASSERT(rcp_ctx->ref == 2); + + /* oscore_free_contexts releases the enter_context ref (ref 2 -> 1) */ + oscore_free_contexts(ctx); + + /* Context detached but recipient still alive (ref == 1) */ + CU_ASSERT_PTR_NULL(ctx->p_osc_ctx); + CU_ASSERT(rcp_ctx->ref == 1); + + /* Session releases its ref - recipient freed */ + oscore_release_recipient_ctx(&rcp_ctx); + CU_ASSERT_PTR_NULL(rcp_ctx); + return; + +fail: + oscore_free_contexts(ctx); +} + +/* + * t_ref_2: Context NOT attached to coap_context. + * When the only session releases its recipient, both the + * recipient and the oscore context should be freed. + */ +static void +t_ref_2(void) { + static const char conf_data[] = REF_CONF_DATA; + const coap_str_const_t conf = { sizeof(conf_data)-1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + CU_ASSERT(coap_context_oscore_server(ctx, oscore_conf) == 1); + + osc_ctx = ctx->p_osc_ctx; + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + rcp_ctx = osc_ctx->recipient_chain; + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + + /* Simulate session attach: ref 1 -> 2 */ + rcp_ctx->ref++; + CU_ASSERT(rcp_ctx->ref == 2); + + /* Detach context from coap_context (simulates context teardown while + * session still holds a ref). This decrements ref by 1 for each + * recipient via oscore_context_release_recipients. */ + oscore_free_contexts(ctx); + CU_ASSERT_PTR_NULL(ctx->p_osc_ctx); + CU_ASSERT(rcp_ctx->ref == 1); + + /* osc_ctx is no longer attached (next == NULL) */ + CU_ASSERT_PTR_NULL(osc_ctx->next); + + /* Session releases - recipient AND context get freed + * (oscore_release_recipient frees context when not attached and + * recipient_chain becomes empty). */ + oscore_release_recipient_ctx(&rcp_ctx); + CU_ASSERT_PTR_NULL(rcp_ctx); + /* osc_ctx is now freed - no further access. */ + return; + +fail: + oscore_free_contexts(ctx); +} + +/* + * t_ref_3: Add/remove recipient via public API + * (coap_new_oscore_recipient / coap_delete_oscore_recipient). + * Verify ref counting is consistent. + */ +static void +t_ref_3(void) { + static const char conf_data[] = REF_CONF_DATA; + const coap_str_const_t conf = { sizeof(conf_data)-1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx; + coap_bin_const_t *peer_id; + coap_bin_const_t peer_id_cmp = { 5, (const uint8_t *)"peer1" }; + int rcp_count; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + CU_ASSERT(coap_context_oscore_server(ctx, oscore_conf) == 1); + + osc_ctx = ctx->p_osc_ctx; + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + + /* Initially 1 recipient ("client") with ref == 1 */ + CU_ASSERT_PTR_NOT_NULL(osc_ctx->recipient_chain); + CU_ASSERT(osc_ctx->recipient_chain->ref == 1); + + /* Add "peer1" via public API - ownership of peer_id moves in */ + peer_id = coap_new_bin_const((const uint8_t *)"peer1", 5); + FailIf_CU_ASSERT_PTR_NOT_NULL(peer_id); + CU_ASSERT(coap_new_oscore_recipient(ctx, peer_id) == 1); + + rcp_count = 0; + for (rcp_ctx = osc_ctx->recipient_chain; rcp_ctx; rcp_ctx = rcp_ctx->next_recipient) + rcp_count++; + CU_ASSERT(rcp_count == 2); + + /* Find peer1 and verify ref */ + for (rcp_ctx = osc_ctx->recipient_chain; rcp_ctx; rcp_ctx = rcp_ctx->next_recipient) { + if (rcp_ctx->recipient_id->length == peer_id_cmp.length && + memcmp(rcp_ctx->recipient_id->s, peer_id_cmp.s, peer_id_cmp.length) == 0) + break; + } + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + CU_ASSERT(rcp_ctx->ref == 1); + + /* Remove "peer1" via public API */ + CU_ASSERT(coap_delete_oscore_recipient(ctx, &peer_id_cmp) == 1); + + rcp_count = 0; + for (rcp_ctx = osc_ctx->recipient_chain; rcp_ctx; rcp_ctx = rcp_ctx->next_recipient) + rcp_count++; + CU_ASSERT(rcp_count == 1); + + /* Original "client" still there */ + CU_ASSERT_PTR_NOT_NULL(osc_ctx->recipient_chain); + +fail: + oscore_free_contexts(ctx); +} + +static int t_find_mode; /* 0 = single rcp, 1 = multi rcp, -1 = return NULL */ + +static coap_oscore_conf_t * +test_find_func(const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id) { + static const char single_conf[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,ascii,\"server\"\n" + "recipient_id,ascii,\"client\"\n"; + static const char multi_conf[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,ascii,\"server\"\n" + "recipient_id,ascii,\"client\"\n" + "recipient_id,ascii,\"client1\"\n" + "recipient_id,ascii,\"client2\"\n"; + coap_str_const_t conf; + + (void)session; + (void)rcpkey_id; + (void)ctxkey_id; + + if (t_find_mode < 0) + return NULL; + + if (t_find_mode > 0) { + conf.s = (const uint8_t *)multi_conf; + conf.length = sizeof(multi_conf) - 1; + } else { + conf.s = (const uint8_t *)single_conf; + conf.length = sizeof(single_conf) - 1; + } + return coap_new_oscore_conf(conf, NULL, NULL, 0); +} + +/* + * t_find_1: External find with single recipient. + * Temporary context is NOT attached to coap_context. + * Freed when recipient is released. + */ +static void +t_find_1(void) { + coap_session_t *session = NULL; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx = NULL; + coap_bin_const_t rcpkey_id = { 6, (const uint8_t *)"client" }; + coap_bin_const_t ctxkey_id = { 0, (const uint8_t *)"" }; + + session = coap_malloc_type(COAP_SESSION, sizeof(coap_session_t)); + FailIf_CU_ASSERT_PTR_NOT_NULL(session); + memset(session, 0, sizeof(coap_session_t)); + session->context = ctx; + session->proto = COAP_PROTO_UDP; + session->type = COAP_SESSION_TYPE_CLIENT; + + t_find_mode = 0; + ctx->oscore_find_cb = test_find_func; + + /* lock to pretend to run from inside coap_io_process */ + coap_lock_lock(); + osc_ctx = oscore_find_context(session, rcpkey_id, &ctxkey_id, NULL, &rcp_ctx); + coap_lock_unlock(); + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + + /* Temporary context NOT attached to coap_context */ + CU_ASSERT(!oscore_is_context_attached(osc_ctx)); + CU_ASSERT_PTR_NULL(ctx->p_osc_ctx); + + /* Single recipient returned */ + CU_ASSERT_PTR_NULL(rcp_ctx->next_recipient); + CU_ASSERT(rcp_ctx->recipient_id->length == 6); + CU_ASSERT(memcmp(rcp_ctx->recipient_id->s, "client", 6) == 0); + + /* Release - frees temporary context too (unattached, last recipient) */ + oscore_release_recipient_ctx(&rcp_ctx); + CU_ASSERT_PTR_NULL(rcp_ctx); + +fail: + ctx->oscore_find_cb = NULL; + coap_free(session); +} + +/* + * t_find_2: External find with multiple recipients. + * Only the matching recipient is kept; others are freed. + */ +static void +t_find_2(void) { + coap_session_t *session = NULL; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx = NULL; + coap_bin_const_t rcpkey_id = { 7, (const uint8_t *)"client1" }; + coap_bin_const_t ctxkey_id = { 0, (const uint8_t *)"" }; + + session = coap_malloc_type(COAP_SESSION, sizeof(coap_session_t)); + FailIf_CU_ASSERT_PTR_NOT_NULL(session); + memset(session, 0, sizeof(coap_session_t)); + session->context = ctx; + session->proto = COAP_PROTO_UDP; + session->type = COAP_SESSION_TYPE_CLIENT; + + t_find_mode = 1; + ctx->oscore_find_cb = test_find_func; + + coap_lock_lock(); + osc_ctx = oscore_find_context(session, rcpkey_id, &ctxkey_id, NULL, &rcp_ctx); + coap_lock_unlock(); + + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + + /* Temporary context NOT attached */ + CU_ASSERT_PTR_NULL(osc_ctx->next); + + /* Only matching recipient "client1" kept */ + CU_ASSERT(rcp_ctx->recipient_id->length == 7); + CU_ASSERT(memcmp(rcp_ctx->recipient_id->s, "client1", 7) == 0); + CU_ASSERT_PTR_NULL(rcp_ctx->next_recipient); + CU_ASSERT(osc_ctx->recipient_chain == rcp_ctx); + + oscore_release_recipient_ctx(&rcp_ctx); + CU_ASSERT_PTR_NULL(rcp_ctx); + +fail: + ctx->oscore_find_cb = NULL; + coap_free(session); +} + +/* + * t_find_3: External find returns NULL - falls through to internal storage. + */ +static void +t_find_3(void) { + static const char conf_data[] = REF_CONF_DATA; + const coap_str_const_t conf = { sizeof(conf_data)-1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + coap_session_t *session = NULL; + oscore_ctx_t *osc_ctx; + oscore_recipient_ctx_t *rcp_ctx = NULL; + coap_bin_const_t rcpkey_id = { 6, (const uint8_t *)"client" }; + coap_bin_const_t ctxkey_id = { 0, (const uint8_t *)"" }; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + CU_ASSERT(coap_context_oscore_server(ctx, oscore_conf) == 1); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx); + + session = coap_malloc_type(COAP_SESSION, sizeof(coap_session_t)); + FailIf_CU_ASSERT_PTR_NOT_NULL(session); + memset(session, 0, sizeof(coap_session_t)); + session->context = ctx; + session->proto = COAP_PROTO_UDP; + session->type = COAP_SESSION_TYPE_CLIENT; + + t_find_mode = -1; + ctx->oscore_find_cb = test_find_func; + + coap_lock_lock(); + osc_ctx = oscore_find_context(session, rcpkey_id, &ctxkey_id, NULL, &rcp_ctx); + coap_lock_unlock(); + + FailIf_CU_ASSERT_PTR_NOT_NULL(osc_ctx); + + /* Context from internal storage - IS attached */ + CU_ASSERT(osc_ctx == ctx->p_osc_ctx); + CU_ASSERT_PTR_NOT_NULL(osc_ctx->next); + + /* Recipient found from internal storage */ + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(rcp_ctx->recipient_id); + CU_ASSERT(rcp_ctx->recipient_id->length == 6); + CU_ASSERT(memcmp(rcp_ctx->recipient_id->s, "client", 6) == 0); + +fail: + ctx->oscore_find_cb = NULL; + oscore_free_contexts(ctx); + coap_free(session); +} + +/************************************************************************ + ** Per-recipient state (last_seq / sliding_window / echo_value) in conf buffer + ************************************************************************/ + +/* Basic: all three state fields populate the head of recipient_chain */ +static void +t_rcp_state_basic(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "master_salt,hex,\"9e7ca92223786340\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,42\n" + "sliding_window,unsigned64,1023\n" + "echo_value,hex,\"a1b2c3d4e5f6a7b8\"\n" + "'\n"; + static const uint8_t expected_echo[8] = { + 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8 + }; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 42); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 1023); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + CU_ASSERT(memcmp(oscore_conf->recipient_chain->echo_value, + expected_echo, 8) == 0); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* Boundary: full uint64 range parses correctly */ +static void +t_rcp_state_max_64bit(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,18446744073709551614\n" + "sliding_window,unsigned64,18446744073709551615\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 18446744073709551614ULL); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 18446744073709551615ULL); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* Only last_seq set: window_initialized still set */ +static void +t_rcp_state_only_last_seq(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,7\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 7); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 0); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* Only sliding_window: window_initialized set */ +static void +t_rcp_state_only_sliding_window(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "sliding_window,unsigned64,15\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 0); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 15); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* No state lines: window_initialized stays 0, echo_value stays zeroed */ +static void +t_rcp_state_no_state(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "recipient_id,hex,\"02\"\n"; + static const uint8_t zero_echo[8] = { 0 }; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 0); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 0); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 0); + CU_ASSERT(memcmp(oscore_conf->recipient_chain->echo_value, + zero_echo, 8) == 0); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* echo_value of wrong length must reject the conf */ +static void +t_rcp_state_echo_value_wrong_length(void) { + coap_log_t level = coap_get_log_level(); + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "recipient_id,hex,\"02\"\n" + "echo_value,hex,\"a1b2c3\"\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + + coap_set_log_level(COAP_LOG_CRIT); + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + CU_ASSERT_PTR_NULL(oscore_conf); + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); + coap_set_log_level(level); +} + +/* State fields before any recipient_id must reject the conf */ +static void +t_rcp_state_before_recipient_id(void) { + coap_log_t level = coap_get_log_level(); + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "last_seq,unsigned64,5\n" + "recipient_id,hex,\"02\"\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + + coap_set_log_level(COAP_LOG_CRIT); + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + CU_ASSERT_PTR_NOT_NULL(oscore_conf); + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); + coap_set_log_level(level); +} + +/* + * Multi-recipient: each state line applies to the most recently parsed + * recipient_id (head of recipient_chain because the parser prepends). + */ +static void +t_rcp_state_multi_recipient(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,11\n" + "'\n" + "complex_recipient,config,'" + "recipient_id,hex,\"03\"\n" + "last_seq,unsigned64,22\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + coap_oscore_rcp_conf_t *rcp; + uint64_t last_seq_for_02 = 0; + uint64_t last_seq_for_03 = 0; + int found_02 = 0, found_03 = 0; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + + for (rcp = oscore_conf->recipient_chain; rcp; rcp = rcp->next_recipient) { + if (rcp->recipient_id && rcp->recipient_id->length == 1) { + if (rcp->recipient_id->s[0] == 0x02) { + last_seq_for_02 = rcp->last_seq; + found_02 = 1; + } else if (rcp->recipient_id->s[0] == 0x03) { + last_seq_for_03 = rcp->last_seq; + found_03 = 1; + } + } + } + CU_ASSERT(found_02 && found_03); + CU_ASSERT(last_seq_for_02 == 11); + CU_ASSERT(last_seq_for_03 == 22); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* complex_recipient: basic state fields parsed correctly */ +static void +t_rcp_state_complex_basic(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "master_salt,hex,\"9e7ca92223786340\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'\n" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,99\n" + "sliding_window,unsigned64,255\n" + "echo_value,hex,\"0102030405060708\"\n" + "'\n"; + static const uint8_t expected_echo[8] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + }; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 99); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 255); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + CU_ASSERT(memcmp(oscore_conf->recipient_chain->echo_value, + expected_echo, 8) == 0); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* complex_recipient: echo_value wrong length is rejected */ +static void +t_rcp_state_complex_echo_wrong_length(void) { + coap_log_t level = coap_get_log_level(); + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'\n" + "recipient_id,hex,\"02\"\n" + "echo_value,hex,\"0102030405\"\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf; + + coap_set_log_level(COAP_LOG_CRIT); + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + CU_ASSERT_PTR_NULL(oscore_conf); + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); + coap_set_log_level(level); +} + +/* complex_recipient: only last_seq sets window_initialized */ +static void +t_rcp_state_complex_only_last_seq(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'\n" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,5\n" + "'\n"; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = NULL; + + oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf->recipient_chain); + + CU_ASSERT(oscore_conf->recipient_chain->last_seq == 5); + CU_ASSERT(oscore_conf->recipient_chain->sliding_window == 0); + CU_ASSERT(oscore_conf->recipient_chain->window_initialized == 1); + + coap_delete_oscore_conf(oscore_conf); + return; + +fail: + if (oscore_conf) + coap_delete_oscore_conf(oscore_conf); +} + +/* Propagation: flat form - last_seq/sliding_window/echo_value reach runtime ctx */ +static void +t_propagate_flat(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "master_salt,hex,\"9e7ca92223786340\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,77\n" + "sliding_window,unsigned64,511\n" + "echo_value,hex,\"0102030405060708\"\n" + "'\n"; + static const uint8_t expected_echo[8] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + }; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + coap_context_oscore_server(ctx, oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx->recipient_chain); + + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->last_seq == 77); + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->sliding_window == 511); + CU_ASSERT(memcmp(ctx->p_osc_ctx->recipient_chain->echo_value, + expected_echo, 8) == 0); +fail: + oscore_free_contexts(ctx); +} + +/* Propagation: complex_recipient - state fields reach runtime ctx */ +static void +t_propagate_complex(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "complex_recipient,config,'\n" + "recipient_id,hex,\"02\"\n" + "last_seq,unsigned64,99\n" + "sliding_window,unsigned64,255\n" + "echo_value,hex,\"a1b2c3d4e5f6a7b8\"\n" + "'\n"; + static const uint8_t expected_echo[8] = { + 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8 + }; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + coap_context_oscore_server(ctx, oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx->recipient_chain); + + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->last_seq == 99); + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->sliding_window == 255); + CU_ASSERT(memcmp(ctx->p_osc_ctx->recipient_chain->echo_value, + expected_echo, 8) == 0); +fail: + oscore_free_contexts(ctx); +} + +/* Propagation: no state in conf - runtime ctx fields remain zero */ +static void +t_propagate_no_state(void) { + static const char conf_data[] = + "master_secret,hex,\"0102030405060708090a0b0c0d0e0f10\"\n" + "sender_id,hex,\"01\"\n" + "recipient_id,hex,\"02\"\n"; + static const uint8_t zero_echo[8] = {0}; + const coap_str_const_t conf = { sizeof(conf_data) - 1, + (const uint8_t *)conf_data + }; + coap_oscore_conf_t *oscore_conf = coap_new_oscore_conf(conf, NULL, NULL, 0); + + FailIf_CU_ASSERT_PTR_NOT_NULL(oscore_conf); + coap_context_oscore_server(ctx, oscore_conf); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx); + FailIf_CU_ASSERT_PTR_NOT_NULL(ctx->p_osc_ctx->recipient_chain); + + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->last_seq == 0); + CU_ASSERT(ctx->p_osc_ctx->recipient_chain->sliding_window == 0); + CU_ASSERT(memcmp(ctx->p_osc_ctx->recipient_chain->echo_value, + zero_echo, 8) == 0); +fail: + oscore_free_contexts(ctx); +} + +/* Echo handler: write-only signature compiles and registers */ +static int echo_handler_called; +static uint8_t echo_handler_last_value[8]; +static int echo_handler_last_was_clear; + +static int +t_echo_handler_mock( + const coap_session_t *session, + const coap_bin_const_t rcpkey_id, + const coap_bin_const_t ctxkey_id, + const uint8_t echo_value[8] +) { + (void)session; + (void)rcpkey_id; + (void)ctxkey_id; + echo_handler_called++; + if (echo_value == NULL) { + echo_handler_last_was_clear = 1; + memset(echo_handler_last_value, 0, 8); + } else { + echo_handler_last_was_clear = 0; + memcpy(echo_handler_last_value, echo_value, 8); + } + return 1; +} + +static void +t_echo_handler_write_only(void) { + /* Verify write-only handler registers without compile or runtime errors */ + coap_oscore_register_external_handlers(ctx, NULL, NULL, t_echo_handler_mock); + + /* Verify the handler is stored correctly */ + CU_ASSERT(ctx->oscore_update_echo_cb == t_echo_handler_mock); + + /* Reset */ + coap_oscore_register_external_handlers(ctx, NULL, NULL, NULL); + CU_ASSERT(ctx->oscore_update_echo_cb == NULL); +} + /************************************************************************ ** initialization ************************************************************************/ @@ -1078,6 +1969,34 @@ t_init_oscore_tests(void) { OSCORE_TEST(t_oscore_c_7_2); OSCORE_TEST(t_oscore_c_8); OSCORE_TEST(t_oscore_c_8_2); + + OSCORE_TEST(t_convert_1); + + OSCORE_TEST(t_ref_1); + OSCORE_TEST(t_ref_2); + OSCORE_TEST(t_ref_3); + + OSCORE_TEST(t_find_1); + OSCORE_TEST(t_find_2); + OSCORE_TEST(t_find_3); + + OSCORE_TEST(t_rcp_state_basic); + OSCORE_TEST(t_rcp_state_max_64bit); + OSCORE_TEST(t_rcp_state_only_last_seq); + OSCORE_TEST(t_rcp_state_only_sliding_window); + OSCORE_TEST(t_rcp_state_no_state); + OSCORE_TEST(t_rcp_state_echo_value_wrong_length); + OSCORE_TEST(t_rcp_state_before_recipient_id); + OSCORE_TEST(t_rcp_state_multi_recipient); + + OSCORE_TEST(t_rcp_state_complex_basic); + OSCORE_TEST(t_rcp_state_complex_echo_wrong_length); + OSCORE_TEST(t_rcp_state_complex_only_last_seq); + + OSCORE_TEST(t_propagate_flat); + OSCORE_TEST(t_propagate_complex); + OSCORE_TEST(t_propagate_no_state); + OSCORE_TEST(t_echo_handler_write_only); } return suite[0];