From 6e436bc5ecfa4ad4acb22b7ce1ad0da4bfb81ff4 Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Mon, 2 Sep 2024 15:11:55 +0200 Subject: [PATCH] Migrate device provisioning service --- .github/workflows/cmake-linux.yml | 10 +- .github/workflows/static-analysis.yml | 10 +- CMakeLists.txt | 107 +-- .../device-provisioning-client/plgd_dps.c | 488 ++++++++++ .../plgd_dps_apis.c | 201 ++++ .../plgd_dps_apis_internal.h | 97 ++ .../plgd_dps_cloud.c | 511 ++++++++++ .../plgd_dps_cloud_internal.h | 150 +++ .../plgd_dps_context.c | 296 ++++++ .../plgd_dps_context_internal.h | 160 ++++ .../plgd_dps_dhcp.c | 352 +++++++ .../plgd_dps_dhcp_internal.h | 87 ++ .../plgd_dps_endpoint.c | 206 ++++ .../plgd_dps_endpoint_internal.h | 115 +++ .../plgd_dps_endpoints.c | 167 ++++ .../plgd_dps_endpoints_internal.h | 84 ++ .../plgd_dps_internal.h | 107 +++ .../device-provisioning-client/plgd_dps_log.c | 113 +++ .../plgd_dps_log_internal.h | 142 +++ .../plgd_dps_manager.c | 336 +++++++ .../plgd_dps_manager_internal.h | 76 ++ .../device-provisioning-client/plgd_dps_pki.c | 418 +++++++++ .../plgd_dps_pki_internal.h | 146 +++ .../plgd_dps_provision.c | 648 +++++++++++++ .../plgd_dps_provision_cloud.c | 407 ++++++++ .../plgd_dps_provision_cloud_internal.h | 107 +++ .../plgd_dps_provision_internal.h | 104 ++ .../plgd_dps_provision_owner.c | 208 ++++ .../plgd_dps_provision_owner_internal.h | 59 ++ .../plgd_dps_resource.c | 592 ++++++++++++ .../plgd_dps_resource_internal.h | 89 ++ .../plgd_dps_retry.c | 272 ++++++ .../plgd_dps_retry_internal.h | 83 ++ .../plgd_dps_security.c | 519 ++++++++++ .../plgd_dps_security_internal.h | 101 ++ .../plgd_dps_store.c | 374 ++++++++ .../plgd_dps_store_internal.h | 107 +++ .../device-provisioning-client/plgd_dps_tag.c | 122 +++ .../plgd_dps_tag_internal.h | 84 ++ .../plgd_dps_time.c | 227 +++++ .../plgd_dps_time_internal.h | 78 ++ .../plgd_dps_verify_certificate.c | 218 +++++ .../plgd_dps_verify_certificate_internal.h | 96 ++ api/plgd/plgd.cmake | 5 + include/plgd/plgd_dps.h | 885 ++++++++++++++++++ messaging/coap/observe.c | 10 + security/oc_tls.c | 4 +- tools/utils.cmake | 71 ++ util/oc_memb.h | 4 +- 49 files changed, 9769 insertions(+), 84 deletions(-) create mode 100644 api/plgd/device-provisioning-client/plgd_dps.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_apis.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_apis_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_cloud.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_cloud_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_context.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_context_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_dhcp.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_endpoint.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_endpoints.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_log.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_log_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_manager.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_manager_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_pki.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_pki_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision_cloud.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision_owner.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_resource.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_resource_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_retry.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_retry_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_security.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_security_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_store.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_store_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_tag.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_tag_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_time.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_time_internal.h create mode 100644 api/plgd/device-provisioning-client/plgd_dps_verify_certificate.c create mode 100644 api/plgd/device-provisioning-client/plgd_dps_verify_certificate_internal.h create mode 100644 api/plgd/plgd.cmake create mode 100644 include/plgd/plgd_dps.h create mode 100644 tools/utils.cmake diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 5a814066ae..069cdb1e5a 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -54,16 +54,16 @@ jobs: - args: "-DOC_IPV4_ENABLED=ON -DOC_TCP_ENABLED=ON" # ipv4 on, tcp on, pki off - args: "-DOC_IPV4_ENABLED=ON -DOC_TCP_ENABLED=ON -DOC_PKI_ENABLED=OFF" - # cloud on (ipv4+tcp on), dynamic allocation off - - args: "-DOC_CLOUD_ENABLED=ON -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF" - # cloud on (ipv4+tcp on), collections create on - - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON" + # cloud on (ipv4+tcp on), dynamic allocation off, push notifications off + - args: "-DOC_CLOUD_ENABLED=ON -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_PUSH_ENABLED=OFF" + # cloud on (ipv4+tcp on), collections create on, oscore off, device provisioning on + - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON" # cloud on (ipv4+tcp on), collections create on, custom message buffer size, custom message buffer pool size, custom app data buffer size, custom app data buffer pool size - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_INOUT_BUFFER_SIZE=2048 -DOC_INOUT_BUFFER_POOL=4 -DOC_APP_DATA_BUFFER_SIZE=2048 -DOC_APP_DATA_BUFFER_POOL=4" # debug on - args: "-DOC_DEBUG_ENABLED=ON" # debug on, cloud on (ipv4+tcp on) - - args: "-DOC_CLOUD_ENABLED=ON -DOC_DEBUG_ENABLED=ON" + - args: "-DOC_CLOUD_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=OFF -DOC_DEBUG_ENABLED=ON" # secure off, tcp on - args: "-DOC_SECURITY_ENABLED=OFF -DOC_TCP_ENABLED=ON" # secure off, ipv4 on, tcp on diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 3fa6d0f15f..101b7b4507 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -16,6 +16,14 @@ on: jobs: clang-tidy-linux: + strategy: + fail-fast: false + matrix: + include: + - build_args: + - build_args: -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_TIME_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON + - build_args: -DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_PUSH_ENABLED=OFF -DOC_JSON_ENCODER_ENABLED=OFF -DOC_DEBUG_ENABLED=ON + runs-on: ubuntu-22.04 steps: @@ -40,5 +48,5 @@ jobs: - name: Build with clang and analyze with clang-tidy run: | mkdir build && cd build - cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DOC_CLANG_TIDY_ENABLED=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DBUILD_TESTING=OFF .. + cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DOC_CLANG_TIDY_ENABLED=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_JSON_ENCODER_ENABLED=ON ${{ matrix.build_args }} -DBUILD_TESTING=OFF .. cmake --build . diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f490b7e3b..e2e5dfdf69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,8 +66,6 @@ set(OC_SIMPLE_MAIN_LOOP_ENABLED OFF CACHE BOOL "Compile with the single-threaded if (BUILD_EXAMPLE_APPLICATIONS OR BUILD_TESTING) set(OC_SIMPLE_MAIN_LOOP_ENABLED ON CACHE BOOL "" FORCE) endif() -set(PLGD_DEV_TIME_ENABLED OFF CACHE BOOL "Enable plgd time feature.") - if (OC_DEBUG_ENABLED) set(OC_LOG_MAXIMUM_LOG_LEVEL "TRACE" CACHE STRING "Maximum supported log level in compile time.") else() @@ -79,6 +77,10 @@ set(OC_APP_DATA_BUFFER_SIZE "" CACHE STRING "Custom static buffer size for appli set(OC_APP_DATA_BUFFER_POOL "" CACHE STRING "Custom static size of application messages.") set(OC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS "" CACHE STRING "Maximum number of messages in the network event queue for a device.") +# plgd.dev features +set(PLGD_DEV_TIME_ENABLED OFF CACHE BOOL "Enable plgd time feature.") +set(PLGD_DEV_DEVICE_PROVISIONING_ENABLED OFF CACHE BOOL "Enable plgd's device provisioning feature.") + set(OC_ASAN_ENABLED OFF CACHE BOOL "Enable address sanitizer build.") set(OC_LSAN_ENABLED OFF CACHE BOOL "Enable leak sanitizer build.") set(OC_TSAN_ENABLED OFF CACHE BOOL "Enable thread sanitizer build.") @@ -169,52 +171,7 @@ endif() set(OC_CLANG_TIDY_ENABLED OFF CACHE BOOL "Enable clang-tidy analysis during compilation.") include(tools/clang-tidy.cmake) - -include(CheckCCompilerFlag) -include(CheckCXXCompilerFlag) -# function oc_add_compile_options([GLOBAL] [IX_CXX] FLAGS [flags...]) -# -# Arguments: -# GLOBAL (option) flags are added as global compilation options -# FLAGS list of flags to check and add for both C and C++ -# CFLAGS list of flags to check and add for C -# CXXFLAGS list of flags to check and add for C++ -# -# Side-effect: C_COMPILER_SUPPORTS_${flag_name} / CXX_COMPILER_SUPPORTS_${flag_name} -# is created and set to ON/OFF based on the result of the check. This variable -# can be used in the context of the caller. -function(oc_add_compile_options) - set(options GLOBAL) - set(oneValueArgs) - set(multiValueArgs CFLAGS CXXFLAGS FLAGS) - cmake_parse_arguments(OC_ADD_COMPILE_OPTIONS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - foreach(flag IN LISTS OC_ADD_COMPILE_OPTIONS_FLAGS OC_ADD_COMPILE_OPTIONS_CFLAGS) - string(REPLACE "-" "_" flag_name ${flag}) - string(REPLACE "=" "_" flag_name ${flag_name}) - string(TOUPPER ${flag_name} flag_name) - set(flag_name "C_COMPILER_SUPPORTS${flag_name}") - unset(${flag_name}) - check_c_compiler_flag(${flag} ${flag_name}) - if((OC_ADD_COMPILE_OPTIONS_GLOBAL) AND (${${flag_name}})) - add_compile_options($<$:${flag}>) - endif() - set(${flag_name} ${${flag_name}} PARENT_SCOPE) - endforeach() - - foreach(flag IN LISTS OC_ADD_COMPILE_OPTIONS_FLAGS OC_ADD_COMPILE_OPTIONS_CXXFLAGS) - string(REPLACE "-" "_" flag_name ${flag}) - string(REPLACE "=" "_" flag_name ${flag_name}) - string(TOUPPER ${flag_name} flag_name) - set(flag_name "CXX_COMPILER_SUPPORTS${flag_name}") - unset(${flag_name}) - check_cxx_compiler_flag(${flag} ${flag_name}) - if((OC_ADD_COMPILE_OPTIONS_GLOBAL) AND (${${flag_name}})) - add_compile_options($<$:${flag}>) - endif() - set(${flag_name} ${${flag_name}} PARENT_SCOPE) - endforeach() -endfunction() +include(tools/utils.cmake) # Global compile options set(PRIVATE_COMPILE_OPTIONS "") @@ -278,24 +235,7 @@ if(BUILD_MBEDTLS) endif() set(OC_LOG_MAXIMUM_LOG_LEVEL_INT) -if(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "DISABLED") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT -1) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "ERROR") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 3) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "WARNING") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 4) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "NOTICE") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 5) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "INFO") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 6) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "DEBUG") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 7) -elseif(OC_LOG_MAXIMUM_LOG_LEVEL STREQUAL "TRACE") - set(OC_LOG_MAXIMUM_LOG_LEVEL_INT 8) -else() - message(FATAL_ERROR "Invalid OC_LOG_MAXIMUM_LOG_LEVEL: ${OC_LOG_MAXIMUM_LOG_LEVEL}") -endif() - +oc_set_maximum_log_level(${OC_LOG_MAXIMUM_LOG_LEVEL} OC_LOG_MAXIMUM_LOG_LEVEL_INT) set(OC_LOG_MAXIMUM_LEVEL ${OC_LOG_MAXIMUM_LOG_LEVEL_INT} CACHE INTERNAL "Maximum supported log level in compile time as integer.") # clang-tidy triggers bugprone-macro-parentheses if the value is not in () @@ -417,13 +357,6 @@ if(OC_SIMPLE_MAIN_LOOP_ENABLED) list(APPEND PUBLIC_COMPILE_DEFINITIONS "OC_SIMPLE_MAIN_LOOP") endif() -if(PLGD_DEV_TIME_ENABLED) - list(APPEND PUBLIC_COMPILE_DEFINITIONS "PLGD_DEV_TIME") - if(BUILD_MBEDTLS) - list(APPEND MBEDTLS_COMPILE_DEFINITIONS "PLGD_DEV_TIME") - endif() -endif() - if(NOT("${OC_INOUT_BUFFER_SIZE}" STREQUAL "")) if(NOT OC_DYNAMIC_ALLOCATION_ENABLED) message(FATAL_ERROR "Cannot OC_INOUT_BUFFER_SIZE without dynamic allocation") @@ -471,6 +404,28 @@ if(NOT("${OC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS}" STREQUAL "")) list(APPEND PUBLIC_COMPILE_DEFINITIONS "OC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS=(${OC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS})") endif() +if(PLGD_DEV_TIME_ENABLED) + list(APPEND PUBLIC_COMPILE_DEFINITIONS "PLGD_DEV_TIME") + if(BUILD_MBEDTLS) + list(APPEND MBEDTLS_COMPILE_DEFINITIONS "PLGD_DEV_TIME") + endif() +endif() + +if(PLGD_DEV_DEVICE_PROVISIONING_ENABLED) + if(OC_OSCORE_ENABLED) + message(WARNING "Cannot set PLGD_DEV_DEVICE_PROVISIONING_ENABLED with OC_OSCORE_ENABLED") + endif() + if(NOT OC_CLOUD_ENABLED) + message(FATAL_ERROR "Cannot set PLGD_DEV_DEVICE_PROVISIONING_ENABLED without OC_CLOUD_ENABLED") + endif() + if(NOT OC_SECURITY_ENABLED) + message(FATAL_ERROR "Cannot set PLGD_DEV_DEVICE_PROVISIONING_ENABLED without OC_SECURITY_ENABLED") + endif() + if(NOT PLGD_DEV_TIME_ENABLED) + message(FATAL_ERROR "Cannot set PLGD_DEV_DEVICE_PROVISIONING_ENABLED without PLGD_DEV_TIME_ENABLED") + endif() +endif() + if(BUILD_TESTING) list(APPEND PRIVATE_COMPILE_DEFINITIONS "OC_TEST") if(BUILD_MBEDTLS) @@ -712,7 +667,11 @@ if(OC_SECURITY_ENABLED) target_link_libraries(server-obj PRIVATE ${MBEDTLS_DEP}) endif() -add_library(client-server-obj OBJECT ${COMMON_SRC} ${CLIENT_SRC}) +if(PLGD_DEV_DEVICE_PROVISIONING_ENABLED) +include(api/plgd/plgd.cmake) +endif() + +add_library(client-server-obj OBJECT ${COMMON_SRC} ${CLIENT_SRC} ${PLGD_DPS_SRC}) target_compile_definitions(client-server-obj PRIVATE ${PRIVATE_COMPILE_DEFINITIONS} PUBLIC ${PUBLIC_COMPILE_DEFINITIONS} "OC_CLIENT" "OC_SERVER") target_compile_options(client-server-obj PRIVATE ${PRIVATE_COMPILE_OPTIONS}) target_include_directories(client-server-obj PRIVATE ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/port ${PORT_INCLUDE_DIR}) diff --git a/api/plgd/device-provisioning-client/plgd_dps.c b/api/plgd/device-provisioning-client/plgd_dps.c new file mode 100644 index 0000000000..5f86fa2d98 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps.c @@ -0,0 +1,488 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_resource_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_store_internal.h" // dps_store_init +#include "plgd_dps_internal.h" + +#include "api/oc_tcp_internal.h" +#include "oc_certs.h" +#include "oc_core_res.h" +#include "oc_network_monitor.h" + +#include + +static void +dps_manager_status_cb(plgd_dps_context_t *ctx) +{ + DPS_DBG("manager status changed %d", (int)ctx->status); + if (ctx->callbacks.on_status_change != NULL) { + ctx->callbacks.on_status_change(ctx, ctx->status, + ctx->callbacks.on_status_change_data); + } +} + +oc_event_callback_retval_t +dps_status_callback_handler(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + dps_manager_status_cb(ctx); + return OC_EVENT_DONE; +} + +#ifdef OC_SESSION_EVENTS + +static void +dps_manager_restart(plgd_dps_context_t *ctx) +{ + dps_manager_stop(ctx); + dps_reset_delayed_callback(ctx, dps_manager_start_async, 0); +} + +static void +dps_handle_endpoint_event(plgd_dps_context_t *ctx, + const oc_endpoint_t *endpoint, + oc_session_state_t state) +{ +#if DPS_DBG_IS_ENABLED +// GCOVR_EXCL_START +#define ENDPOINT_STR_LEN 256 + char ep_str[ENDPOINT_STR_LEN] = { 0 }; +#undef ENDPOINT_STR_LEN + dps_endpoint_log_string(endpoint, ep_str, sizeof(ep_str)); +// GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + DPS_DBG("dps_ep_session_event_handler for %s, ep_state: %d", ep_str, + (int)state); + if (!ctx->manager_started) { + DPS_DBG("manager not started yet"); + return; + } + if (state == OC_SESSION_CONNECTED && ctx->endpoint->session_id == 0 && + (ctx->endpoint->flags & TCP) != 0) { + ctx->endpoint->session_id = endpoint->session_id; + DPS_DBG("%s session_id set", ep_str); + } + bool changed = ctx->endpoint_state != state; + if (!changed) { + DPS_DBG("%s state hasn't changed", ep_str); + return; + } + ctx->endpoint_state = state; + if (state == OC_SESSION_CONNECTED) { + DPS_DBG("%s connected", ep_str); + return; + } + if (state == OC_SESSION_DISCONNECTED) { + DPS_DBG("%s disconnected", ep_str); + if (ctx->closing_insecure_peer) { + DPS_DBG("insecure TLS session closed"); + ctx->closing_insecure_peer = false; + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) == 0) { + // keep the endpoint, we only need a new secure session -> set + // new session_id + ctx->endpoint->session_id = oc_tcp_get_new_session_id(); + DPS_DBG("continuing provisioning with new session_id=%" PRIu32, + ctx->endpoint->session_id); + dps_provisioning_schedule_next_step(ctx); + return; + } + // an error occurred -> clean up the endpoint, retry will reinitialize it + DPS_DBG("retry provisioning"); + memset(ctx->endpoint, 0, sizeof(oc_endpoint_t)); + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + dps_retry_get_delay(&ctx->retry)); + return; + } + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) == 0 && + !dps_is_provisioned_with_cloud_started(ctx)) { + dps_manager_restart(ctx); + } + return; + } +} + +static void +dps_ep_session_event_handler(const oc_endpoint_t *endpoint, + oc_session_state_t state, void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + if (oc_endpoint_compare(endpoint, ctx->endpoint) == 0) { + dps_handle_endpoint_event(ctx, endpoint, state); + return; + } + + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if ((cloud_ctx != NULL) && + oc_endpoint_compare(endpoint, oc_cloud_get_server(cloud_ctx)) == 0) { + DPS_DBG("dps_ep_session_event_handler cloud_state: %d", (int)state); + if (state == OC_SESSION_DISCONNECTED && + oc_cloud_manager_is_started(cloud_ctx)) { + // TODO + // dps_cloud_observe_status(ctx); + } + return; + } +} + +static bool +dps_restart_initialized(plgd_dps_context_t *ctx, void *data) +{ + (void)data; + if (ctx->status == PLGD_DPS_INITIALIZED) { + dps_manager_restart(ctx); + } + return true; +} + +static void +dps_interface_event_handler(oc_interface_event_t event) +{ + if (event == NETWORK_INTERFACE_UP) { + dps_contexts_iterate(dps_restart_initialized, NULL); + } +} + +#endif /* OC_SESSION_EVENTS */ + +int +plgd_dps_init(void) +{ + dps_update_list_init(); + for (size_t device = 0; device < oc_core_get_num_devices(); device++) { + plgd_dps_context_t *ctx = dps_context_alloc(); + if (ctx == NULL) { + DPS_ERR("insufficient memory to create context"); + return -1; + } + dps_context_init(ctx, device); + if (dps_store_load(&ctx->store, device) == 0) { + DPS_INFO("DPS data loaded from storage"); + } + dps_context_list_add(ctx); + } + return 0; +} + +void +plgd_dps_shutdown(void) +{ + for (size_t device = 0; device < oc_core_get_num_devices(); device++) { + plgd_dps_context_t *ctx = plgd_dps_get_context(device); + if (ctx == NULL) { + continue; + } +#ifdef OC_SESSION_EVENTS + oc_remove_session_event_callback_v1(dps_ep_session_event_handler, ctx, + false); +#endif /* OC_SESSION_EVENTS */ + dps_manager_stop(ctx); + oc_delayed_delete_resource(ctx->conf); + dps_endpoint_close(ctx->endpoint); + dps_context_deinit(ctx); + dps_context_list_remove(ctx); + dps_context_free(ctx); + dps_update_list_cleanup(); + DPS_DBG("dps_shutdown for %zu", device); + } +} + +#ifdef OC_SESSION_EVENTS + +void +plgd_dps_session_callbacks_init(plgd_dps_context_t *ctx) +{ + oc_add_session_event_callback_v1(dps_ep_session_event_handler, ctx); +} + +void +plgd_dps_session_callbacks_deinit(plgd_dps_context_t *ctx) +{ + oc_remove_session_event_callback_v1(dps_ep_session_event_handler, ctx, false); +} + +void +plgd_dps_interface_callbacks_init(void) +{ + oc_add_network_interface_event_callback(dps_interface_event_handler); +} + +void +plgd_dps_interface_callbacks_deinit(void) +{ + oc_remove_network_interface_event_callback(dps_interface_event_handler); +} + +#endif /* OC_SESSION_EVENTS */ + +bool +dps_try_set_identity_chain(size_t device) +{ + int dps_id_credid = dps_get_identity_credid(device); + if (dps_id_credid == -1) { + DPS_DBG("identity certificate not found"); + return false; + } + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(device); + if (cloud_ctx == NULL) { + return false; + } + if (oc_cloud_get_identity_cert_chain(cloud_ctx) == dps_id_credid) { + // cloud has same cert chain as before. + return true; + } + oc_cloud_set_identity_cert_chain(cloud_ctx, dps_id_credid); + DPS_DBG("certificate chain updated to credid=%d", dps_id_credid); + return true; +} + +bool +plgd_cloud_manager_start(const plgd_dps_context_t *ctx) +{ + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("Cloud context not found"); + return false; + } +#if DPS_INFO_IS_ENABLED + // GCOVR_EXCL_START + const oc_string_t *ep_str = oc_cloud_get_server_uri(cloud_ctx); + const char *ep_cstr = ep_str != NULL ? oc_string(*ep_str) : "NULL"; + DPS_INFO("Starting cloud registration with endpoint(%s)", + ep_cstr != NULL ? ep_cstr : "NULL"); + // GCOVR_EXCL_STOP +#endif /* DPS_INFO_IS_ENABLED */ + return oc_cloud_manager_start( + cloud_ctx, ctx->callbacks.on_cloud_status_change, + ctx->callbacks.on_cloud_status_change_data) == 0; +} + +static bool +dps_set_certificate_fingerprint(plgd_dps_context_t *ctx, + mbedtls_md_type_t md_type, + const uint8_t *fingerprint, size_t size) +{ + assert(ctx != NULL); + if (md_type != MBEDTLS_MD_NONE) { + if (!oc_sec_certs_md_algorithm_is_allowed(md_type)) { + DPS_ERR("DPS Service certificate fingerprint algorithm not allowed"); + return false; + } + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(md_type); + if (md_info == NULL) { + DPS_ERR("DPS Service certificate fingerprint algorithm not found"); + return false; + } + if (mbedtls_md_get_size(md_info) != size) { + DPS_ERR("DPS Service certificate fingerprint size mismatch"); + return false; + } + } + if (ctx->certificate_fingerprint.md_type == md_type && + dps_is_equal_string_len(ctx->certificate_fingerprint.data, + (const char *)fingerprint, size)) { + return true; + } + oc_set_string(&ctx->certificate_fingerprint.data, (const char *)fingerprint, + size); + ctx->certificate_fingerprint.md_type = md_type; +#if DPS_DBG_IS_ENABLED + dps_print_fingerprint(md_type, fingerprint, size); +#endif /* DPS_DBG_IS_ENABLED */ + return true; +} + +bool +plgd_dps_set_certificate_fingerprint(plgd_dps_context_t *ctx, + mbedtls_md_type_t md_type, + const uint8_t *fingerprint, size_t size) +{ + return dps_set_certificate_fingerprint(ctx, md_type, fingerprint, size); +} + +int +plgd_dps_get_certificate_fingerprint(const plgd_dps_context_t *ctx, + mbedtls_md_type_t *md_type, + uint8_t *buffer, size_t buffer_size) +{ + assert(ctx != NULL); + assert(md_type != NULL); + assert(buffer != NULL); + if (oc_string(ctx->certificate_fingerprint.data) == NULL) { + DPS_DBG("No certificate_fingerprint set"); + *md_type = MBEDTLS_MD_NONE; + return 0; + } + size_t len = oc_string_len(ctx->certificate_fingerprint.data); + if (buffer_size < len) { + DPS_ERR("cannot copy certificate_fingerprint to buffer: buffer too small " + "(minimal size=%zu)", + len); + return -1; + } + if (len > 0) { + memcpy(buffer, oc_string(ctx->certificate_fingerprint.data), len); + } + *md_type = ctx->certificate_fingerprint.md_type; + return (int)len; +} + +static oc_event_callback_retval_t +dps_notify_observers_callback(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + if (ctx->conf) { + oc_notify_observers(ctx->conf); + } + return OC_EVENT_DONE; +} + +void +dps_notify_observers(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + if (ctx->conf != NULL) { + dps_reset_delayed_callback(ctx, dps_notify_observers_callback, 0); + } +} + +const char * +dps_status_flag_to_str(plgd_dps_status_t status) +{ + switch (status) { + case PLGD_DPS_INITIALIZED: + return kPlgdDpsStatusInitialized; + case PLGD_DPS_GET_TIME: + return kPlgdDpsStatusGetTime; + case PLGD_DPS_HAS_TIME: + return kPlgdDpsStatusHasTime; + case PLGD_DPS_GET_OWNER: + return kPlgdDpsStatusGetOwner; + case PLGD_DPS_HAS_OWNER: + return kPlgdDpsStatusHasOwner; + case PLGD_DPS_GET_CREDENTIALS: + return kPlgdDpsStatusGetCredentials; + case PLGD_DPS_HAS_CREDENTIALS: + return kPlgdDpsStatusHasCredentials; + case PLGD_DPS_GET_ACLS: + return kPlgdDpsStatusGetAcls; + case PLGD_DPS_HAS_ACLS: + return kPlgdDpsStatusHasAcls; + case PLGD_DPS_GET_CLOUD: + return kPlgdDpsStatusGetCloud; + case PLGD_DPS_HAS_CLOUD: + return kPlgdDpsStatusHasCloud; + case PLGD_DPS_CLOUD_STARTED: + return kPlgdDpsStatusProvisioned; + case PLGD_DPS_RENEW_CREDENTIALS: + return kPlgdDpsStatusRenewCredentials; + case PLGD_DPS_TRANSIENT_FAILURE: + return kPlgdDpsStatusTransientFailure; + case PLGD_DPS_FAILURE: + return kPlgdDpsStatusFailure; + } + return ""; +} + +typedef struct +{ + char *buffer; + size_t buffer_size; + bool add_separator; +} dps_status_buffer_t; + +static bool +dps_status_write_flag_to_buffer(dps_status_buffer_t *buffer, + plgd_dps_status_t status) +{ + if (buffer->add_separator) { + int written = snprintf(buffer->buffer, buffer->buffer_size, "|"); + if (written < 0 || (size_t)written >= buffer->buffer_size) { + return false; + } + buffer->buffer_size -= (size_t)written; + buffer->buffer += (size_t)written; + } + int written = snprintf(buffer->buffer, buffer->buffer_size, "%s", + dps_status_flag_to_str(status)); + if (written < 0 || (size_t)written >= buffer->buffer_size) { + return false; + } + buffer->buffer_size -= (size_t)written; + buffer->buffer += (size_t)written; + buffer->add_separator = true; + return true; +} + +int +dps_status_to_logstr(uint32_t status, char *buffer, size_t buffer_size) +{ + if (status == 0) { + int written = + snprintf(buffer, buffer_size, "%s", kPlgdDpsStatusUninitialized); + return (written < 0 || (size_t)written >= buffer_size) ? -1 : 0; + } + + dps_status_buffer_t status_buffer = { + .buffer = buffer, + .buffer_size = buffer_size, + .add_separator = false, + }; + + plgd_dps_status_t all_statuses[] = { + PLGD_DPS_INITIALIZED, PLGD_DPS_GET_TIME, + PLGD_DPS_HAS_TIME, PLGD_DPS_GET_OWNER, + PLGD_DPS_HAS_OWNER, PLGD_DPS_GET_CREDENTIALS, + PLGD_DPS_HAS_CREDENTIALS, PLGD_DPS_GET_ACLS, + PLGD_DPS_HAS_ACLS, PLGD_DPS_GET_CLOUD, + PLGD_DPS_HAS_CLOUD, PLGD_DPS_CLOUD_STARTED, + PLGD_DPS_RENEW_CREDENTIALS, PLGD_DPS_TRANSIENT_FAILURE, + PLGD_DPS_FAILURE, + }; + for (size_t i = 0; i < sizeof(all_statuses) / sizeof(all_statuses[0]); i++) { + if ((status & all_statuses[i]) == 0) { + continue; + } + if (!dps_status_write_flag_to_buffer(&status_buffer, all_statuses[i])) { + return -1; + } + } + return 0; +} + +#if DPS_DBG_IS_ENABLED +void +dps_print_status(const char *prefix, uint32_t status) +{ + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(status, str, sizeof(str)); + if (prefix == NULL) { + DPS_DBG("status(%u:%s)", status, ret >= 0 ? str : "(NULL)"); + return; + } + DPS_DBG("%sstatus(%u:%s)", prefix, status, ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +} +#endif /* DPS_DBG_IS_ENABLED */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_apis.c b/api/plgd/device-provisioning-client/plgd_dps_apis.c new file mode 100644 index 0000000000..343a1b8d26 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_apis.c @@ -0,0 +1,201 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_context_internal.h" +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_log_internal.h" // DPS_DBG +#include "plgd_dps_endpoint_internal.h" +#include "plgd/plgd_dps.h" // plgd_dps_context_t + +#include "oc_api.h" // oc_remove_delayed_callback +#include "oc_rep.h" // oc_rep_get_by_type_and_key +#include "oc_ri.h" // oc_ri_add_timed_event_callback_ticks +#include "oc_helpers.h" // oc_string_t +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_macros_internal.h" + +#include +#include + +bool +dps_is_equal_string_len(oc_string_t str1, const char *str2, size_t str2_len) +{ + if (oc_string(str1) == NULL) { + return str2 == NULL; + } + return str2 != NULL && oc_string_len_unsafe(str1) == str2_len && + memcmp(oc_string(str1), str2, str2_len) == 0; +} + +bool +dps_is_equal_string(oc_string_t str1, oc_string_t str2) +{ + return str1.size == str2.size && + (str1.size == 0 || + memcmp(oc_string(str1), oc_string(str2), str1.size) == 0); +} + +bool +dps_is_property(const oc_rep_t *rep, oc_rep_value_type_t ptype, + const char *pname, size_t pname_len) +{ + size_t prop_len = oc_string_len_unsafe(rep->name); + const char *prop = oc_string(rep->name); + return rep->type == ptype && prop_len == pname_len && + memcmp(prop, pname, pname_len) == 0; +} + +static uint64_t +dps_get_time_max(void) +{ + if (sizeof(oc_clock_time_t) >= sizeof(uint64_t)) { + return UINT64_MAX; + } + if (sizeof(oc_clock_time_t) >= sizeof(uint32_t)) { + return UINT32_MAX; + } + return UINT16_MAX; +} + +void +dps_reset_delayed_callback_ms(void *cb_data, oc_trigger_t callback, + uint64_t milliseconds) +{ + oc_remove_delayed_callback(cb_data, callback); + const uint64_t oc_clock_time_max = dps_get_time_max(); + +#define MILLISECONDS_IN_SECONDS 1000 +#define OC_CLOCK_MILLISECOND (OC_CLOCK_SECOND / MILLISECONDS_IN_SECONDS) + if (oc_clock_time_max / OC_CLOCK_MILLISECOND < milliseconds) { + DPS_DBG("delayed callback interval truncated to %lu", oc_clock_time_max); + milliseconds = oc_clock_time_max / OC_CLOCK_MILLISECOND; + } + oc_clock_time_t interval = milliseconds * OC_CLOCK_MILLISECOND; + oc_ri_add_timed_event_callback_ticks(cb_data, callback, interval); +} + +void +dps_reset_delayed_callback(void *cb_data, oc_trigger_t callback, + uint64_t seconds) +{ +#define MILLISECONDS_IN_SECONDS 1000 + dps_reset_delayed_callback_ms(cb_data, callback, + seconds * MILLISECONDS_IN_SECONDS); +} + +bool +dps_is_timeout_error_code(oc_status_t code) +{ + return code == OC_REQUEST_TIMEOUT || code == OC_TRANSACTION_TIMEOUT; +} + +bool +dps_is_connection_error_code(oc_status_t code) +{ + return code == OC_STATUS_SERVICE_UNAVAILABLE || + code == OC_STATUS_GATEWAY_TIMEOUT; +} + +bool +dps_is_error_code(oc_status_t code) +{ + return code >= OC_STATUS_BAD_REQUEST; +} + +plgd_dps_error_t +dps_response_get_error_code(oc_status_t code) +{ + if (dps_is_timeout_error_code(code) || dps_is_connection_error_code(code)) { + return PLGD_DPS_ERROR_CONNECT; + } + if (dps_is_error_code(code)) { + return PLGD_DPS_ERROR_RESPONSE; + } + return PLGD_DPS_OK; +} + +bool +dps_handle_redirect_response(plgd_dps_context_t *ctx, const oc_rep_t *payload) +{ +#define REDIRECTURI_KEY "redirecturi" + const oc_rep_t *redirect = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, REDIRECTURI_KEY, + OC_CHAR_ARRAY_LEN(REDIRECTURI_KEY)); + if (redirect == NULL) { + return true; + } + const oc_string_t *redirecturi = &redirect->value.string; + if (oc_string_is_empty(redirecturi)) { + DPS_ERR("invalid redirect uri"); + return false; + } + + if (oc_endpoint_addresses_is_selected(&ctx->store.endpoints, + oc_string_view2(redirecturi))) { + return true; + } + DPS_INFO("Redirect to endpoint: %s detected", oc_string(*redirecturi)); + + const oc_endpoint_address_t *ep_selected = + oc_endpoint_addresses_selected(&ctx->store.endpoints); + oc_string_view_t ep_selected_name = OC_STRING_VIEW_NULL; + if (ep_selected != NULL) { + assert(ep_selected->metadata.id_type == + OC_ENDPOINT_ADDRESS_METADATA_TYPE_NAME); + ep_selected_name = oc_string_view2(&ep_selected->metadata.id.name); + } + + if (!oc_endpoint_addresses_contains(&ctx->store.endpoints, + oc_string_view2(redirecturi)) && + oc_endpoint_addresses_add( + &ctx->store.endpoints, + oc_endpoint_address_make_view_with_name(oc_string_view2(redirecturi), + ep_selected_name)) == NULL) { + DPS_ERR("failed to add endpoint to the list"); + return false; + } + + // remove the original server from the list + if (ep_selected != NULL) { + oc_endpoint_addresses_remove(&ctx->store.endpoints, ep_selected); + } + // select the new server + oc_endpoint_addresses_select_by_uri(&ctx->store.endpoints, + oc_string_view2(redirecturi)); + dps_endpoint_disconnect(ctx); + return true; +} + +plgd_dps_error_t +dps_check_response(plgd_dps_context_t *ctx, oc_status_t code, + const oc_rep_t *payload) +{ + plgd_dps_error_t err = dps_response_get_error_code(code); + if (err != PLGD_DPS_OK) { + return err; + } + + if (payload == NULL) { + return PLGD_DPS_OK; + } + DPS_DBG("dps_check_response OK %p", (void *)payload); + if (!dps_handle_redirect_response(ctx, payload)) { + DPS_WRN("failed to handle redirect response"); + } + return PLGD_DPS_OK; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_apis_internal.h b/api/plgd/device-provisioning-client/plgd_dps_apis_internal.h new file mode 100644 index 0000000000..62b39a757b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_apis_internal.h @@ -0,0 +1,97 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_APIS_INTERNAL_H +#define PLGD_DPS_APIS_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_config.h" +#include "oc_endpoint.h" +#include "oc_helpers.h" +#include "util/oc_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// @brief Compare oc_string_t with a C-string with len +OC_NO_DISCARD_RETURN +bool dps_is_equal_string_len(oc_string_t str1, const char *str2, + size_t str2_len); + +/// @brief Compare 2 oc_string_t +OC_NO_DISCARD_RETURN +bool dps_is_equal_string(oc_string_t str1, oc_string_t str2); + +/// @brief Check if oc_rep_t is a property with a given name and type +OC_NO_DISCARD_RETURN +bool dps_is_property(const oc_rep_t *rep, oc_rep_value_type_t ptype, + const char *pname, size_t pname_len) OC_NONNULL(); + +/// @brief Remove scheduled callback (if it exists) and schedule it again +void dps_reset_delayed_callback(void *cb_data, oc_trigger_t callback, + uint64_t seconds); + +/// @brief Remove scheduled callback (if it exists) and schedule it again +/// (interval in milliseconds) +void dps_reset_delayed_callback_ms(void *cb_data, oc_trigger_t callback, + uint64_t milliseconds); + +/// @brief Check if status code is the request timeout error +/// (OC_REQUEST_TIMEOUT) +bool dps_is_timeout_error_code(oc_status_t code); + +/// @brief Check if status code is a request connection error +/// (OC_STATUS_SERVICE_UNAVAILABLE, OC_STATUS_GATEWAY_TIMEOUT) +bool dps_is_connection_error_code(oc_status_t code); + +/// @brief Check if status code is an error code. +bool dps_is_error_code(oc_status_t code); + +/// @brief Handle DPS redirect response +OC_NO_DISCARD_RETURN +bool dps_handle_redirect_response(plgd_dps_context_t *ctx, + const oc_rep_t *payload) OC_NONNULL(); + +/** + * @brief Check DPS service response for errors or redirect. + * + * @param ctx device context (cannot be NULL) + * @param code response status code + * @param payload payload to check for redirect + * @return PLGD_DPS_OK on success + * @return >PLGD_DPS_OK on error + */ +OC_NO_DISCARD_RETURN +plgd_dps_error_t dps_check_response(plgd_dps_context_t *ctx, oc_status_t code, + const oc_rep_t *payload) OC_NONNULL(1); + +/// @brief Convert oc_status_t code from request response to plgd_dps_error_t +OC_NO_DISCARD_RETURN +plgd_dps_error_t dps_response_get_error_code(oc_status_t code); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_APIS_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_cloud.c b/api/plgd/device-provisioning-client/plgd_dps_cloud.c new file mode 100644 index 0000000000..1f10252c04 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_cloud.c @@ -0,0 +1,511 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_cloud_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_internal.h" + +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/oc_helpers_internal.h" +#include "oc_api.h" +#include "oc_cloud.h" +#include "oc_rep.h" // oc_rep_get_by_type_and_key +#include "oc_uuid.h" +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_macros_internal.h" + +#include +#include +#include + +bool +dps_cloud_is_started(size_t device) +{ + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(device); + if (cloud_ctx == NULL) { + return false; + } + return oc_cloud_manager_is_started(cloud_ctx); +} + +static bool +cloud_check_status(size_t device, uint8_t status) +{ + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(device); + if (cloud_ctx == NULL) { + return false; + } + return (oc_cloud_get_status(cloud_ctx) & status) == status; +} + +bool +dps_cloud_is_registered(size_t device) +{ + return cloud_check_status(device, OC_CLOUD_REGISTERED); +} + +bool +dps_cloud_is_logged_in(size_t device) +{ + return cloud_check_status(device, OC_CLOUD_LOGGED_IN); +} + +void +dps_cloud_observer_init(plgd_cloud_status_observer_t *obs) +{ + assert(obs != NULL); + memset(obs, 0, sizeof(plgd_cloud_status_observer_t)); + obs->cfg.max_count = 30; // NOLINT + obs->cfg.interval_s = 1; +} + +static void +dps_cloud_observer_on_cloud_server_change(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + // invoke the original callback + if (ctx->cloud_observer.original_on_selected_change.cb != NULL) { + ctx->cloud_observer.original_on_selected_change.cb( + ctx->cloud_observer.original_on_selected_change.cb_data); + } + + dps_cloud_observer_on_server_change(ctx); +} + +void +dps_cloud_observer_deinit(plgd_dps_context_t *ctx) +{ + oc_remove_delayed_callback(ctx, dps_cloud_observe_status_async); + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx != NULL) { + oc_endpoint_addresses_on_selected_change_t on_selected_change = + oc_endpoint_addresses_get_on_selected_change( + &cloud_ctx->store.ci_servers); + if (on_selected_change.cb == &dps_cloud_observer_on_cloud_server_change) { + oc_endpoint_addresses_set_on_selected_change( + &cloud_ctx->store.ci_servers, + ctx->cloud_observer.original_on_selected_change.cb, + ctx->cloud_observer.original_on_selected_change.cb_data); + ctx->cloud_observer.original_on_selected_change.cb = NULL; + ctx->cloud_observer.original_on_selected_change.cb_data = NULL; + } + } + ctx->cloud_observer.last_status = 0; + ctx->cloud_observer.retry_count = 0; + oc_free_string(&ctx->cloud_observer.initial_endpoint_uri); + memset(&ctx->cloud_observer.last_endpoint_uuid, 0, sizeof(oc_uuid_t)); + ctx->cloud_observer.remaining_endpoint_changes = 0; +} + +bool +dps_cloud_observer_copy_endpoint_uuid(plgd_cloud_status_observer_t *obs, + const oc_uuid_t *uuid) +{ + oc_uuid_t nil_uuid = { { 0 } }; + if (uuid == NULL) { + uuid = &nil_uuid; + } + + if (oc_uuid_is_equal(obs->last_endpoint_uuid, *uuid)) { + return false; + } + memcpy(&obs->last_endpoint_uuid, uuid, sizeof(oc_uuid_t)); + return true; +} + +static bool +dps_cloud_observer_server_retry_is_ongoing( + const plgd_cloud_status_observer_t *obs) +{ + return oc_string(obs->initial_endpoint_uri) != NULL; +} + +bool +dps_cloud_observer_load(plgd_cloud_status_observer_t *obs, + const oc_cloud_context_t *cloud_ctx) +{ + // get the selected cloud server + const oc_endpoint_address_t *selected = + oc_cloud_selected_server_address(cloud_ctx); + if (selected == NULL) { + DPS_ERR("No cloud server selected"); + return false; + } + oc_copy_string(&obs->initial_endpoint_uri, oc_endpoint_address_uri(selected)); + dps_cloud_observer_copy_endpoint_uuid(obs, + oc_endpoint_address_uuid(selected)); + + // endpoint retry count = number of cloud servers (except the currently + // selected one) + obs->remaining_endpoint_changes = dps_cloud_count_servers(cloud_ctx, true); + DPS_DBG("Number of alternative cloud servers: %u", + (unsigned)obs->remaining_endpoint_changes); + obs->retry_count = 0; + obs->last_status = 0; + return true; +} + +void +dps_cloud_observer_on_provisioning_started(plgd_dps_context_t *ctx, + oc_cloud_context_t *cloud_ctx) +{ + if (dps_cloud_observer_server_retry_is_ongoing(&ctx->cloud_observer)) { + DPS_INFO("Reinitializing cloud observer on cloud provisioning of server " + "with different ID"); + dps_cloud_observe_status(ctx); + return; + } + + DPS_INFO("Initializing cloud observer on cloud provisioning start-up"); + if (!dps_cloud_observer_load(&ctx->cloud_observer, cloud_ctx)) { + dps_manager_reprovision_and_restart(ctx); + return; + } + + // add on selection change callback, but store the original callback and data + // to be able to invoke it and restore it + oc_endpoint_addresses_on_selected_change_t cloud_on_selected_change = + oc_endpoint_addresses_get_on_selected_change(&cloud_ctx->store.ci_servers); + assert(cloud_on_selected_change.cb != + &dps_cloud_observer_on_cloud_server_change); + if (cloud_on_selected_change.cb != + &dps_cloud_observer_on_cloud_server_change) { + ctx->cloud_observer.original_on_selected_change = cloud_on_selected_change; + oc_endpoint_addresses_set_on_selected_change( + &cloud_ctx->store.ci_servers, dps_cloud_observer_on_cloud_server_change, + ctx); + } + + dps_cloud_observe_status(ctx); +} + +oc_event_callback_retval_t +dps_cloud_observer_reprovision_server_uuid_change_async(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx != NULL) { + oc_cloud_manager_stop_v1(cloud_ctx, false); + } + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + // remove credentials and ACLs + dps_set_ps_and_last_error(ctx, 0, + PLGD_DPS_GET_CREDENTIALS | + PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_GET_ACLS | + PLGD_DPS_HAS_ACLS | PLGD_DPS_CLOUD_STARTED, + ctx->last_error); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + + // go to next step -> get credentials + dps_provisioning_schedule_next_step(ctx); + return OC_EVENT_DONE; +} + +void +dps_cloud_observer_on_server_change(plgd_dps_context_t *ctx) +{ + if (ctx->cloud_observer.remaining_endpoint_changes == 0) { + DPS_DBG("No cloud server left to try"); + return; + } + + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("failed to obtain cloud context for device(%zu)", ctx->device); + goto reprovision; + } + const oc_endpoint_address_t *selected = + oc_cloud_selected_server_address(cloud_ctx); + if (selected == NULL) { + DPS_ERR("no cloud server selected"); + goto reprovision; + } + if (oc_string_is_equal(&ctx->cloud_observer.initial_endpoint_uri, + oc_endpoint_address_uri(selected))) { + DPS_INFO("initial cloud server reached, forcing reprovisioning"); + ctx->cloud_observer.remaining_endpoint_changes = 0; + goto reprovision; + } + + --ctx->cloud_observer.remaining_endpoint_changes; + ctx->cloud_observer.retry_count = 0; + ctx->cloud_observer.last_status = 0; + + if (dps_cloud_observer_copy_endpoint_uuid( + &ctx->cloud_observer, oc_endpoint_address_uuid(selected))) { + DPS_INFO( + "cloud server uuid has changed, reprovisioning credentials and ACLs"); + oc_remove_delayed_callback(ctx, dps_cloud_observe_status_async); + // execute outside of the on change callback + dps_reset_delayed_callback_ms( + ctx, dps_cloud_observer_reprovision_server_uuid_change_async, 0); + return; + } + + dps_cloud_observe_status(ctx); + return; + +reprovision: + oc_remove_delayed_callback(ctx, dps_cloud_observe_status_async); + dps_reset_delayed_callback(ctx, dps_manager_reprovision_and_restart_async, 0); +} + +bool +plgd_dps_set_cloud_observer_configuration(plgd_dps_context_t *ctx, + uint8_t max_retry_count, + uint8_t retry_interval_s) +{ + assert(ctx != NULL); + if (retry_interval_s == 0) { + DPS_ERR("configure cloud observer failed: invalid interval"); + return false; + } + ctx->cloud_observer.cfg.max_count = max_retry_count; + ctx->cloud_observer.cfg.interval_s = retry_interval_s; + DPS_DBG("cloud status observer cfg:"); + DPS_DBG("\tmax_count:%u", (unsigned)ctx->cloud_observer.cfg.max_count); + DPS_DBG("\tinterval_s:%u", (unsigned)ctx->cloud_observer.cfg.interval_s); + return true; +} + +plgd_cloud_status_observer_configuration_t +plgd_dps_get_cloud_observer_configuration(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->cloud_observer.cfg; +} + +void +dps_cloud_observe_status(plgd_dps_context_t *ctx) +{ + if (ctx->cloud_observer.cfg.max_count == 0) { + DPS_DBG("cloud status observer disabled"); + dps_cloud_observer_deinit(ctx); + return; + } + if (oc_has_delayed_callback(ctx, dps_cloud_observe_status_async, false)) { + DPS_DBG("cloud status observer already scheduled or running"); + return; + } + DPS_DBG("cloud status observer scheduled to start in %u seconds", + (unsigned)ctx->cloud_observer.cfg.interval_s); + dps_reset_delayed_callback(ctx, dps_cloud_observe_status_async, + ctx->cloud_observer.cfg.interval_s); +} + +static bool +dps_cloud_observer_update_status(plgd_cloud_status_observer_t *obs, + oc_cloud_status_t add_status) +{ + if ((obs->last_status & add_status) == 0) { + obs->last_status |= add_status; + obs->retry_count = 0; + return true; + } + return false; +} + +oc_event_callback_retval_t +dps_cloud_observe_status_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("Cannot obtain cloud context for device(%zu), force reprovisioning", + ctx->device); + goto provisioning_restart; + } + if (!oc_cloud_manager_is_started(cloud_ctx)) { + DPS_ERR("Cloud manager has not been started, force reprovisioning"); + goto provisioning_restart; + } + + if (ctx->cloud_observer.cfg.max_count == 0) { + DPS_INFO("Cloud status observer disabled"); + return OC_EVENT_DONE; + } + + if (oc_cloud_get_server_session_state(cloud_ctx) == OC_SESSION_DISCONNECTED) { + DPS_DBG("Cloud disconnected"); + ctx->cloud_observer.last_status = 0; + goto retry; + } + + uint8_t cloud_status = oc_cloud_get_status(cloud_ctx); + if ((cloud_status & OC_CLOUD_REGISTERED) != 0 && + dps_cloud_observer_update_status(&ctx->cloud_observer, + OC_CLOUD_REGISTERED)) { + DPS_DBG("Cloud registered"); + } + if ((cloud_status & OC_CLOUD_LOGGED_IN) != 0 && + dps_cloud_observer_update_status(&ctx->cloud_observer, + OC_CLOUD_LOGGED_IN)) { + DPS_DBG("Cloud logged in"); + } + if ((ctx->cloud_observer.last_status & + (OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN)) == + (OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN)) { + DPS_INFO("Cloud registered and logged in"); + dps_cloud_observer_deinit(ctx); + return OC_EVENT_DONE; + } + +retry: + DPS_DBG("Waiting for cloud (retry:%d)", (int)ctx->cloud_observer.retry_count); + ++ctx->cloud_observer.retry_count; + if (ctx->cloud_observer.retry_count >= ctx->cloud_observer.cfg.max_count) { + DPS_DBG("Cloud observer reached max retry count"); + if (ctx->cloud_observer.remaining_endpoint_changes == 0) { + DPS_DBG("No cloud server left to try, force reprovisioning"); + // rollback to the initial cloud server + oc_endpoint_addresses_select_by_uri( + &cloud_ctx->store.ci_servers, + oc_string_view2(&ctx->cloud_observer.initial_endpoint_uri)); + goto provisioning_restart; + } + DPS_DBG("Switching to the next cloud server"); + if (!oc_endpoint_addresses_select_next(&cloud_ctx->store.ci_servers)) { + DPS_DBG( + "Failed to switch to the next cloud server, force reprovisioning"); + goto provisioning_restart; + } + oc_cloud_manager_restart(cloud_ctx); + return OC_EVENT_DONE; + } + + return OC_EVENT_CONTINUE; + +provisioning_restart: + dps_cloud_observer_deinit(ctx); + dps_manager_reprovision_and_restart(ctx); + return OC_EVENT_DONE; +} + +typedef struct +{ + const oc_string_t *uri; + oc_uuid_t uuid; + bool found; +} dps_cloud_match_data_t; + +static bool +dps_cloud_match(oc_endpoint_address_t *eaddr, void *data) +{ + dps_cloud_match_data_t *match = (dps_cloud_match_data_t *)data; + const oc_string_t *ea_uri = oc_endpoint_address_uri(eaddr); + assert(ea_uri != NULL); + const oc_uuid_t *ea_uuid = oc_endpoint_address_uuid(eaddr); + assert(ea_uuid != NULL); + if (oc_string_is_equal(match->uri, ea_uri) && + oc_uuid_is_equal(match->uuid, *ea_uuid)) { + match->found = true; + return false; // stop iteration + } + return true; // continue iteration +} + +static bool +dps_cloud_contains_server(const oc_cloud_context_t *cloud_ctx, + const oc_string_t *uri, oc_uuid_t uuid) +{ + dps_cloud_match_data_t match = { + .uri = uri, + .uuid = uuid, + .found = false, + }; + oc_cloud_iterate_server_addresses(cloud_ctx, dps_cloud_match, &match); + return match.found; +} + +void +dps_cloud_add_servers(oc_cloud_context_t *cloud_ctx, const oc_rep_t *servers) +{ + for (const oc_rep_t *server = servers; server != NULL; + server = server->next) { + const oc_rep_t *rep = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, DPS_CLOUD_ENDPOINT_URI, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ENDPOINT_URI)); + if (rep == NULL) { + DPS_ERR("cloud server uri missing"); + continue; + } + const oc_string_t *uri = &rep->value.string; + + rep = oc_rep_get_by_type_and_key(server->value.object, OC_REP_STRING, + DPS_CLOUD_ENDPOINT_ID, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ENDPOINT_ID)); + oc_string_view_t idv = { 0 }; + oc_uuid_t uuid = { 0 }; + if (rep != NULL) { + idv = oc_string_view2(&rep->value.string); + if (oc_str_to_uuid_v1(idv.data, idv.length, &uuid) < 0) { + DPS_ERR("cloud server id(%s) invalid", idv.data); + continue; + } + } + + oc_string_view_t uriv = oc_string_view2(uri); + if (dps_cloud_contains_server(cloud_ctx, uri, uuid)) { + DPS_DBG("cloud server address already added (uri:%s, id=%s)", uriv.data, + idv.data != NULL ? idv.data : "NULL"); + continue; + } + if (oc_cloud_add_server_address(cloud_ctx, uriv.data, uriv.length, uuid) == + NULL) { + DPS_ERR("failed to add cloud server address (uri:%s, id=%s)", uriv.data, + idv.data != NULL ? idv.data : "NULL"); + continue; + } + DPS_DBG("cloud server address added (uri:%s, id=%s)", uriv.data, + idv.data != NULL ? idv.data : "NULL"); + } +} + +typedef struct +{ + const oc_endpoint_address_t *toIgnore; + uint8_t count; +} dps_cloud_count_data_t; + +static bool +dps_cloud_count_address(oc_endpoint_address_t *address, void *data) +{ + dps_cloud_count_data_t *ccd = (dps_cloud_count_data_t *)data; + if (address != ccd->toIgnore) { + ++ccd->count; + } + return true; +} + +uint8_t +dps_cloud_count_servers(const oc_cloud_context_t *cloud_ctx, + bool ignoreSelected) +{ + dps_cloud_count_data_t ccd = { + .toIgnore = + ignoreSelected ? oc_cloud_selected_server_address(cloud_ctx) : NULL, + .count = 0, + }; + oc_cloud_iterate_server_addresses(cloud_ctx, dps_cloud_count_address, &ccd); + return ccd.count; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_cloud_internal.h b/api/plgd/device-provisioning-client/plgd_dps_cloud_internal.h new file mode 100644 index 0000000000..22311e152a --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_cloud_internal.h @@ -0,0 +1,150 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef DPS_CLOUD_INTERNAL_H +#define DPS_CLOUD_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_cloud.h" +#include "oc_config.h" +#include "oc_rep.h" +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_compiler.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DPS_CLOUD_ACCESSTOKEN "at" +#define DPS_CLOUD_AUTHPROVIDER "apn" +#define DPS_CLOUD_CISERVER "cis" +#define DPS_CLOUD_SERVERID "sid" +#define DPS_CLOUD_ENDPOINTS "x.org.iotivity.servers" +#define DPS_CLOUD_ENDPOINT_ID "id" +#define DPS_CLOUD_ENDPOINT_URI "uri" + +/// @brief Check whether cloud has been started. +bool dps_cloud_is_started(size_t device); + +/** + * @brief Check cloud registration status. + * + * @param device index of the device + * @return true device has been successfully registered to cloud + * @return false otherwise + */ +bool dps_cloud_is_registered(size_t device); + +/** + * @brief Check cloud login status. + * + * @param device index of the device + * @return true device has been successfully logged in to cloud + * @return false otherwise + */ +bool dps_cloud_is_logged_in(size_t device); + +typedef struct +{ + oc_string_t initial_endpoint_uri; ///< the URI of the first endpoint when + ///< provisioning was started + oc_uuid_t last_endpoint_uuid; ///< uuid of the last tried endpoint + uint8_t + remaining_endpoint_changes; ///< remaining number of server changes allowed + ///< before full provisioning is triggered + uint8_t last_status; ///< latest observed cloud status + uint8_t retry_count; ///< current retry counter + + oc_endpoint_addresses_on_selected_change_t + original_on_selected_change; ///< original on_selected_change callback on + ///< the cloud context + plgd_cloud_status_observer_configuration_t cfg; +} plgd_cloud_status_observer_t; + +/// @brief Initialize the cloud observer +void dps_cloud_observer_init(plgd_cloud_status_observer_t *obs) OC_NONNULL(); + +/// @brief Load cloud observer values from the cloud context +bool dps_cloud_observer_load(plgd_cloud_status_observer_t *obs, + const oc_cloud_context_t *cloud_ctx) OC_NONNULL(); + +/// @brief Deinitialize the cloud observer +void dps_cloud_observer_deinit(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Copy the endpoint UUID to the cloud observer +bool dps_cloud_observer_copy_endpoint_uuid(plgd_cloud_status_observer_t *obs, + const oc_uuid_t *uuid) OC_NONNULL(1); + +/** + * @brief Callback to handle cloud provisioning start + * + * @param ctx device provisioning context (cannot be NULL) + * @param cloud_ctx cloud context (cannot be NULL) + */ +void dps_cloud_observer_on_provisioning_started(plgd_dps_context_t *ctx, + oc_cloud_context_t *cloud_ctx) + OC_NONNULL(); + +/** + * @brief Wait for cloud to register and log in. + * + * @param ctx device provisioning context (cannot be NULL) + */ +void dps_cloud_observe_status(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Callback to handle cloud server change + * + * @param ctx cloud status observer (cannot be NULL) + */ +void dps_cloud_observer_on_server_change(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Delayed callback to repeatedly check current cloud status. + * + * @param user_data user provided context (expected device provisioning context, + * cannot be NULL) + * @return OC_EVENT_DONE on success, or on failure with forced reprovisioning + * @return OC_EVENT_CONTINUE on failure with retry + */ +oc_event_callback_retval_t dps_cloud_observe_status_async(void *user_data) + OC_NONNULL(); + +/** @brief Delayed callback to reprovision from the credentials step */ +oc_event_callback_retval_t +dps_cloud_observer_reprovision_server_uuid_change_async(void *data); + +/** Add cloud servers from an oc_rep_t */ +void dps_cloud_add_servers(oc_cloud_context_t *cloud_ctx, + const oc_rep_t *servers) OC_NONNULL(1); + +/** Count the number of cloud servers */ +uint8_t dps_cloud_count_servers(const oc_cloud_context_t *cloud_ctx, + bool ignoreSelected) OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* DPS_CLOUD_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_context.c b/api/plgd/device-provisioning-client/plgd_dps_context.c new file mode 100644 index 0000000000..bf913d2286 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_context.c @@ -0,0 +1,296 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd/plgd_dps.h" +#include "plgd_dps_cloud_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_dhcp_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_resource_internal.h" +#include "plgd_dps_store_internal.h" + +#include "oc_endpoint.h" +#include "oc_session_events.h" +#include "util/oc_list.h" +#include "util/oc_memb.h" + +#include + +OC_LIST(g_dps_context_list); +OC_MEMB(g_dps_context_pool, plgd_dps_context_t, OC_MAX_NUM_DEVICES); + +static void +dps_on_endpoint_change(void *data) +{ + dps_store_dump_async((plgd_dps_context_t *)data); +} + +plgd_dps_context_t * +dps_context_alloc(void) +{ + return (plgd_dps_context_t *)oc_memb_alloc(&g_dps_context_pool); +} + +void +dps_context_free(plgd_dps_context_t *ctx) +{ + oc_memb_free(&g_dps_context_pool, ctx); +} + +void +dps_context_list_add(plgd_dps_context_t *ctx) +{ + oc_list_add(g_dps_context_list, ctx); +} + +void +dps_context_list_remove(plgd_dps_context_t *ctx) +{ + oc_list_remove(g_dps_context_list, ctx); +} + +bool +dps_context_list_is_empty(void) +{ + return oc_list_length(g_dps_context_list) == 0; +} + +void +dps_contexts_iterate(dps_contexts_iterate_fn_t fn, void *data) +{ + for (plgd_dps_context_t *ctx = oc_list_head(g_dps_context_list); ctx != NULL; + ctx = ctx->next) { + if (!fn(ctx, data)) { + return; + } + } +} + +void +dps_context_init(plgd_dps_context_t *ctx, size_t device) +{ + ctx->next = NULL; + ctx->device = device; + ctx->callbacks.on_status_change = NULL; + ctx->callbacks.on_status_change_data = NULL; + ctx->callbacks.on_cloud_status_change = NULL; + ctx->callbacks.on_cloud_status_change_data = NULL; + dps_store_init(&ctx->store, dps_on_endpoint_change, ctx); + ctx->status = 0; + ctx->transient_retry_count = 0; + dps_pki_init(&ctx->pki); + dps_cloud_observer_init(&ctx->cloud_observer); + ctx->endpoint = oc_new_endpoint(); + memset(ctx->endpoint, 0, sizeof(oc_endpoint_t)); + ctx->endpoint_state = OC_SESSION_DISCONNECTED; + dps_retry_init(&ctx->retry); + ctx->last_error = PLGD_DPS_OK; + ctx->conf = NULL; + ctx->manager_started = false; + ctx->force_reprovision = false; + ctx->skip_verify = false; + plgd_dps_dhcp_init(&ctx->dhcp); + memset(&ctx->certificate_fingerprint.data, 0, + sizeof(ctx->certificate_fingerprint.data)); + ctx->certificate_fingerprint.md_type = MBEDTLS_MD_NONE; +} + +void +dps_context_deinit(plgd_dps_context_t *ctx) +{ + oc_remove_delayed_callback(ctx, dps_store_dump_handler); + dps_cloud_observer_deinit(ctx); + dps_store_deinit(&ctx->store); + if (ctx->endpoint != NULL) { + oc_free_endpoint(ctx->endpoint); + ctx->endpoint = NULL; + } + oc_set_string(&ctx->certificate_fingerprint.data, NULL, 0); +} + +void +dps_context_reset(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + dps_cloud_observer_deinit(ctx); + dps_endpoint_disconnect(ctx); + dps_manager_stop(ctx); + dps_store_deinit(&ctx->store); + dps_store_init(&ctx->store, dps_on_endpoint_change, ctx); + ctx->last_error = 0; + ctx->status = 0; + ctx->transient_retry_count = 0; + oc_set_string(&ctx->certificate_fingerprint.data, NULL, 0); + ctx->certificate_fingerprint.md_type = MBEDTLS_MD_NONE; + dps_store_dump_async(ctx); +} + +plgd_dps_context_t * +plgd_dps_get_context(size_t device) +{ + plgd_dps_context_t *ctx = oc_list_head(g_dps_context_list); + while (ctx != NULL && ctx->device != device) { + ctx = ctx->next; + } + return ctx; +} + +int +plgd_dps_on_factory_reset(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + oc_cloud_set_retry_timeouts(NULL, 0); +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + dps_context_reset(ctx); + return 0; +} + +void +plgd_dps_set_skip_verify(plgd_dps_context_t *ctx, bool skip_verify) +{ + DPS_DBG("DPS Service skip_verify=%d", (int)skip_verify); + assert(ctx != NULL); + ctx->skip_verify = skip_verify; +} + +bool +plgd_dps_get_skip_verify(const plgd_dps_context_t *ctx) +{ + return ctx->skip_verify; +} + +void +plgd_dps_set_manager_callbacks(plgd_dps_context_t *ctx, + plgd_dps_manager_callbacks_t callbacks) +{ + assert(ctx != NULL); + ctx->callbacks.on_status_change = callbacks.on_status_change; + ctx->callbacks.on_status_change_data = callbacks.on_status_change_data; + ctx->callbacks.on_cloud_status_change = callbacks.on_cloud_status_change; + ctx->callbacks.on_cloud_status_change_data = + callbacks.on_cloud_status_change_data; +} + +void +plgd_dps_force_reprovision(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + DPS_DBG("DPS force reprovision"); + ctx->force_reprovision = true; +} + +bool +plgd_dps_has_forced_reprovision(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->force_reprovision; +} + +bool +dps_set_has_been_provisioned_since_reset(plgd_dps_context_t *ctx, bool dump) +{ + assert(ctx != NULL); + bool has_been_provisioned_since_reset = true; + bool changed = ctx->store.has_been_provisioned_since_reset != + has_been_provisioned_since_reset; + if (!changed) { + return false; + } + ctx->store.has_been_provisioned_since_reset = + has_been_provisioned_since_reset; + if (dump) { + dps_store_dump_async(ctx); + } + return true; +} + +bool +plgd_dps_has_been_provisioned_since_reset(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->store.has_been_provisioned_since_reset; +} + +uint32_t +plgd_dps_get_provision_status(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->status; +} + +bool +dps_set_last_error(plgd_dps_context_t *ctx, plgd_dps_error_t error) +{ + assert(ctx != NULL); + bool changed = error != ctx->last_error; + if (changed) { + ctx->last_error = error; + dps_notify_observers(ctx); + } + return changed; +} + +plgd_dps_error_t +plgd_dps_get_last_error(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->last_error; +} + +bool +dps_set_ps_and_last_error(plgd_dps_context_t *ctx, uint32_t add_flags, + uint32_t remove_flags, plgd_dps_error_t error) +{ + assert(ctx != NULL); + uint32_t new_status = ctx->status; + new_status &= ~remove_flags; + new_status |= add_flags; + bool changed = (ctx->status != new_status) || (error != ctx->last_error); + if (changed) { + ctx->status = new_status; + ctx->last_error = error; + dps_notify_observers(ctx); + } + return changed; +} + +size_t +plgd_dps_get_device(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->device; +} + +void +plgd_dps_set_configuration_resource(plgd_dps_context_t *ctx, + bool create_resource) +{ + assert(ctx != NULL); + DPS_DBG("DPS Service create_resource=%d", (int)create_resource); + if (!create_resource) { + dps_delete_dpsconf_resource(ctx->conf); + ctx->conf = NULL; + return; + } + if (ctx->conf == NULL) { + ctx->conf = dps_create_dpsconf_resource(ctx->device); + } +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_context_internal.h b/api/plgd/device-provisioning-client/plgd_dps_context_internal.h new file mode 100644 index 0000000000..914ccb999d --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_context_internal.h @@ -0,0 +1,160 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_CONTEXT_INTERNAL_H +#define PLGD_DPS_CONTEXT_INTERNAL_H + +#include "plgd_dps_cloud_internal.h" +#include "plgd_dps_pki_internal.h" +#include "plgd_dps_retry_internal.h" +#include "plgd/plgd_dps.h" // plgd_dps_context_t, plgd_dps_manager_callbacks_t + +#include "oc_api.h" +#include "util/oc_compiler.h" +#include "util/oc_endpoint_address_internal.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + oc_endpoint_addresses_t endpoints; ///< list of OCF endpoints + oc_string_t owner; + bool has_been_provisioned_since_reset; ///< true if the device has been + ///< provisioned after factory reset +} plgd_dps_store_t; + +typedef struct +{ + uint8_t option_code_dps_endpoint; + uint8_t option_code_dps_certificate_fingerprint; + uint8_t option_code_dps_certificate_fingerprint_md_type; +} plgd_dps_dhcp_t; + +typedef struct +{ + oc_string_t data; ///< fingerprint of the DPS server certificate. (eg SHA256) + mbedtls_md_type_t + md_type; ///< Hash algorithm used to calculate the fingerprint. +} plgd_dps_certificate_fingerprint_t; + +struct plgd_dps_context_t +{ + struct plgd_dps_context_t *next; + + oc_resource_t *conf; ///< configuration resource + size_t device; + plgd_dps_manager_callbacks_t callbacks; + plgd_dps_store_t store; ///< data stored in oc_storage + oc_endpoint_t *endpoint; ///< DPS service endpoint + oc_session_state_t endpoint_state; ///< DPS service endpoint state + plgd_dps_retry_t retry; ///< retry configuration and current counter + plgd_dps_error_t last_error; + plgd_dps_certificate_fingerprint_t + certificate_fingerprint; ///< fingerprint of the DPS server certificate or + ///< intermediate certificate. + uint32_t status; ///< provisioning status - bitmask of provisioning steps + dps_pki_configuration_t pki; ///< pki configuration + plgd_cloud_status_observer_t + cloud_observer; ///< observer for changes of cloud status + plgd_dps_dhcp_t dhcp; ///< DHCP configuration + uint8_t + transient_retry_count; ///< count of consecutive transient failures of the + ///< current provisioning step, if a limit is reached + ///< then full reprovisioning of the device is forced + bool manager_started; ///< provisioning manager has been started + bool force_reprovision; ///< force full reprovision on (re)start - refresh + ///< creds, acls, cloud from DPS service + bool skip_verify; ///< insecure skip verify controls whether a dps client + ///< verifies the device provision service's certificate + ///< chain against trust anchor in the device. + bool closing_insecure_peer; ///< a TLS peer with disabled time verification + ///< was opened and scheduled to close, we must + ///< wait for the scheduled asynchronous close to + ///< finish before continuing +}; + +/// @brief Allocate context +plgd_dps_context_t *dps_context_alloc(void); + +/// @brief Deallocate context +void dps_context_free(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Add to global lists of contexts +void dps_context_list_add(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Remove from global lists of contexts +void dps_context_list_remove(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Check if the global lists of contexts is empty +bool dps_context_list_is_empty(void); + +/** + * @brief Callback invoked for each iterated DPS context. + * + * @param ctx context to iterate + * @param data custom user data provided to the iteration function + * @return true to continue iteration + * @return false to stop iteration + */ +typedef bool (*dps_contexts_iterate_fn_t)(plgd_dps_context_t *ctx, void *data) + OC_NONNULL(1); + +/** Iterate the list of DPS contexts. */ +void dps_contexts_iterate(dps_contexts_iterate_fn_t fn, void *data) + OC_NONNULL(1); + +/// @brief Initialize device context. +void dps_context_init(plgd_dps_context_t *ctx, size_t device) OC_NONNULL(); + +/// @brief Deinitialize device context. +void dps_context_deinit(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Clear device context. +void dps_context_reset(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Sets flag to indicate that the device has been provisioned after DPS + * reset. + * @param ctx dps context (cannot be NULL) + * @param dump dump the value to persistent storage + * @return true if the value has changed + * @return false otherwise + */ +bool dps_set_has_been_provisioned_since_reset(plgd_dps_context_t *ctx, + bool dump) OC_NONNULL(); + +/// @brief Set last error and notify observers if it has changed. +bool dps_set_last_error(plgd_dps_context_t *ctx, plgd_dps_error_t error) + OC_NONNULL(); + +/// @brief Set provisioning status flags, last error and notify observers if it +/// has changed. +bool dps_set_ps_and_last_error(plgd_dps_context_t *ctx, uint32_t add_flags, + uint32_t remove_flags, plgd_dps_error_t error) + OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_CONTEXT_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_dhcp.c b/api/plgd/device-provisioning-client/plgd_dps_dhcp.c new file mode 100644 index 0000000000..564d798f62 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_dhcp.c @@ -0,0 +1,352 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_internal.h" +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_dhcp_internal.h" +#include "plgd_dps_endpoints_internal.h" +#include "plgd_dps_log_internal.h" // DPS_DBG +#include "plgd_dps_provision_internal.h" +#include "plgd/plgd_dps.h" // plgd_dps_context_t + +#include +#include + +// define default codes for vendor encapsulated options +enum { + DHCP_OPTION_CODE_DPS_ENDPOINT = 200, + DHCP_OPTION_CODE_DPS_CERTIFICATE_FINGERPRINT = 201, + DHCP_OPTION_CODE_DPS_CERTIFICATE_FINGERPRINT_MD_TYPE = 202, +}; + +// NOLINTNEXTLINE(modernize-*) +#define MAX_DHCP_VENDOR_ENCAPSULATED_OPTION_BYTE_SIZE (255) + +void +plgd_dps_dhcp_init(plgd_dps_dhcp_t *dhcp) +{ + assert(dhcp); + dhcp->option_code_dps_endpoint = DHCP_OPTION_CODE_DPS_ENDPOINT; + dhcp->option_code_dps_certificate_fingerprint = + DHCP_OPTION_CODE_DPS_CERTIFICATE_FINGERPRINT; + dhcp->option_code_dps_certificate_fingerprint_md_type = + DHCP_OPTION_CODE_DPS_CERTIFICATE_FINGERPRINT_MD_TYPE; +} + +void +plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_endpoint( + plgd_dps_context_t *ctx, uint8_t code) +{ + assert(ctx); + ctx->dhcp.option_code_dps_endpoint = code; +} + +uint8_t +plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_endpoint( + const plgd_dps_context_t *ctx) +{ + assert(ctx); + return ctx->dhcp.option_code_dps_endpoint; +} + +void +plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_certificate_fingerprint( + plgd_dps_context_t *ctx, uint8_t code) +{ + assert(ctx); + ctx->dhcp.option_code_dps_certificate_fingerprint = code; +} + +uint8_t +plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_certificate_fingerprint( + const plgd_dps_context_t *ctx) +{ + assert(ctx); + return ctx->dhcp.option_code_dps_certificate_fingerprint; +} + +static int +hexchar_to_decimal(char hex) +{ + if (hex >= '0' && hex <= '9') { + return hex - '0'; + } + if (hex >= 'A' && hex <= 'F') { + return 10 + (hex - 'A'); // NOLINT(readability-magic-numbers) + } + if (hex >= 'a' && hex <= 'f') { + return 10 + (hex - 'a'); // NOLINT(readability-magic-numbers) + } + return -1; +} + +static ssize_t +hex_to_value(const char *data, size_t data_size, uint8_t *value) +{ + assert(data); + assert(data_size > 0); + assert(value); + *value = 0; + // number of bytes used - it can be 1 or 2 or 3 + ssize_t used = 0; + for (size_t i = 0; i < data_size; i++) { + char hexc = data[i]; + if (hexc == ':') { + // end of hex value + used += 1; + return used; + } + int val = hexchar_to_decimal(hexc); + if (val == -1) { + DPS_ERR("invalid character in vendor encapsulated options %c", hexc); + return -1; + } + if (i == 2) { + // we have 2 chars, so we are done + return used; + } + if (i == 1) { + // we have 1 char, so shift it about 4 bits + *value = (uint8_t)(*value << 4); + } + *value |= val; + used += 1; + } + return used; +} + +ssize_t +plgd_dps_hex_string_to_bytes(const char *isc_dhcp_vendor_encapsulated_options, + size_t isc_dhcp_vendor_encapsulated_options_size, + uint8_t *buffer, size_t buffer_size) +{ + assert(isc_dhcp_vendor_encapsulated_options); + assert(isc_dhcp_vendor_encapsulated_options_size > 0); + size_t needed = 0; + if (buffer && buffer_size > 0) { + memset(buffer, 0, buffer_size); + } + for (size_t i = 0; i < isc_dhcp_vendor_encapsulated_options_size;) { + uint8_t val = 0; + ssize_t used = + hex_to_value(isc_dhcp_vendor_encapsulated_options + i, + isc_dhcp_vendor_encapsulated_options_size - i, &val); + if (used < 0) { + return -1; + } + if (buffer && (needed < buffer_size)) { + buffer[needed] = val; + } + needed++; + i += used; + } + return (ssize_t)needed; +} + +typedef bool plgd_dps_dhcp_set_option_cbk_t(uint8_t option_code, + const uint8_t *data, + size_t data_size, void *user_data); + +static bool +parse_option(uint8_t option_code, const uint8_t *data, size_t data_size, + size_t *used, plgd_dps_dhcp_set_option_cbk_t set_option_cbk, + void *user_data) +{ + assert(data); + assert(data_size > 0); + assert(used); + if (data_size < 1) { + return false; + } + size_t size = data[0]; + if (size > data_size - 1) { + return false; + } + if (set_option_cbk != NULL && + !set_option_cbk(option_code, data + 1, size, user_data)) { + return false; + } + *used = size + 1; + return true; +} + +static bool +plgd_dps_dhcp_set_option_cbk(uint8_t option_code, const uint8_t *data, + size_t data_size, void *user_data) +{ + assert(user_data != NULL); + dhcp_parse_data_t *dpd = (dhcp_parse_data_t *)user_data; + if (dpd->dhcp == NULL) { + return false; + } + if (data_size == 0) { + return false; + } + if (option_code == dpd->dhcp->option_code_dps_endpoint) { + dpd->endpoint = data; + dpd->endpoint_size = data_size; + return true; + } + if (option_code == dpd->dhcp->option_code_dps_certificate_fingerprint) { + dpd->certificate_fingerprint = data; + dpd->certificate_fingerprint_size = data_size; + return true; + } + if (option_code == + dpd->dhcp->option_code_dps_certificate_fingerprint_md_type) { + dpd->certificate_fingerprint_md_type = data; + dpd->certificate_fingerprint_md_type_size = data_size; + return true; + } +#if DPS_DBG_IS_ENABLED + // GCOVR_EXCL_START + char buf[256]; // NOLINT(readability-magic-numbers) + size_t len = data_size > sizeof(buf) - 1 ? sizeof(buf) - 1 : data_size; + memcpy(buf, data, len); + buf[len] = 0; + DPS_DBG("Unknown option code %d with data: %s", option_code, buf); +// GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + return false; +} + +bool +dps_dhcp_parse_vendor_encapsulated_options( + dhcp_parse_data_t *dhcp_parse_data, + const uint8_t *vendor_encapsulated_options, + size_t vendor_encapsulated_options_size) +{ + assert(dhcp_parse_data); + assert(vendor_encapsulated_options); + assert(vendor_encapsulated_options_size > 0); + for (size_t i = 0; i + 1 < vendor_encapsulated_options_size;) { + size_t used = 0; + if (!parse_option(vendor_encapsulated_options[i], + vendor_encapsulated_options + i + 1, + vendor_encapsulated_options_size - (i + 1), &used, + plgd_dps_dhcp_set_option_cbk, dhcp_parse_data)) { + return false; + } + i += used + 1; + } + return true; +} + +static bool +dhcp_set_endpoint(plgd_dps_context_t *ctx, const dhcp_parse_data_t *cbk_data) +{ + assert(ctx); + assert(cbk_data); + if (oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view((const char *)cbk_data->endpoint, + cbk_data->endpoint_size))) { + DPS_DBG("dps_dhcp_parse_vendor_encapsulated_options: endpoint not changed"); + return false; + } + char buffer[MAX_DHCP_VENDOR_ENCAPSULATED_OPTION_BYTE_SIZE + 1]; + size_t len = 0; + if (cbk_data->endpoint) { + len = sizeof(buffer) - 1 > cbk_data->endpoint_size ? cbk_data->endpoint_size + : sizeof(buffer) - 1; + memcpy(buffer, cbk_data->endpoint, len); + buffer[len] = '\0'; + } else { + assert(len == 0); + buffer[0] = '\0'; + } + dps_set_endpoint(ctx, buffer, len, /*notify*/ true); + return true; +} + +static bool +dhcp_parse_md_type(const dhcp_parse_data_t *cbk_data, + mbedtls_md_type_t *md_type) +{ + assert(cbk_data); + assert(md_type); + mbedtls_md_type_t mdt = MBEDTLS_MD_NONE; + if (cbk_data->certificate_fingerprint_md_type != NULL && + cbk_data->certificate_fingerprint_md_type_size > 0) { + char buffer[MAX_DHCP_VENDOR_ENCAPSULATED_OPTION_BYTE_SIZE + 1]; + size_t len = + sizeof(buffer) - 1 > cbk_data->certificate_fingerprint_md_type_size + ? cbk_data->certificate_fingerprint_md_type_size + : sizeof(buffer) - 1; + memcpy(buffer, cbk_data->certificate_fingerprint_md_type, len); + buffer[len] = '\0'; + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_string(buffer); + if (md_info == NULL) { + DPS_ERR("dps_dhcp_parse_vendor_encapsulated_options: unknown fingerprint " + "md type: %s", + buffer); + return false; + } + mdt = mbedtls_md_get_type(md_info); + } + *md_type = mdt; + return true; +} + +plgd_dps_dhcp_set_values_t +plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + plgd_dps_context_t *ctx, const uint8_t *vendor_encapsulated_options, + size_t vendor_encapsulated_options_size) +{ + assert(ctx); + assert(vendor_encapsulated_options); + assert(vendor_encapsulated_options_size > 0); + dhcp_parse_data_t cbk_data = { 0 }; + cbk_data.dhcp = &ctx->dhcp; + if (!dps_dhcp_parse_vendor_encapsulated_options( + &cbk_data, vendor_encapsulated_options, + vendor_encapsulated_options_size)) { + return PLGD_DPS_DHCP_SET_VALUES_ERROR; + } + + mbedtls_md_type_t md_type; + if (!dhcp_parse_md_type(&cbk_data, &md_type)) { + return PLGD_DPS_DHCP_SET_VALUES_ERROR; + } + + if (ctx->certificate_fingerprint.md_type == md_type && + oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view((const char *)cbk_data.endpoint, + cbk_data.endpoint_size)) && + dps_is_equal_string_len(ctx->certificate_fingerprint.data, + (const char *)cbk_data.certificate_fingerprint, + cbk_data.certificate_fingerprint_size)) { + DPS_DBG("dps_dhcp_parse_vendor_encapsulated_options: endpoint and " + "certificate_fingerprint are the same"); + return PLGD_DPS_DHCP_SET_VALUES_NOT_CHANGED; + } + if (!plgd_dps_set_certificate_fingerprint( + ctx, md_type, cbk_data.certificate_fingerprint, + cbk_data.certificate_fingerprint_size)) { + return PLGD_DPS_DHCP_SET_VALUES_ERROR; + } + plgd_dps_dhcp_set_values_t ret = PLGD_DPS_DHCP_SET_VALUES_UPDATED; + if (dhcp_set_endpoint(ctx, &cbk_data)) { + ret = PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION; + } + if (!dps_is_provisioned(ctx)) { + DPS_DBG( + "dps_dhcp_parse_vendor_encapsulated_options: not still not provisioned"); + ret = PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION; + } + return ret; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h b/api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h new file mode 100644 index 0000000000..361957447a --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h @@ -0,0 +1,87 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_DHCP_INTERNAL_H +#define PLGD_DPS_DHCP_INTERNAL_H + +#include "plgd_dps_context_internal.h" + +#include "util/oc_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize the DHCP configuration. + * + * @param[out] dhcp pointer to DHCP configuration to initialize (cannot be NULL) + */ +void plgd_dps_dhcp_init(plgd_dps_dhcp_t *dhcp) OC_NONNULL(); + +typedef struct +{ + const plgd_dps_dhcp_t *dhcp; ///< pointer to the DHCP configuration + const uint8_t *endpoint; ///< offset from DHCP vendor encapsulated options to + ///< the DPS endpoint + size_t + endpoint_size; ///< parsed from DHCP vendor encapsulated options DPS + ///< endpoint size (without the terminating null character) + const uint8_t + *certificate_fingerprint; ///< offset from DHCP vendor encapsulated options + ///< to the DPS certificate fingerprint + size_t + certificate_fingerprint_size; ///< parsed from DHCP vendor encapsulated + ///< options DPS certificate fingerprint size + const uint8_t + *certificate_fingerprint_md_type; ///< offset from DHCP vendor encapsulated + ///< options to the DPS certificate + ///< fingerprint MD type + size_t certificate_fingerprint_md_type_size; ///< parsed from DHCP vendor + ///< encapsulated options DPS + ///< certificate fingerprint MD + ///< type size +} dhcp_parse_data_t; + +/** + * @brief Parse the DPS configuration from the DHCP vendor encapsulated options. + * + * The parsed data are stored in the dhcp_parse_data_t structure. The data + * are not copied. + * + * @param[out] dhcp_parse_data pointer to the structure where the parsed data + * are stored (cannot be NULL) + * @param[in] vendor_encapsulated_options pointer to the DHCP vendor + * encapsulated options (cannot be NULL) + * @param[in] vendor_encapsulated_options_size size of the DHCP vendor + * encapsulated options + * @return true if the parsing was successful + */ +bool dps_dhcp_parse_vendor_encapsulated_options( + dhcp_parse_data_t *dhcp_parse_data, + const uint8_t *vendor_encapsulated_options, + size_t vendor_encapsulated_options_size) OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_DHCP_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_endpoint.c b/api/plgd/device-provisioning-client/plgd_dps_endpoint.c new file mode 100644 index 0000000000..7addf018fa --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoint.c @@ -0,0 +1,206 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_log_internal.h" // DPS_DBG, DPS_ERR +#include "plgd_dps_security_internal.h" +#include "plgd_dps_verify_certificate_internal.h" + +#include "api/oc_endpoint_internal.h" +#include "api/oc_tcp_internal.h" // oc_tcp_get_new_session_id, ... +#include "oc_api.h" // oc_close_session +#include "oc_endpoint.h" // oc_endpoint_t, oc_string_to_endpoint +#include "port/oc_connectivity.h" // oc_dns_clear_cache +#include "security/oc_tls_internal.h" // oc_tls_peer_t, oc_tls_select_cloud_ciphersuite, ... + +#include "mbedtls/ssl.h" // MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_IS_CLIENT + +#include +#include // PRId64 + +int +dps_endpoint_init(plgd_dps_context_t *ctx, const oc_string_t *ep_str) +{ + assert(ctx != NULL); + int ret = 0; + if ((ctx->endpoint != NULL) && dps_endpoint_is_empty(ctx->endpoint)) { + ret = oc_string_to_endpoint(ep_str, ctx->endpoint, NULL); + if (ret != 0) { + memset(ctx->endpoint, 0, sizeof(oc_endpoint_t)); + } +#ifdef OC_DNS_CACHE + oc_dns_clear_cache(); +#endif /* OC_DNS_CACHE */ + } + return ret; +} + +#if DPS_DBG_IS_ENABLED + +void +dps_endpoint_print_peers(const oc_endpoint_t *endpoint) +{ + // GCOVR_EXCL_START + oc_tls_peer_t *peer = oc_tls_get_peer(endpoint); + DPS_DBG("peers for endpoint:"); + if (peer == NULL) { + DPS_DBG("\tno peers were found"); + return; + } + + while (peer != NULL) { +#define ENDPOINT_STR_LEN 256 + char ep_str[ENDPOINT_STR_LEN] = { 0 }; +#undef ENDPOINT_STR_LEN + bool valid = + dps_endpoint_log_string(&peer->endpoint, ep_str, sizeof(ep_str)); + int is_server = valid && peer->role == MBEDTLS_SSL_IS_SERVER ? 1 : 0; + DPS_DBG("\t%s, server: %d", valid ? ep_str : "NULL", is_server); + peer = peer->next; + } + // GCOVR_EXCL_STOP +} +#endif /* DPS_DBG_IS_ENABLED */ + +oc_tls_peer_t * +dps_endpoint_add_peer(const oc_endpoint_t *endpoint) +{ +#if DPS_DBG_IS_ENABLED +// GCOVR_EXCL_START +#define ENDPOINT_STR_LEN 256 + char ep_str[ENDPOINT_STR_LEN] = { 0 }; +#undef ENDPOINT_STR_LEN + bool valid = dps_endpoint_log_string(endpoint, ep_str, sizeof(ep_str)); + DPS_DBG("add peer %s", valid ? ep_str : "NULL"); +// GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + + oc_tls_select_cloud_ciphersuite(); + // force to use mfg cert + oc_tls_select_identity_cert_chain( + PLGD_DPS_DISABLE_SELECT_IDENTITY_CERT_CHAIN); + + dps_verify_certificate_data_t *vcd = dps_verify_certificate_data_new( + oc_tls_peer_pki_default_verification_params()); + if (vcd == NULL) { + return NULL; + } + oc_tls_new_peer_params_t new_peer = { + .endpoint = endpoint, + .role = MBEDTLS_SSL_IS_CLIENT, + .user_data = { + .data = vcd, + .free = dps_verify_certificate_data_free, + }, + .verify_certificate = dps_verify_certificate, + }; + oc_tls_peer_t *peer = oc_tls_add_new_peer(new_peer); + if (peer == NULL) { + DPS_ERR("cannot add endpoint peer: oc_tls_add_new_peer peer failed"); + dps_verify_certificate_data_free(vcd); + return NULL; + } +#if DPS_DBG_IS_ENABLED + dps_endpoint_print_peers(endpoint); +#endif /* DPS_DBG_IS_ENABLED */ + + return peer; +} + +void +dps_endpoint_close(const oc_endpoint_t *endpoint) +{ + assert(endpoint != NULL); + if (!dps_endpoint_is_empty(endpoint)) { + DPS_DBG("dps_endpoint_close"); + oc_close_session(endpoint); + } +} + +void +dps_endpoint_disconnect(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + DPS_DBG("dps_endpoint_disconnect"); + if (ctx->endpoint != NULL) { + dps_endpoint_close(ctx->endpoint); + memset(ctx->endpoint, 0, sizeof(oc_endpoint_t)); + } + ctx->endpoint_state = OC_SESSION_DISCONNECTED; +} + +bool +dps_endpoint_is_empty(const oc_endpoint_t *endpoint) +{ + assert(endpoint != NULL); + return oc_endpoint_is_empty(endpoint); +} + +bool +dps_endpoint_log_string(const oc_endpoint_t *endpoint, char *buffer, + size_t buffer_size) +{ + oc_string_t ep_str; + memset(&ep_str, 0, sizeof(oc_string_t)); + if (oc_endpoint_to_string(endpoint, &ep_str) != 0) { + return false; + } + size_t ep_str_len = oc_string_len_unsafe(ep_str); + if ((ep_str_len == 0) || (ep_str_len >= buffer_size)) { + oc_free_string(&ep_str); + return false; + } + +#if DPS_DBG_IS_ENABLED + // include session_id in debug + int64_t session_id = oc_endpoint_session_id(endpoint); + int len = + snprintf(buffer, buffer_size, "endpoint(addr=%s, session_id=%" PRId64 ")", + oc_string(ep_str), session_id); +#else /* !DPS_DBG_IS_ENABLED */ + int len = snprintf(buffer, buffer_size, "endpoint(%s)", oc_string(ep_str)); +#endif /* DPS_DBG_IS_ENABLED */ + if (len < 0 || (size_t)len >= buffer_size) { + oc_free_string(&ep_str); + return false; + } + + oc_free_string(&ep_str); + return true; +} + +void +dps_setup_tls(const plgd_dps_context_t *ctx) +{ + if (oc_tls_get_peer(ctx->endpoint) != NULL) { + return; + } + if (dps_endpoint_add_peer(ctx->endpoint) == NULL) { + DPS_ERR("add peer failed"); + return; + } + DPS_DBG("setup tls with cloud cipher suite and manufacturer certificates"); +} + +void +dps_reset_tls(void) +{ + oc_tls_reset_ciphersuite(); + oc_tls_select_identity_cert_chain(PLGD_DPS_ENABLE_SELECT_IDENTITY_CERT_CHAIN); + DPS_DBG("reset tls to use default cipher suite and default certificates"); +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h b/api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h new file mode 100644 index 0000000000..c7cf7012dd --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h @@ -0,0 +1,115 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_ENDPOINT_INTERNAL_H +#define PLGD_DPS_ENDPOINT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "plgd_dps_context_internal.h" + +#include "oc_config.h" +#include "oc_endpoint.h" +#include "security/oc_tls_internal.h" // oc_tls_peer_t +#include "util/oc_compiler.h" + +#include +#include + +enum { + PLGD_DPS_ENABLE_SELECT_IDENTITY_CERT_CHAIN = -1, + PLGD_DPS_DISABLE_SELECT_IDENTITY_CERT_CHAIN = -2, +}; + +/** + * @brief Initialize endpoint field parsing the provided string value. + * + * Function checks if ctx->endpoint field is empty and if it is, it initializes + * by parsing the provided string value. + * + * @param ctx device context (cannot be NULL) + * @param ep_str endpoint in string format + * @return int 0 if endpoint is already set or has been successfully parsed + * <0 on error + */ +OC_NO_DISCARD_RETURN +int dps_endpoint_init(plgd_dps_context_t *ctx, const oc_string_t *ep_str) + OC_NONNULL(1); + +/** + * @brief Add endpoint to trusted peers without authorization. + * + * @param endpoint endpoint to add as peer + * @return int 0 peer was created/found and added to trusted peers + * -1 on error + */ +int dps_endpoint_add_unauthorized_peer(const oc_endpoint_t *endpoint); + +/// @brief Close endpoint connection. +void dps_endpoint_close(const oc_endpoint_t *endpoint); + +/// @brief Close endpoint connection, reset endpoint and set disconnected state. +void dps_endpoint_disconnect(plgd_dps_context_t *ctx); + +/// @brief Check if endpoint is set to empty value. +bool dps_endpoint_is_empty(const oc_endpoint_t *endpoint); + +/** + * @brief Write endpoint address and session id ("endpoint(addr=%s, + * session_id=%d)") to buffer. + * + * @param endpoint endpoint + * @param[out] buffer output buffer + * @param buffer_size size of the output buffer + * @return true on success + * @return false on failure + */ +bool dps_endpoint_log_string(const oc_endpoint_t *endpoint, char *buffer, + size_t buffer_size); + +/** + * @brief setup TLS for establishing a secure connection to DPS + * + * @param ctx dps context (cannot be NULL) + */ +void dps_setup_tls(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Reset TLS configuration for establishing a secure connection. +void dps_reset_tls(void); + +/** + * @brief Add endpoint to with configured mbedtls TLS. + * + * @param endpoint endpoint to add as peer + * @return oc_tls_peer_t* pointer to peer or NULL on error + */ +oc_tls_peer_t *dps_endpoint_add_peer(const oc_endpoint_t *endpoint) + OC_NONNULL(); + +#if DPS_DBG_IS_ENABLED +/// @brief Print peers. +void dps_endpoint_print_peers(const oc_endpoint_t *endpoint); +#endif /* DPS_DBG_IS_ENABLED */ + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_ENDPOINT_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_endpoints.c b/api/plgd/device-provisioning-client/plgd_dps_endpoints.c new file mode 100644 index 0000000000..b25b3f56af --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoints.c @@ -0,0 +1,167 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_endpoints_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_store_internal.h" + +#include "oc_helpers.h" +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_memb.h" + +#include +#include + +OC_MEMB(g_dps_endpoint_address_pool, oc_endpoint_address_t, + 2 * OC_MAX_NUM_DEVICES); + +int +dps_set_endpoint(plgd_dps_context_t *ctx, const char *endpoint, + size_t endpoint_len, bool notify) +{ + assert(ctx != NULL); + assert(endpoint != NULL); + DPS_DBG("DPS Service endpoint=%s", endpoint); + + if (oc_endpoint_addresses_size(&ctx->store.endpoints) == 1 && + oc_endpoint_addresses_contains(&ctx->store.endpoints, + oc_string_view(endpoint, endpoint_len))) { + DPS_DBG("DPS Service endpoint already set"); + return DPS_ENDPOINT_NOT_CHANGED; + } + dps_endpoint_disconnect(ctx); + if (!oc_endpoint_addresses_reinit( + &ctx->store.endpoints, + oc_endpoint_address_make_view_with_name( + oc_string_view(endpoint, endpoint_len), OC_STRING_VIEW_NULL))) { + DPS_ERR("DPS Service failed to set endpoint"); + return -1; + } + if (notify) { + dps_notify_observers(ctx); + } + DPS_INFO("DPS Service endpoint set to %s", endpoint); + return DPS_ENDPOINT_CHANGED; +} + +bool +dps_set_endpoints(plgd_dps_context_t *ctx, const oc_string_t *selected_endpoint, + const oc_string_t *selected_endpoint_name, + const oc_rep_t *endpoints) +{ + assert(ctx != NULL); + bool changed = !oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view2(selected_endpoint)); + if (!dps_store_set_endpoints(&ctx->store, selected_endpoint, + selected_endpoint_name, endpoints)) { + DPS_ERR("DPS Service failed to set endpoints"); + return false; + } + if (changed) { + dps_endpoint_disconnect(ctx); + } + return true; +} + +void +plgd_dps_set_endpoint(plgd_dps_context_t *ctx, const char *endpoint) +{ + dps_set_endpoint(ctx, endpoint, strlen(endpoint), /* notify */ true); +} + +int +plgd_dps_get_endpoint(const plgd_dps_context_t *ctx, char *buffer, + size_t buffer_size) +{ + assert(ctx != NULL); + assert(buffer != NULL); + + const oc_string_t *ep_addr = + oc_endpoint_addresses_selected_uri(&ctx->store.endpoints); + if (ep_addr == NULL) { + DPS_DBG("No endpoint set"); + return 0; + } + if (buffer_size < ep_addr->size) { + DPS_ERR( + "cannot copy endpoint to buffer: buffer too small (minimal size=%zu)", + ep_addr->size); + return -1; + } + memcpy(buffer, ep_addr->ptr, ep_addr->size); + return (int)ep_addr->size; +} + +bool +plgd_dps_endpoint_is_empty(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return oc_endpoint_addresses_selected(&ctx->store.endpoints) == NULL; +} + +bool +dps_endpoints_init(oc_endpoint_addresses_t *eas, + on_selected_endpoint_address_change_fn_t on_selected_change, + void *on_selected_change_data) +{ + return oc_endpoint_addresses_init( + eas, &g_dps_endpoint_address_pool, on_selected_change, + on_selected_change_data, + oc_endpoint_address_make_view_with_name(OC_STRING_VIEW_NULL, + OC_STRING_VIEW_NULL)); +} + +oc_endpoint_address_t * +plgd_dps_add_endpoint_address(plgd_dps_context_t *ctx, const char *uri, + size_t uri_len, const char *name, size_t name_len) +{ + return oc_endpoint_addresses_add( + &ctx->store.endpoints, + oc_endpoint_address_make_view_with_name(oc_string_view(uri, uri_len), + oc_string_view(name, name_len))); +} + +bool +plgd_dps_remove_endpoint_address(plgd_dps_context_t *ctx, + const oc_endpoint_address_t *address) +{ + return oc_endpoint_addresses_remove(&ctx->store.endpoints, address); +} + +void +plgd_dps_iterate_server_addresses(const plgd_dps_context_t *ctx, + oc_endpoint_addresses_iterate_fn_t iterate_fn, + void *iterate_fn_data) +{ + oc_endpoint_addresses_iterate(&ctx->store.endpoints, iterate_fn, + iterate_fn_data); +} + +bool +plgd_dps_select_endpoint_address(plgd_dps_context_t *ctx, + const oc_endpoint_address_t *address) +{ + return oc_endpoint_addresses_select(&ctx->store.endpoints, address); +} + +const oc_endpoint_address_t * +plgd_dps_selected_endpoint_address(const plgd_dps_context_t *ctx) +{ + return oc_endpoint_addresses_selected(&ctx->store.endpoints); +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h b/api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h new file mode 100644 index 0000000000..79698a7f17 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h @@ -0,0 +1,84 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_ENDPOINTS_INTERNAL_H +#define PLGD_DPS_ENDPOINTS_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "util/oc_compiler.h" +#include "util/oc_endpoint_address_internal.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + DPS_ENDPOINT_NOT_CHANGED, + DPS_ENDPOINT_CHANGED, +}; + +/** + * @brief Sets endpoint to DPS. + * + * @param ctx dps context (cannot be NULL) + * @param endpoint endpoint of the provisioning server (cannot be NULL) + * @param endpoint_len length of \p endpoint + * @param notify notify observers + * @return -1 on error + * @return DPS_ENDPOINT_NOT_CHANGED if endpoint was not changed + * @return DPS_ENDPOINT_CHANGED if endpoint was changed + */ +int dps_set_endpoint(plgd_dps_context_t *ctx, const char *endpoint, + size_t endpoint_len, bool notify) OC_NONNULL(); + +/** Set DPS endpoint list and select one endpoint to be used by DPS + * + * @param ctx DPS context (cannot be NULL) + * @param selected_endpoint selected endpoint address (cannot be NULL) + * @param selected_endpoint_name name associated with the selected endpoint + * @param endpoints list of available endpoints + * @return true on success + * @return false on failure + */ +bool dps_set_endpoints(plgd_dps_context_t *ctx, + const oc_string_t *selected_endpoint, + const oc_string_t *selected_endpoint_name, + const oc_rep_t *endpoints) OC_NONNULL(1, 2); + +/** Initialize DPS endpoints + * + * @param eas endpoint addresses to initialize (cannot be NULL) + * @param on_selected_change callback invoked when the selected endpoint changes + * @param on_selected_change_data data passed to the on_selected_change callback + * @return true on success + * @return false on failure + */ +bool dps_endpoints_init( + oc_endpoint_addresses_t *eas, + on_selected_endpoint_address_change_fn_t on_selected_change, + void *on_selected_change_data) OC_NONNULL(1); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_ENDPOINTS_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_internal.h b/api/plgd/device-provisioning-client/plgd_dps_internal.h new file mode 100644 index 0000000000..8f89d8fdff --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_internal.h @@ -0,0 +1,107 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_INTERNAL_H +#define PLGD_DPS_INTERNAL_H + +#include "plgd_dps_log_internal.h" +#include "plgd/plgd_dps.h" // plgd_dps_context_t, plgd_dps_manager_callbacks_t + +#include "oc_ri.h" +#include "util/oc_compiler.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PLGD_DPS_PROVISIONED_MASK \ + (PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | PLGD_DPS_HAS_CLOUD | \ + PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_HAS_ACLS) +#define PLGD_DPS_PROVISIONED_ERROR_FLAGS \ + (PLGD_DPS_TRANSIENT_FAILURE | PLGD_DPS_FAILURE) +#define PLGD_DPS_PROVISIONED_ALL_FLAGS \ + (PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME | PLGD_DPS_HAS_TIME | \ + PLGD_DPS_GET_OWNER | PLGD_DPS_HAS_OWNER | PLGD_DPS_GET_CLOUD | \ + PLGD_DPS_HAS_CLOUD | PLGD_DPS_GET_CREDENTIALS | PLGD_DPS_HAS_CREDENTIALS | \ + PLGD_DPS_GET_ACLS | PLGD_DPS_HAS_ACLS | PLGD_DPS_CLOUD_STARTED | \ + PLGD_DPS_RENEW_CREDENTIALS | PLGD_DPS_TRANSIENT_FAILURE | PLGD_DPS_FAILURE) + +static const char kPlgdDpsStatusUninitialized[] = "uninitialized"; +static const char kPlgdDpsStatusInitialized[] = "initialized"; +static const char kPlgdDpsStatusGetTime[] = "provisioning time"; +static const char kPlgdDpsStatusHasTime[] = "provisioned time"; +static const char kPlgdDpsStatusGetOwner[] = "provisioning owner"; +static const char kPlgdDpsStatusHasOwner[] = "provisioned owner"; +static const char kPlgdDpsStatusGetCredentials[] = "provisioning credentials"; +static const char kPlgdDpsStatusHasCredentials[] = "provisioned credentials"; +static const char kPlgdDpsStatusGetAcls[] = "provisioning acls"; +static const char kPlgdDpsStatusHasAcls[] = "provisioned acls"; +static const char kPlgdDpsStatusGetCloud[] = "provisioning cloud"; +static const char kPlgdDpsStatusHasCloud[] = "provisioned cloud"; +static const char kPlgdDpsStatusProvisioned[] = "provisioned"; +static const char kPlgdDpsStatusRenewCredentials[] = "renew credentials"; +static const char kPlgdDpsStatusTransientFailure[] = "transient failure"; +static const char kPlgdDpsStatusFailure[] = "failure"; + +/// @brief Convert DPS status flags to string in format "flag|flag|flag" for +/// logs and copy it into the output buffer +int dps_status_to_logstr(uint32_t status, char *buffer, size_t buffer_size); + +#if DPS_DBG_IS_ENABLED +void dps_print_status(const char *prefix, uint32_t status); +#endif /* DPS_DBG_IS_ENABLED */ + +/// @brief Callback to report DPS status. +oc_event_callback_retval_t dps_status_callback_handler(void *data); + +/// @brief Try set cloud to use the latest identity certificate chain provided +/// by DPS. +OC_NO_DISCARD_RETURN +bool dps_try_set_identity_chain(size_t device); + +/// @brief Notifies observers about resource change. +void dps_notify_observers(plgd_dps_context_t *ctx); + +/// @brief Converts DPS status flag to string. +const char *dps_status_flag_to_str(plgd_dps_status_t status); + +#ifdef OC_SESSION_EVENTS + +/// @brief Initialize session callbacks +void plgd_dps_session_callbacks_init(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Deinitialize session callbacks +void plgd_dps_session_callbacks_deinit(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Initialize interface callbacks +void plgd_dps_interface_callbacks_init(void); + +/// @brief Deinitialize interface callbacks +void plgd_dps_interface_callbacks_deinit(void); + +#endif /* OC_SESSION_EVENTS */ + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_log.c b/api/plgd/device-provisioning-client/plgd_dps_log.c new file mode 100644 index 0000000000..65defa290a --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_log.c @@ -0,0 +1,113 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_log_internal.h" + +#include "oc_clock_util.h" +#include "oc_log.h" +#include "util/oc_atomic.h" +#include "util/oc_compiler.h" + +#include +#include +#include +#include + +static struct +{ + plgd_dps_print_log_fn_t fn; ///< logging function + OC_ATOMIC_INT8_T level; ///< enabled log level + OC_ATOMIC_UINT32_T components; ///< mask of enabled log components +} g_dps_logger = { + .fn = NULL, + .level = OC_LOG_LEVEL_INFO, + .components = PLGD_DPS_LOG_COMPONENT_DEFAULT, +}; + +const char * +dps_log_get_component_label(plgd_dps_log_component_t component) +{ + if (component == PLGD_DPS_LOG_COMPONENT_DEFAULT) { + return "default"; + } + return ""; +} + +static void dps_log_default_fn(oc_log_level_t level, + plgd_dps_log_component_t component, + const char *file, int line, + const char *func_name, const char *format, ...) + OC_PRINTF_FORMAT(6, 7); +static void +dps_log_default_fn(oc_log_level_t level, plgd_dps_log_component_t component, + const char *file, int line, const char *func_name, + const char *format, ...) +{ + char dps_log_fn_buf[64] = { 0 }; // NOLINT(readability-magic-numbers) + oc_clock_time_rfc3339(dps_log_fn_buf, sizeof(dps_log_fn_buf)); + + printf("[DPS:%s %s] %s: %s <%s:%d>: ", dps_log_get_component_label(component), + dps_log_fn_buf, oc_log_level_to_label(level), file, func_name, line); + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); + printf("\n"); + fflush(stdout); +} + +void +plgd_dps_set_log_fn(plgd_dps_print_log_fn_t log_fn) +{ + g_dps_logger.fn = log_fn; +} + +plgd_dps_print_log_fn_t +plgd_dps_get_log_fn(void) +{ + if (g_dps_logger.fn == NULL) { + return &dps_log_default_fn; + } + return g_dps_logger.fn; +} + +void +plgd_dps_log_set_level(oc_log_level_t level) +{ + assert(level >= INT8_MIN); + assert(level <= INT8_MAX); + OC_ATOMIC_STORE8(g_dps_logger.level, (int8_t)level); +} + +oc_log_level_t +plgd_dps_log_get_level(void) +{ + return OC_ATOMIC_LOAD8(g_dps_logger.level); +} + +void +plgd_dps_log_set_components(uint32_t components) +{ + OC_ATOMIC_STORE32(g_dps_logger.components, components); +} + +uint32_t +plgd_dps_log_get_components(void) +{ + return OC_ATOMIC_LOAD32(g_dps_logger.components); +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_log_internal.h b/api/plgd/device-provisioning-client/plgd_dps_log_internal.h new file mode 100644 index 0000000000..ade4785264 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_log_internal.h @@ -0,0 +1,142 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +// TODO: merge with oc_log? + +#ifndef PLGD_DPS_LOG_INTERNAL_H +#define PLGD_DPS_LOG_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "string.h" +#include "util/oc_compiler.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef __FILENAME__ +#ifdef WIN32 +// NOLINTNEXTLINE(bugprone-reserved-identifier) +#define __FILENAME__ \ + (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__) +#else +// NOLINTNEXTLINE(bugprone-reserved-identifier) +#define __FILENAME__ \ + (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#endif +#endif /* !__FILENAME__ */ + +#ifndef PLGD_DPS_LOG_MAXIMUM_LEVEL +#define PLGD_DPS_LOG_MAXIMUM_LEVEL (OC_LOG_LEVEL_DISABLED_MACRO) +#endif + +#define PLGD_DPS_LOG_LEVEL_IS_ENABLED(level) \ + ((level) <= (PLGD_DPS_LOG_MAXIMUM_LEVEL)) + +#define DPS_LOG(log_level, log_component, ...) \ + do { \ + if (plgd_dps_log_get_level() >= (log_level) && \ + (plgd_dps_log_get_components() & log_component) != 0) { \ + plgd_dps_get_log_fn()((log_level), \ + (plgd_dps_log_component_t)(log_component), \ + __FILENAME__, __LINE__, __func__, __VA_ARGS__); \ + } \ + } while (0) + +#define DPS_TRACE_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_TRACE_MACRO) +#if DPS_TRACE_IS_ENABLED +#define DPS_TRACE_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_TRACE, (component), __VA_ARGS__) +#define DPS_TRACE(...) \ + DPS_TRACE_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_TRACE_IS_ENABLED */ +#define DPS_TRACE(...) +#define DPS_TRACE_COMPONENT(component, ...) +#endif /* DPS_TRACE_IS_ENABLED */ + +#define DPS_DBG_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_DEBUG_MACRO) +#if DPS_DBG_IS_ENABLED +#define DPS_DBG_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_DEBUG, (component), __VA_ARGS__) +#define DPS_DBG(...) \ + DPS_DBG_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_DBG_IS_ENABLED */ +#define DPS_DBG_COMPONENT(component, ...) +#define DPS_DBG(...) +#endif /* DPS_DBG_IS_ENABLED */ + +#define DPS_INFO_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_INFO_MACRO) +#if DPS_INFO_IS_ENABLED +#define DPS_INFO_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_INFO, (component), __VA_ARGS__) +#define DPS_INFO(...) \ + DPS_INFO_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_INFO_IS_ENABLED */ +#define DPS_INFO_COMPONENT(component, ...) +#define DPS_INFO(...) +#endif /* DPS_INFO_IS_ENABLED */ + +#define DPS_NOTE_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_NOTICE_MACRO) +#if DPS_NOTE_IS_ENABLED +#define DPS_NOTE_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_NOTICE, (component), __VA_ARGS__) +#define DPS_NOTE(...) \ + DPS_NOTE_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_NOTE_IS_ENABLED */ +#define DPS_NOTE_COMPONENT(component, ...) +#define DPS_NOTE(...) +#endif /* DPS_NOTE_IS_ENABLED */ + +#define DPS_WRN_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_WARNING_MACRO) +#if DPS_WRN_IS_ENABLED +#define DPS_WRN_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_WARNING, (component), __VA_ARGS__) +#define DPS_WRN(...) \ + DPS_WRN_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_WRN_IS_ENABLED */ +#define DPS_WRN_COMPONENT(component, ...) +#define DPS_WRN(...) +#endif /* DPS_WRN_IS_ENABLED */ + +#define DPS_ERR_IS_ENABLED \ + PLGD_DPS_LOG_LEVEL_IS_ENABLED(OC_LOG_LEVEL_ERROR_MACRO) +#if DPS_ERR_IS_ENABLED +#define DPS_ERR_COMPONENT(component, ...) \ + DPS_LOG(OC_LOG_LEVEL_ERROR, (component), __VA_ARGS__) +#define DPS_ERR(...) \ + DPS_ERR_COMPONENT(PLGD_DPS_LOG_COMPONENT_DEFAULT, __VA_ARGS__) +#else /* !DPS_ERR_IS_ENABLED */ +#define DPS_ERR_COMPONENT(component, ...) +#define DPS_ERR(...) +#endif /* DPS_ERR_IS_ENABLED */ + +/** * @brief Get component label as a C-string. */ +const char *dps_log_get_component_label(plgd_dps_log_component_t component) + OC_RETURNS_NONNULL; + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_LOG_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_manager.c b/api/plgd/device-provisioning-client/plgd_dps_manager.c new file mode 100644 index 0000000000..91789c748a --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_manager.c @@ -0,0 +1,336 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_cloud_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_cloud_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_store_internal.h" +#include "plgd_dps_time_internal.h" +#include "plgd_dps_internal.h" + +#include "oc_cred.h" +#include "oc_network_monitor.h" +#include "util/oc_list.h" + +#include + +#define ACCESS_TOKEN_KEY "accesstoken" +#define REFRESH_TOKEN_KEY "refreshtoken" +#define REDIRECTURI_KEY "redirecturi" +#define USER_ID_KEY "uid" +#define EXPIRESIN_KEY "expiresin" + +void +dps_manager_start(plgd_dps_context_t *ctx) +{ + if (!ctx->manager_started) { + DPS_DBG("dps_manager_start"); + ctx->manager_started = true; + dps_reset_delayed_callback(ctx, dps_manager_provision_async, 0); + } + _oc_signal_event_loop(); +} + +static bool +dps_has_mfg_certificate(size_t device) +{ + const oc_sec_cred_t *mfg_cred = NULL; + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + const oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + while (cred != NULL) { + if (cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_MFG_CERT && !dps_is_dps_cred(cred)) { + mfg_cred = cred; + break; + } + cred = cred->next; + } + + if (mfg_cred != NULL) { + DPS_DBG("Manufacturer certificate(%d) found", mfg_cred->credid); + return true; + } + return false; +} + +static bool +dps_has_mfg_trusted_root_ca(size_t device) +{ + const oc_sec_cred_t *trusted_ca = NULL; + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + const oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + while (cred != NULL) { + if (cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_MFG_TRUSTCA && !dps_is_dps_cred(cred)) { + trusted_ca = cred; + break; + } + cred = cred->next; + } + if (trusted_ca != NULL) { + DPS_DBG("manufacturer trusted root ca(%d) found", trusted_ca->credid); + return true; + } + return false; +} + +static uint32_t +dps_try_reprovision(plgd_dps_context_t *ctx) +{ + uint32_t provisionFlags = 0; + if (dps_has_plgd_time()) { + provisionFlags |= PLGD_DPS_HAS_TIME; + } + if (((provisionFlags & PLGD_DPS_HAS_TIME) != 0) && dps_has_owner(ctx)) { + provisionFlags |= PLGD_DPS_HAS_OWNER; + } + if (((provisionFlags & PLGD_DPS_HAS_OWNER) != 0) && + dps_has_cloud_configuration(ctx->device)) { + provisionFlags |= PLGD_DPS_HAS_CLOUD; + } + if (((provisionFlags & PLGD_DPS_HAS_CLOUD) != 0) && + dps_check_credentials_and_schedule_renewal(ctx, 0) && + dps_try_set_identity_chain(ctx->device)) { + provisionFlags |= PLGD_DPS_HAS_CREDENTIALS; + } + if (((provisionFlags & PLGD_DPS_HAS_CREDENTIALS) != 0) && + dps_has_acls(ctx->device)) { + provisionFlags |= PLGD_DPS_HAS_ACLS; + } + if (((provisionFlags & PLGD_DPS_HAS_ACLS) != 0) && + dps_cloud_is_registered(ctx->device)) { + if (dps_cloud_is_started(ctx->device)) { + provisionFlags |= PLGD_DPS_CLOUD_STARTED; + } + ctx->cloud_observer.last_status |= OC_CLOUD_REGISTERED; + if (dps_cloud_is_logged_in(ctx->device)) { + ctx->cloud_observer.last_status |= OC_CLOUD_LOGGED_IN; + } + } + return provisionFlags; +} + +int +plgd_dps_manager_start(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + if (plgd_dps_manager_is_started(ctx)) { + DPS_DBG("DPS manager already started"); + return 0; + } + + if (plgd_dps_endpoint_is_empty(ctx)) { + DPS_DBG("DPS is uninitialized state: endpoint is empty"); + dps_set_ps_and_last_error(ctx, 0, PLGD_DPS_PROVISIONED_ALL_FLAGS, + PLGD_DPS_OK); + ctx->force_reprovision = false; + return 0; + } + + DPS_DBG("DPS manager starting"); +#if DPS_DBG_IS_ENABLED + dps_print_peers(); + dps_print_certificates(ctx->device); + dps_print_acls(ctx->device); +#endif /* DPS_DBG_IS_ENABLED */ + if (!dps_has_mfg_certificate(ctx->device)) { + DPS_ERR("Manufacturer certificate not set"); + return -1; + } + if (!ctx->skip_verify && !dps_has_mfg_trusted_root_ca(ctx->device)) { + DPS_WRN("Manufacturer trusted root CA not set"); + } + + ctx->status = 0; + uint32_t new_status = PLGD_DPS_INITIALIZED; + if (!ctx->force_reprovision) { + new_status |= dps_try_reprovision(ctx); + } + ctx->force_reprovision = false; + dps_set_ps_and_last_error(ctx, new_status, 0, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + if (dps_is_provisioned(ctx)) { + dps_set_has_been_provisioned_since_reset(ctx, false); + } + + dps_store_dump_async(ctx); + dps_manager_start(ctx); +#ifdef OC_SESSION_EVENTS + plgd_dps_session_callbacks_deinit(ctx); + plgd_dps_session_callbacks_init(ctx); + plgd_dps_interface_callbacks_deinit(); + plgd_dps_interface_callbacks_init(); +#endif /* OC_SESSION_EVENTS */ + return 0; +} + +bool +plgd_dps_manager_is_started(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->manager_started; +} + +oc_event_callback_retval_t +dps_manager_start_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + oc_free_endpoint(ctx->endpoint); + ctx->endpoint = oc_new_endpoint(); + memset(ctx->endpoint, 0, sizeof(oc_endpoint_t)); + dps_manager_start(ctx); + return OC_EVENT_DONE; +} + +int +plgd_dps_manager_restart(plgd_dps_context_t *ctx) +{ + plgd_dps_manager_stop(ctx); + return plgd_dps_manager_start(ctx); +} + +void +dps_manager_stop(plgd_dps_context_t *ctx) +{ + DPS_DBG("dps_manager_stop"); + oc_remove_delayed_callback(ctx, dps_provisioning_start_async); + oc_remove_delayed_callback(ctx, dps_manager_provision_async); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + oc_remove_delayed_callback(ctx, dps_manager_reprovision_and_restart_async); + oc_remove_delayed_callback(ctx, dps_provision_next_step_async); + oc_remove_delayed_callback(ctx, dps_status_callback_handler); + oc_remove_delayed_callback(ctx, dps_pki_renew_certificates_async); + oc_remove_delayed_callback(ctx, dps_pki_renew_certificates_retry_async); + oc_remove_delayed_callback( + ctx, dps_cloud_observer_reprovision_server_uuid_change_async); + dps_cloud_observer_deinit(ctx); + ctx->manager_started = false; +} + +void +plgd_dps_manager_stop(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + DPS_DBG("DPS manager stop"); +#ifdef OC_SESSION_EVENTS + plgd_dps_session_callbacks_deinit(ctx); + if (dps_context_list_is_empty()) { + plgd_dps_interface_callbacks_deinit(); + } + oc_remove_delayed_callback(ctx, dps_manager_start_async); +#endif /* OC_SESSION_EVENTS */ + dps_manager_stop(ctx); + dps_endpoint_disconnect(ctx); +} + +void +dps_manager_reprovision_and_restart(plgd_dps_context_t *ctx) +{ + plgd_dps_force_reprovision(ctx); + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx != NULL) { + DPS_DBG("Stop cloud manager"); + if (oc_cloud_manager_stop(cloud_ctx) != 0) { + DPS_ERR("failed to stop cloud manager"); + } + } + if (plgd_dps_manager_restart(ctx) != 0) { + DPS_ERR("failed to reprovisiong and restart DPS"); + } +} + +oc_event_callback_retval_t +dps_manager_reprovision_and_restart_async(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + dps_manager_reprovision_and_restart(ctx); + return OC_EVENT_DONE; +} + +oc_event_callback_retval_t +dps_manager_provision_retry_async(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + dps_endpoint_disconnect(ctx); + // TODO: wait for disconnect, only if really disconnected then continue + dps_retry_increment(ctx, dps_provision_get_next_action(ctx)); + return dps_manager_provision_async(ctx); +} + +oc_event_callback_retval_t +dps_manager_provision_async(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + if (ctx->force_reprovision) { + DPS_DBG("update status to force full reprovision"); + dps_set_ps_and_last_error(ctx, 0, + PLGD_DPS_GET_TIME | PLGD_DPS_GET_OWNER | + PLGD_DPS_GET_CLOUD | PLGD_DPS_GET_CREDENTIALS | + PLGD_DPS_GET_ACLS | PLGD_DPS_PROVISIONED_MASK, + ctx->last_error); + ctx->force_reprovision = false; + } + + if (dps_is_provisioned_with_cloud_started(ctx)) { + dps_cloud_observe_status(ctx); + return OC_EVENT_DONE; + } + + if ((ctx->status & PLGD_DPS_INITIALIZED) == 0) { + DPS_DBG("provisioning skipped: DPS is not initialized"); + return OC_EVENT_DONE; + } + + DPS_DBG("try provision(%d)", ctx->retry.count); + if (plgd_dps_endpoint_is_empty(ctx)) { + DPS_DBG("endpoint dps is empty"); + ctx->status = 0; + dps_manager_stop(ctx); + return OC_EVENT_DONE; + } + const oc_string_t *ep_uri = + oc_endpoint_addresses_selected_uri(&ctx->store.endpoints); + assert(ep_uri != NULL); // checked in plgd_dps_endpoint_is_empty above + if (dps_endpoint_init(ctx, ep_uri) != 0) { + DPS_ERR("failed to initialize endpoint %s to dps", + ep_uri != NULL ? oc_string(*ep_uri) : "NULL"); + goto retry; + } + bool valid_owned = dps_is_dos_owned(ctx->device) || dps_set_self_owned(ctx); + if (!valid_owned) { + DPS_ERR("failed to set device(%zu) as self owned", ctx->device); + goto retry; + } + dps_provisioning_start(ctx); + return OC_EVENT_DONE; + +retry: + // while retrying, keep last error (lec) to PLGD_DPS_OK + dps_set_last_error(ctx, PLGD_DPS_OK); + // retry on error + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + dps_retry_get_delay(&ctx->retry)); + return OC_EVENT_DONE; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_manager_internal.h b/api/plgd/device-provisioning-client/plgd_dps_manager_internal.h new file mode 100644 index 0000000000..1db85bddc3 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_manager_internal.h @@ -0,0 +1,76 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_MANAGER_INTERNAL_H +#define PLGD_DPS_MANAGER_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_config.h" +#include "util/oc_compiler.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/// @brief Start DPS process. +void dps_manager_start(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Start DPS process from an async callback. +oc_event_callback_retval_t dps_manager_start_async(void *user_data) + OC_NONNULL(); + +/// @brief Stop DPS process. +void dps_manager_stop(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Force reprovisioning, and restart manager and cloud manager. +void dps_manager_reprovision_and_restart(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Asynchrounous wrapper for @ref dps_manager_reprovision_and_restart. +oc_event_callback_retval_t dps_manager_reprovision_and_restart_async(void *data) + OC_NONNULL(); + +/** + * @brief Callback function to start DPS provisioning process. + * + * @param data User data provided to the callback (must be the DPS context of + * the device) + * @return oc_event_callback_retval_t OC_EVENT_DONE + */ +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_manager_provision_async(void *data) OC_NONNULL(); + +/** + * @brief Callback to retry DPS provisioning in case of an error or a timeout. + * + * @note Each call increments the retry counter + * + * @param data User data provided to the callback (must be the DPS context of + * the device) + * @return oc_event_callback_retval_t OC_EVENT_DONE + */ +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_manager_provision_retry_async(void *data) + OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_MANAGER_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_pki.c b/api/plgd/device-provisioning-client/plgd_dps_pki.c new file mode 100644 index 0000000000..9b392f269c --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_pki.c @@ -0,0 +1,418 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_pki_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_retry_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_tag_internal.h" +#include "plgd_dps_time_internal.h" + +#include "oc_api.h" +#include "oc_certs.h" +#include "oc_core_res.h" +#include "oc_cred.h" +#include "oc_csr.h" +#include "oc_ri.h" +#include "oc_store.h" +#include "security/oc_pstat_internal.h" +#include "security/oc_security_internal.h" +#include "security/oc_tls_internal.h" + +#include +#include + +// NOLINTNEXTLINE(modernize-*) +#define SECONDS_PER_MINUTE (60) +// NOLINTNEXTLINE(modernize-*) +#define MINUTES_PER_HOUR (60) + +void +dps_pki_init(dps_pki_configuration_t *pki) +{ + assert(pki != NULL); + const uint16_t kDefaultExpiringLimit = + UINT16_C(MINUTES_PER_HOUR * SECONDS_PER_MINUTE); // 1 hour + pki->expiring_limit = kDefaultExpiringLimit; +} + +bool +dps_pki_send_csr(plgd_dps_context_t *ctx, oc_response_handler_t handler) +{ + assert(ctx != NULL); + assert(handler != NULL); + + // NOLINTNEXTLINE(readability-magic-numbers) + unsigned char csr_data[1024]; + int ret = + oc_sec_csr_generate(ctx->device, oc_sec_certs_md_signature_algorithm(), + csr_data, sizeof(csr_data)); + if (ret != 0) { + return false; + } + + if (!oc_init_post(PLGD_DPS_CREDS_URI, ctx->endpoint, NULL, handler, LOW_QOS, + ctx)) { + DPS_ERR("could not init POST request to %s", PLGD_DPS_CREDS_URI); + return false; + } + + const oc_uuid_t *device_id = oc_core_get_device_id(ctx->device); + if (device_id == NULL) { + DPS_ERR("failed to get device id for POST request to %s", + PLGD_DPS_CREDS_URI); + return false; + } + char uuid[OC_UUID_LEN] = { 0 }; + int uuid_len = oc_uuid_to_str_v1(device_id, uuid, OC_UUID_LEN); + assert(uuid_len > 0); + + oc_rep_start_root_object(); + oc_rep_set_text_string_v1(root, di, uuid, uuid_len); + oc_rep_set_object(root, csr); + oc_rep_set_text_string(csr, data, (const char *)csr_data); + oc_rep_set_text_string(csr, encoding, "oic.sec.encoding.pem"); + oc_rep_close_object(root, csr); + oc_rep_end_root_object(); + + dps_setup_tls(ctx); + if (!oc_do_post_with_timeout(dps_retry_get_timeout(&ctx->retry))) { + dps_reset_tls(); + DPS_ERR("failed to dispatch POST request to %s", PLGD_DPS_CREDS_URI); + return false; + } + return true; +} + +static const char *g_dps_certificate_state_str[] = { + "valid", + "not yet valid", + "expiring", + "expired", +}; + +const char * +dps_pki_certificate_state_to_str(dps_certificate_state_t state) +{ +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + assert((size_t)state < ARRAY_SIZE(g_dps_certificate_state_str)); + return g_dps_certificate_state_str[(size_t)state]; +} + +static bool +dps_pki_certificate_is_expiring(dps_pki_configuration_t cfg, uint64_t valid_to, + uint64_t now_s) +{ + return now_s + cfg.expiring_limit > valid_to; +} + +int +dps_pki_validate_certificate(dps_pki_configuration_t cfg, uint64_t valid_from, + uint64_t valid_to) +{ + oc_clock_time_t now = dps_time(); + if (now == (oc_clock_time_t)-1) { + DPS_ERR("cannot validate certificate: %s", "failed to get current time"); + return -1; + } + + uint64_t now_s = now / OC_CLOCK_SECOND; + DPS_DBG( + "\tcheck certificate validity: now=%lu from=%lu to=%lu expiring_limit:%u", + now_s, valid_from, valid_to, (unsigned)cfg.expiring_limit); + if (now_s < valid_from) { + return DPS_CERTIFICATE_NOT_YET_VALID; + } + + if (dps_pki_certificate_is_expiring(cfg, valid_to, now_s)) { + return now_s > valid_to ? DPS_CERTIFICATE_EXPIRED + : DPS_CERTIFICATE_EXPIRING; + } + return DPS_CERTIFICATE_VALID; +} + +static void +dps_on_apply_cred(oc_sec_on_apply_cred_data_t cred_data, void *user_data) +{ + (void)user_data; + if (cred_data.cred == NULL) { + return; + } + bool *credentials_replaced = (bool *)user_data; + *credentials_replaced = *credentials_replaced || (cred_data.replaced != NULL); +#if DPS_DBG_IS_ENABLED + // GCOVR_EXCL_START + DPS_DBG("apply cred:"); + bool duplicate = !cred_data.created && cred_data.replaced == NULL; + int replaced_credid = + cred_data.replaced != NULL ? cred_data.replaced->credid : -1; + DPS_DBG("\tcreated:%d duplicate:%d replaced credid:%d", cred_data.created, + duplicate, replaced_credid); + char uuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&cred_data.cred->subjectuuid, uuid, sizeof(uuid)); + const char *tag = oc_string_len(cred_data.cred->tag) > 0 + ? oc_string(cred_data.cred->tag) + : ""; + DPS_DBG("\tcredid:%d credtype:%d credusage:%d subjectuuid:%s tag:%s", + cred_data.cred->credid, cred_data.cred->credtype, + cred_data.cred->credusage, uuid, tag); + // GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ +} + +bool +dps_pki_replace_certificates(size_t device, const oc_rep_t *rep, + const oc_endpoint_t *endpoint) +{ + const oc_resource_t *sec_cred = + oc_core_get_resource_by_index(OCF_SEC_CRED, device); + if (sec_cred == NULL) { + DPS_ERR("cannot find credential resource for device(%zu)", device); + return false; + } + + dps_credentials_set_stale_tag(device); + oc_sec_pstat_t *pstat = oc_sec_get_pstat(device); + pstat->s = OC_DOS_RFPRO; + oc_sec_on_apply_cred_cb_t on_apply_cred_cb = dps_on_apply_cred; + bool credentials_replaced = false; + int ret = oc_sec_apply_cred(rep, sec_cred, endpoint, on_apply_cred_cb, + &credentials_replaced); + pstat->s = OC_DOS_RFNOP; + if (ret != 0) { + DPS_ERR("cannot apply credential resource update for device(%zu)", device); + dps_credentials_remove_stale_tag(device); + return false; + } + int credentials_removed = dps_remove_stale_credentials(device); + +#if DPS_DBG_IS_ENABLED + dps_print_certificates(device); +#endif /* DPS_DBG_IS_ENABLED */ + + oc_sec_dump_cred(device); + + if (credentials_replaced || credentials_removed > 0) { + DPS_DBG("credentials modification detected"); + // must be called after assignment pstat->s = OC_DOS_RFNOP + oc_tls_close_peers(dps_endpoint_peer_is_server, NULL); + } + return true; +} + +bool +dps_pki_can_replace_certificates(const plgd_dps_context_t *ctx) +{ + return dps_is_provisioned_with_cloud_started(ctx); +} + +void +dps_pki_schedule_renew_certificates(plgd_dps_context_t *ctx, uint64_t valid_to, + uint64_t min_interval) +{ + uint64_t interval = + dps_pki_calculate_renew_certificates_interval(ctx->pki, valid_to); + if (interval == (uint64_t)-1) { + DPS_ERR("certificate renewal not scheduled: failed to calculate " + "credentials check interval"); + return; + } + if (interval < min_interval) { + DPS_DBG( + "substituting minimal allowed interval for the calculated interval"); + interval = min_interval; + } + DPS_INFO("certificate renewal scheduled to run in %" PRIu64 " milliseconds", + interval); + dps_reset_delayed_callback_ms(ctx, dps_pki_renew_certificates_async, + interval); +} + +uint64_t +dps_pki_calculate_renew_certificates_interval(dps_pki_configuration_t cfg, + uint64_t valid_to) +{ + oc_clock_time_t now = dps_time(); + if (now == (oc_clock_time_t)-1) { + return (uint64_t)-1; + } + + uint64_t now_s = now / OC_CLOCK_SECOND; + if (dps_pki_certificate_is_expiring(cfg, valid_to, now_s)) { + return 0; // recheck the credentials right away + } + + const uint64_t kMinInterval = UINT64_C( + 10); // use some minimal interval to prevent constant retries in case the + // server issues certificates with short expiration time + const uint64_t kLimit1 = UINT64_C(1) * SECONDS_PER_MINUTE; + if (now_s + kLimit1 > valid_to) { // expiring within 1 minute + return kMinInterval * + MILLISECONDS_PER_SECOND; // recheck after some minimal interval + } + + const uint64_t kLimit2 = UINT64_C(3) * SECONDS_PER_MINUTE; + if (now_s + kLimit2 > valid_to) { // expiring within 3 minutes + return UINT64_C(1) * SECONDS_PER_MINUTE * + MILLISECONDS_PER_SECOND; // recheck in a minute + } + + const uint64_t kLimit3 = UINT64_C(6) * SECONDS_PER_MINUTE; + if (now_s + kLimit3 > valid_to) { // expiring within 6 minutes + return UINT64_C(2) * SECONDS_PER_MINUTE * + MILLISECONDS_PER_SECOND; // recheck in 2 minutes + } + + return ((UINT64_C(2) * (valid_to - now_s)) / UINT64_C(3)) * + MILLISECONDS_PER_SECOND; // try after 2/3 of the remaining time passes +} + +static bool +dps_pki_replace_credentials_retry(plgd_dps_context_t *ctx) +{ + uint64_t retry = dps_retry_get_delay(&ctx->retry); + dps_retry_increment(ctx, PLGD_DPS_RENEW_CREDENTIALS); + DPS_DBG("retry certificates renewal"); + return dps_check_credentials_and_schedule_renewal(ctx, retry); +} + +static void +dps_pki_replace_credentials_handler(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; + oc_remove_delayed_callback(ctx, dps_pki_renew_certificates_retry_async); + if (!dps_pki_can_replace_certificates(ctx) && + (ctx->status & PLGD_DPS_RENEW_CREDENTIALS) == 0) { + DPS_DBG("replacing of certificates skipped"); + return; + } + + plgd_dps_error_t err = dps_check_response(ctx, data->code, data->payload); + if (err != PLGD_DPS_OK) { + DPS_ERR("cannot replace certificates: invalid %s response(status=%d)", + PLGD_DPS_CREDS_URI, data->code); + goto retry; + } + + if (!dps_pki_replace_certificates(ctx->device, data->payload, + data->endpoint)) { + goto retry; + } + + if (!dps_check_credentials_and_schedule_renewal(ctx, 0)) { + DPS_ERR("valid certificates not found"); + goto reprovision; + } + + if (!dps_try_set_identity_chain(ctx->device)) { + DPS_ERR("failed to set identity certificate chain"); + goto reprovision; + } + DPS_DBG("certificates renewed successfully"); + dps_set_ps_and_last_error(ctx, 0, PLGD_DPS_RENEW_CREDENTIALS, PLGD_DPS_OK); + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + dps_endpoint_close(ctx->endpoint); + + return; + +retry: + if (dps_pki_replace_credentials_retry(ctx)) { + return; + } + DPS_ERR("failed to reschedule certificates renewal, force reprovisioning"); + +reprovision: + dps_manager_reprovision_and_restart(ctx); +} + +/// @brief Try replacing current (expiring) DPS certificates with newer +/// certificates retrieved from the DPS service. +static bool +dps_pki_try_renew_certificates(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + DPS_DBG("trying to replace expiring DPS certificates"); + + if (!dps_pki_send_csr(ctx, dps_pki_replace_credentials_handler)) { + DPS_ERR("failed to obtain new DPS certificates: %s", + "failed to send CSR request"); + return false; + } + + // schedule retry in case response is not retrieved + dps_reset_delayed_callback_ms(ctx, dps_pki_renew_certificates_retry_async, + dps_retry_get_delay(&ctx->retry)); + + if (dps_set_ps_and_last_error(ctx, PLGD_DPS_RENEW_CREDENTIALS, 0, + PLGD_DPS_OK)) { + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + } + return true; +} + +oc_event_callback_retval_t +dps_pki_renew_certificates_retry_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + dps_retry_increment(ctx, PLGD_DPS_RENEW_CREDENTIALS); + return dps_pki_renew_certificates_async(ctx); +} + +oc_event_callback_retval_t +dps_pki_renew_certificates_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + if (!dps_pki_can_replace_certificates(ctx)) { + DPS_DBG("renewal of certificates skipped"); + return OC_EVENT_DONE; + } + + if (!dps_pki_try_renew_certificates(ctx)) { + uint64_t retry = dps_retry_get_delay(&ctx->retry); + dps_retry_increment(ctx, PLGD_DPS_RENEW_CREDENTIALS); + if (!dps_check_credentials_and_schedule_renewal(ctx, retry)) { + DPS_ERR( + "failed to reschedule certificates renewal, force reprovisioning"); + dps_manager_reprovision_and_restart(ctx); + } + } + return OC_EVENT_DONE; +} + +void +plgd_dps_pki_set_expiring_limit(plgd_dps_context_t *ctx, + uint16_t expiring_limit) +{ + assert(ctx != NULL); + DPS_DBG("certificate expiring limit set to %us", (unsigned)expiring_limit); + ctx->pki.expiring_limit = expiring_limit; +} + +uint16_t +plgd_dps_pki_get_expiring_limit(const plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + return ctx->pki.expiring_limit; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_pki_internal.h b/api/plgd/device-provisioning-client/plgd_dps_pki_internal.h new file mode 100644 index 0000000000..e46c4ec246 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_pki_internal.h @@ -0,0 +1,146 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_PKI_INTERNAL_H +#define PLGD_DPS_PKI_INTERNAL_H + +#include "plgd/plgd_dps.h" // plgd_dps_context_t + +#include "oc_client_state.h" +#include "util/oc_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PLGD_DPS_CREDS_URI "/api/v1/provisioning/credentials" + +/** + * @brief Send Certificate Signing Request request using POST to the DPS + * service. + * + * @param ctx dps context (cannot be NULL) + * @param handler response handler (cannot be NULL) + * @return true request has been send + * @return false on error + */ +OC_NO_DISCARD_RETURN +bool dps_pki_send_csr(plgd_dps_context_t *ctx, oc_response_handler_t handler) + OC_NONNULL(); + +/** + * @brief Possible validity state of a certificate. + */ +typedef enum { + DPS_CERTIFICATE_VALID = 0, + DPS_CERTIFICATE_NOT_YET_VALID, + DPS_CERTIFICATE_EXPIRING, + DPS_CERTIFICATE_EXPIRED, +} dps_certificate_state_t; + +typedef struct dps_pki_configuration_t +{ + uint16_t expiring_limit; ///< interval in seconds within which a certificate + ///< is considered as expiring +} dps_pki_configuration_t; + +/// @brief Initialize PKI configuration +void dps_pki_init(dps_pki_configuration_t *pki) OC_NONNULL(); + +/// @brief Return string representation of certificate state +OC_NO_DISCARD_RETURN +const char *dps_pki_certificate_state_to_str(dps_certificate_state_t state); + +/** + * @brief Check validity state of a certificate based on its valid-from and + * valid-to timestamps. + * + * @param cfg PKI configuration + * @param valid_from valid-from UNIX timestamp of a certificate + * @param valid_to valid-to UNIX timestamp of a certificate + * @return -1 on error + * @return dps_certificate_state_t resolved validity state of the certificate + */ +OC_NO_DISCARD_RETURN +int dps_pki_validate_certificate(dps_pki_configuration_t cfg, + uint64_t valid_from, uint64_t valid_to); + +/// @brief Check if the device is in a valid state to renew certificates +/// (without needing to do full reprovisioning) +OC_NO_DISCARD_RETURN +bool dps_pki_can_replace_certificates(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Schedule renewal of certificates based on the valid-to timestamp of + * the certificate that expires the earliest. + * + * @param ctx dps context (cannot be NULL) + * @param valid_to valid-to of the certificate that expires the earliest + * @param min_interval minimal interval allowed (if the calculated interval is + * lower then it will be subsituted for this minimal interval) in milliseconds + */ +void dps_pki_schedule_renew_certificates(plgd_dps_context_t *ctx, + uint64_t valid_to, + uint64_t min_interval) OC_NONNULL(); + +/** + * @brief Calculate interval in which to renewal of certificate should be. + * + * @param valid_to the valid_to timestamp of the certificate closest to + * expiration + * @return interval in milliseconds after which certificates should be rechecked + */ +OC_NO_DISCARD_RETURN +uint64_t dps_pki_calculate_renew_certificates_interval( + dps_pki_configuration_t cfg, uint64_t valid_to); + +/// @brief Delayed callback to renew DPS certificates. +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_pki_renew_certificates_async(void *user_data) + OC_NONNULL(); + +/// @brief Delayed callback to renew DPS certificates and increment retry +/// counter. +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_pki_renew_certificates_retry_async( + void *user_data) OC_NONNULL(); + +/** + * @brief Replace credentials with new credentials received from the DPS + * service. + * + * @param device device index + * @param rep data with certificates retrieved from the DPS service (cannot be + * NULL) + * @param endpoint endpoint of the credentials owner + * @return true if certificates were successfully replace + * @return false on failure + */ +OC_NO_DISCARD_RETURN +bool dps_pki_replace_certificates(size_t device, const oc_rep_t *rep, + const oc_endpoint_t *endpoint) OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_PKI_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision.c b/api/plgd/device-provisioning-client/plgd_dps_provision.c new file mode 100644 index 0000000000..a8b85e9168 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision.c @@ -0,0 +1,648 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_provision_cloud_internal.h" +#include "plgd_dps_provision_owner_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_store_internal.h" +#include "plgd_dps_tag_internal.h" +#include "plgd_dps_time_internal.h" +#include "plgd_dps_internal.h" + +#include "oc_acl.h" +#include "oc_core_res.h" +#include "oc_store.h" +#include "security/oc_pstat_internal.h" + +#include +#include + +#define PLGD_DPS_ACLS_URI "/api/v1/provisioning/acls" + +int +dps_provisioning_check_response(plgd_dps_context_t *ctx, oc_status_t code, + const oc_rep_t *payload) +{ + plgd_dps_error_t err = dps_check_response(ctx, code, payload); + if (err == PLGD_DPS_OK) { + return 0; + } + + uint32_t err_status = 0; + if (err == PLGD_DPS_ERROR_CONNECT) { + // While retrying, keep last error (lec) to PLGD_DPS_OK + err = PLGD_DPS_OK; + err_status = PLGD_DPS_TRANSIENT_FAILURE; + } else { + err_status = PLGD_DPS_FAILURE; + } + + dps_set_ps_and_last_error(ctx, err_status, 0, err); + return -1; +} + +#if DPS_DBG_IS_ENABLED +static void +dps_on_apply_acl(oc_sec_on_apply_acl_data_t acl_data, void *user_data) +{ + // GCOVR_EXCL_START + (void)user_data; + if (acl_data.ace == NULL) { + return; + } + DPS_DBG("apply acl:"); + bool duplicate = !acl_data.created && acl_data.replaced_ace == NULL; + int replaced_aceid = + acl_data.replaced_ace != NULL ? acl_data.replaced_ace->aceid : -1; + DPS_DBG("\tcreated:%d created resource:%d duplicate:%d replaced aceid:%d", + acl_data.created, acl_data.created_resource, duplicate, + replaced_aceid); + char uuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&acl_data.rowneruuid, uuid, sizeof(uuid)); + const char *tag = + oc_string_len(acl_data.ace->tag) > 0 ? oc_string(acl_data.ace->tag) : ""; + DPS_DBG("\trowneruuid:%s aceid:%d subject_type:%d permission:%d tag:%s", uuid, + acl_data.ace->aceid, acl_data.ace->subject_type, + acl_data.ace->permission, tag); + oc_ace_res_t *res = (oc_ace_res_t *)oc_list_head(acl_data.ace->resources); + if (res != NULL) { + DPS_DBG("\tresources:"); + for (; res != NULL; res = res->next) { + const char *href = + oc_string_len(res->href) > 0 ? oc_string(res->href) : ""; + DPS_DBG("\t\thref:%s wildcard:%d", href, res->wildcard); + } + } + // GCOVR_EXCL_STOP +} +#endif /* DPS_DBG_IS_ENABLED */ + +static int +dps_handle_get_acls_response(oc_client_response_t *data) +{ + const plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; + const oc_resource_t *res = + oc_core_get_resource_by_index(OCF_SEC_ACL, ctx->device); + if (res == NULL) { + DPS_ERR("cannot find ACL resource for device(%zu)", ctx->device); + return -1; + } + + dps_acls_set_stale_tag(ctx->device); + + oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + pstat->s = OC_DOS_RFPRO; +#if DPS_DBG_IS_ENABLED + oc_sec_on_apply_acl_cb_t on_apply_ace_cb = dps_on_apply_acl; +#else /* !DPS_DBG_IS_ENABLED */ + oc_sec_on_apply_acl_cb_t on_apply_ace_cb = NULL; +#endif /* DPS_DBG_IS_ENABLED */ + int ret = oc_sec_apply_acl(data->payload, ctx->device, on_apply_ace_cb, + /*on_apply_ace_data*/ NULL); + pstat->s = OC_DOS_RFNOP; + if (ret != 0) { + DPS_ERR("cannot apply acl resource update for device(%zu)", ctx->device); + dps_acls_remove_stale_tag(ctx->device); + return -1; + } + dps_remove_stale_acls(ctx->device); + +#if DPS_DBG_IS_ENABLED + dps_print_acls(ctx->device); +#endif /* DPS_DBG_IS_ENABLED */ + + oc_sec_dump_acl(ctx->device); + return 0; +} + +static void +dps_get_acls_handler(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; +#if DPS_DBG_IS_ENABLED + dps_print_status("get acls handler: ", ctx->status); +#endif /* DPS_DBG_IS_ENABLED */ + // we check only for PLGD_DPS_FAILURE flag, because retry will be rescheduled + // if necessary + if ((ctx->status & (PLGD_DPS_HAS_ACLS | PLGD_DPS_FAILURE)) == + PLGD_DPS_HAS_ACLS) { + DPS_DBG("skipping duplicit call of get acls handler"); + return; + } + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + ctx->status &= ~PLGD_DPS_PROVISIONED_ERROR_FLAGS; + + const uint32_t expected_status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | + PLGD_DPS_HAS_OWNER | PLGD_DPS_HAS_CLOUD | + PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_GET_ACLS; + if (ctx->status != expected_status) { +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(ctx->status, str, sizeof(str)); + DPS_ERR("invalid status(%u:%s) in get acls handler", (unsigned)ctx->status, + ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + goto error; + } + + int ret = dps_provisioning_check_response(ctx, data->code, data->payload); + if (ret != 0) { + DPS_ERR("invalid %s response(status=%d)", PLGD_DPS_ACLS_URI, data->code); + // ctx->status and ctx->last_error are set in + // dps_provisioning_check_response + goto finish; + } + + ret = dps_handle_get_acls_response(data); + if (ret != 0) { + goto error; + } + + DPS_INFO("Acls set successfully"); + dps_set_ps_and_last_error( + ctx, PLGD_DPS_HAS_ACLS, + PLGD_DPS_GET_ACLS | PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + + // go to next step -> start cloud + dps_provisioning_schedule_next_step(ctx); + return; + +error: + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, PLGD_DPS_HAS_ACLS, + PLGD_DPS_ERROR_GET_ACLS); +finish: + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) != 0) { + dps_provisioning_handle_failure(ctx, data->code, /*schedule_retry*/ true); + } +} + +static bool +dps_get_acls(plgd_dps_context_t *ctx) +{ + DPS_INFO("Get acls"); + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFNOP) { + return false; + } + + dps_setup_tls(ctx); + if (!oc_do_get_with_timeout(PLGD_DPS_ACLS_URI, ctx->endpoint, NULL, + dps_retry_get_timeout(&ctx->retry), + dps_get_acls_handler, LOW_QOS, ctx)) { + DPS_ERR("failed to dispatch GET request to %s", PLGD_DPS_ACLS_URI); + dps_reset_tls(); + return false; + } + dps_set_ps_and_last_error(ctx, PLGD_DPS_GET_ACLS, + PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + return true; +} + +static void +dps_get_credentials_handler(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; +#if DPS_DBG_IS_ENABLED + dps_print_status("get credentials handler: ", ctx->status); +#endif /* DPS_DBG_IS_ENABLED */ + // we check only for PLGD_DPS_FAILURE flag, because retry will be rescheduled + // if necessary + if ((ctx->status & (PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_FAILURE)) == + PLGD_DPS_HAS_CREDENTIALS) { + DPS_DBG("skipping duplicit call of get credentials handler"); + return; + } + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + ctx->status &= ~PLGD_DPS_PROVISIONED_ERROR_FLAGS; + + const uint32_t expected_status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | + PLGD_DPS_HAS_OWNER | PLGD_DPS_HAS_CLOUD | + PLGD_DPS_GET_CREDENTIALS; + if (ctx->status != expected_status) { +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(ctx->status, str, sizeof(str)); + DPS_ERR("invalid status(%u:%s) in get credentials handler", + (unsigned)ctx->status, ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + goto error; + } + + if (dps_provisioning_check_response(ctx, data->code, data->payload) != 0) { + DPS_ERR("invalid %s response(status=%d)", PLGD_DPS_CREDS_URI, data->code); + // ctx->status and ctx->last_error are set in + // dps_provisioning_check_response + goto finish; + } + + if (!dps_pki_replace_certificates(ctx->device, data->payload, + data->endpoint)) { + goto error; + } + + if (!dps_check_credentials_and_schedule_renewal(ctx, 0)) { + DPS_ERR("valid certificates not found"); + goto error; + } + + if (!dps_try_set_identity_chain(ctx->device)) { + DPS_ERR("failed to set identity certificate chain"); + goto error; + } + + DPS_INFO("Credentials set successfully"); + dps_set_ps_and_last_error( + ctx, PLGD_DPS_HAS_CREDENTIALS, + PLGD_DPS_GET_CREDENTIALS | PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + + // go to next step -> get acls + dps_provisioning_schedule_next_step(ctx); + return; + +error: + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, PLGD_DPS_HAS_CREDENTIALS, + PLGD_DPS_ERROR_GET_CREDENTIALS); +finish: + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) != 0) { + dps_provisioning_handle_failure(ctx, data->code, /*schedule_retry*/ true); + } +} + +/** + * @brief Request provisioning credentials. + * + * Prepare and send POST request to PLGD_DPS_CREDS_URI and register + * handler for response with credentials. + * + * @param ctx device registration context + * @return true POST request successfully dispatched + * @return false on failure + */ +static bool +dps_get_credentials(plgd_dps_context_t *ctx) +{ + DPS_INFO("Get credentials"); + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFNOP) { + return false; + } + if (!dps_pki_send_csr(ctx, dps_get_credentials_handler)) { + return false; + } + dps_set_ps_and_last_error(ctx, PLGD_DPS_GET_CREDENTIALS, + PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + return true; +} + +void +dps_provisioning_schedule_next_step(plgd_dps_context_t *ctx) +{ + dps_reset_delayed_callback(ctx, dps_provision_next_step_async, 0); +} + +static bool +dps_provision_next_step_time(plgd_dps_context_t *ctx) +{ + if (!dps_get_plgd_time(ctx)) { + DPS_ERR("Getting of DPS time failed"); + return false; + } + return true; +} + +static bool +dps_provision_next_step_owner(plgd_dps_context_t *ctx) +{ + if (!dps_get_owner(ctx)) { + DPS_ERR("Getting of DPS ownership failed"); + return false; + } + return true; +} + +static bool +dps_provision_next_step_cloud_configuration(plgd_dps_context_t *ctx) +{ + if (!dps_provisioning_set_cloud(ctx)) { + DPS_ERR("Get of cloud configuration failed"); + return false; + } + return true; +} + +static bool +dps_provision_next_step_credentials(plgd_dps_context_t *ctx) +{ + if (!dps_get_credentials(ctx)) { + DPS_ERR("Getting of DPS credentials failed"); + return false; + } + return true; +} + +static bool +dps_provision_next_step_acls(plgd_dps_context_t *ctx) +{ + if (!dps_get_acls(ctx)) { + DPS_ERR("Getting of DPS ACLs failed"); + return false; + } + return true; +} + +enum { + DPS_START_CLOUD_OK = 0, + DPS_START_CLOUD_MISSING_CERTIFICATES = -1, + DPS_START_CLOUD_FAILED = -2, +}; + +static int +dps_provision_next_step_start_cloud(plgd_dps_context_t *ctx) +{ + if (!oc_has_delayed_callback(ctx, dps_pki_renew_certificates_async, false)) { + // replacing of certificates was triggered and skipped in the meantime, we + // recheck all credentials to verify that they are still valid and to + // reschedule callback to replace expired certificates + DPS_DBG("renewal of certificates not scheduled, force recheck"); + if (!dps_check_credentials_and_schedule_renewal(ctx, 0)) { + DPS_ERR("Starting of cloud registration failed: %s", + "no valid certificates found"); + return DPS_START_CLOUD_MISSING_CERTIFICATES; + } + } + if (!dps_provisioning_start_cloud(ctx)) { + DPS_ERR("Starting of cloud registration failed"); + return DPS_START_CLOUD_FAILED; + } + return DPS_START_CLOUD_OK; +} + +plgd_dps_status_t +dps_provision_get_next_action(const plgd_dps_context_t *ctx) +{ + if ((ctx->status & PLGD_DPS_HAS_TIME) == 0) { + return PLGD_DPS_GET_TIME; + } + if ((ctx->status & PLGD_DPS_HAS_OWNER) == 0) { + return PLGD_DPS_GET_OWNER; + } + if ((ctx->status & PLGD_DPS_HAS_CLOUD) == 0) { + return PLGD_DPS_GET_CLOUD; + } + if ((ctx->status & PLGD_DPS_HAS_CREDENTIALS) == 0) { + return PLGD_DPS_GET_CREDENTIALS; + } + if ((ctx->status & PLGD_DPS_HAS_ACLS) == 0) { + return PLGD_DPS_GET_ACLS; + } + return 0; +} + +oc_event_callback_retval_t +dps_provision_next_step_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; + bool provisioned = false; + bool failure = false; + bool missing_certificates = false; + + if ((ctx->status & PLGD_DPS_HAS_TIME) == 0) { + failure = !dps_provision_next_step_time(ctx); + goto finish; + } + + if ((ctx->status & PLGD_DPS_HAS_OWNER) == 0) { + failure = !dps_provision_next_step_owner(ctx); + goto finish; + } + + if ((ctx->status & PLGD_DPS_HAS_CLOUD) == 0) { + failure = !dps_provision_next_step_cloud_configuration(ctx); + goto finish; + } + + if ((ctx->status & PLGD_DPS_HAS_CREDENTIALS) == 0) { + failure = !dps_provision_next_step_credentials(ctx); + goto finish; + } + + if ((ctx->status & PLGD_DPS_HAS_ACLS) == 0) { + failure = !dps_provision_next_step_acls(ctx); + goto finish; + } + + if (dps_is_provisioned(ctx)) { + provisioned = true; + if (!dps_is_provisioned_with_cloud_started(ctx)) { + int ret = dps_provision_next_step_start_cloud(ctx); + failure = ret != DPS_START_CLOUD_OK; + missing_certificates = ret == DPS_START_CLOUD_MISSING_CERTIFICATES; + goto finish; + } + } + +finish: + if (failure) { + ctx->status |= PLGD_DPS_FAILURE; + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + if (!provisioned) { + // if provisioning is not done then schedule retry in case of error or + // timeout + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + dps_retry_get_delay(&ctx->retry)); + } else if (missing_certificates) { + // we have to redo from the credentials step if certificates expired in + // the meantime + dps_set_ps_and_last_error(ctx, 0, + PLGD_DPS_GET_CREDENTIALS | + PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_GET_ACLS | + PLGD_DPS_HAS_ACLS, + ctx->last_error); + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + dps_retry_get_delay(&ctx->retry)); + } + return OC_EVENT_DONE; + } + if (provisioned) { + dps_set_has_been_provisioned_since_reset(ctx, true); + } + return OC_EVENT_DONE; +} + +oc_event_callback_retval_t +dps_provisioning_start_async(void *user_data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)user_data; +#define DPS_PROVISIONING_WAIT_INTERVAL_MS (500) + if (oc_reset_in_progress(plgd_dps_get_device(ctx))) { + DPS_DBG("reset in progress"); + dps_reset_delayed_callback_ms(ctx, dps_provisioning_start_async, + DPS_PROVISIONING_WAIT_INTERVAL_MS); + return OC_EVENT_DONE; + } + if (oc_process_is_closing_all_tls_sessions()) { + DPS_DBG( + "tls not ready, waiting for close all tls sessions event to finish"); + dps_reset_delayed_callback_ms(ctx, dps_provisioning_start_async, + DPS_PROVISIONING_WAIT_INTERVAL_MS); + return OC_EVENT_DONE; + } + dps_provisioning_schedule_next_step(ctx); + return OC_EVENT_DONE; +} + +void +dps_provisioning_start(plgd_dps_context_t *ctx) +{ +#if DPS_INFO_IS_ENABLED + // GCOVR_EXCL_START +#define ENDPOINT_STR_LEN 256 + char ep_str[ENDPOINT_STR_LEN] = { 0 }; +#undef ENDPOINT_STR_LEN + bool valid = dps_endpoint_log_string(ctx->endpoint, ep_str, sizeof(ep_str)); + DPS_INFO("Provisioning starting with %s", valid ? ep_str : "NULL"); + // GCOVR_EXCL_STOP +#endif /* DPS_INFO_IS_ENABLED */ + dps_reset_delayed_callback(ctx, dps_provisioning_start_async, 0); +} + +bool +dps_provisioning_start_cloud(plgd_dps_context_t *ctx) +{ + DPS_INFO("DPS provisioning steps finished successfully"); + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("Cloud context not found"); + return false; + } + + if (oc_cloud_manager_is_started(cloud_ctx)) { +#if DPS_INFO_IS_ENABLED + // GCOVR_EXCL_START + const oc_string_t *ep_str = oc_cloud_get_server_uri(cloud_ctx); + const char *ep_cstr = ep_str != NULL ? oc_string(*ep_str) : "NULL"; + DPS_INFO("Restarting cloud registration with endpoint(%s)", + ep_cstr != NULL ? ep_cstr : "NULL"); + // GCOVR_EXCL_STOP +#endif /* DPS_INFO_IS_ENABLED */ + oc_cloud_manager_restart(cloud_ctx); + goto finish; + } + +#if DPS_INFO_IS_ENABLED + // GCOVR_EXCL_START + const oc_string_t *ep_str = oc_cloud_get_server_uri(cloud_ctx); + const char *ep_cstr = ep_str != NULL ? oc_string(*ep_str) : "NULL"; + DPS_INFO("Starting cloud registration with endpoint(%s)", + ep_cstr != NULL ? ep_cstr : "NULL"); + // GCOVR_EXCL_STOP +#endif /* DPS_INFO_IS_ENABLED */ + if (oc_cloud_manager_start(cloud_ctx, ctx->callbacks.on_cloud_status_change, + ctx->callbacks.on_cloud_status_change_data) != 0) { + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, PLGD_DPS_CLOUD_STARTED, + PLGD_DPS_ERROR_START_CLOUD); + plgd_dps_force_reprovision(ctx); + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + dps_retry_get_delay(&ctx->retry)); + return false; + } + +finish: + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + dps_set_ps_and_last_error(ctx, PLGD_DPS_CLOUD_STARTED, + PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_store_dump_async(ctx); + dps_endpoint_close(ctx->endpoint); + dps_cloud_observer_on_provisioning_started(ctx, cloud_ctx); + return true; +} + +bool +dps_is_provisioned(const plgd_dps_context_t *ctx) +{ + return (ctx->status & + (PLGD_DPS_PROVISIONED_MASK | PLGD_DPS_PROVISIONED_ERROR_FLAGS)) == + PLGD_DPS_PROVISIONED_MASK; +} + +bool +dps_is_provisioned_with_cloud_started(const plgd_dps_context_t *ctx) +{ + if (!dps_is_provisioned(ctx)) { + return false; + } + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL || !oc_cloud_manager_is_started(cloud_ctx)) { + return false; + } + + return (ctx->status & PLGD_DPS_CLOUD_STARTED) != 0; +} + +/// Maximal number of allowed consecutive transient failures before full +/// reprovisioning is forced +const uint8_t DPS_MAX_TRANSIENT_RETRY_COUNT = 3; + +void +dps_provisioning_handle_failure(plgd_dps_context_t *ctx, oc_status_t code, + bool schedule_retry) +{ + bool reprovision = (ctx->status & PLGD_DPS_FAILURE) != 0; + if ((ctx->status & PLGD_DPS_TRANSIENT_FAILURE) != 0) { + ++ctx->transient_retry_count; + if (ctx->transient_retry_count >= DPS_MAX_TRANSIENT_RETRY_COUNT) { + DPS_DBG( + "transient retry count limit reached, forcing full reprovisioning"); + ctx->transient_retry_count = 0; + reprovision = true; + } + } + if (reprovision) { + plgd_dps_force_reprovision(ctx); + } + + if (schedule_retry) { + uint64_t interval = + dps_is_timeout_error_code(code) ? 0 : dps_retry_get_delay(&ctx->retry); + dps_reset_delayed_callback_ms(ctx, dps_manager_provision_retry_async, + interval); + } +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision_cloud.c b/api/plgd/device-provisioning-client/plgd_dps_provision_cloud.c new file mode 100644 index 0000000000..110bfd438b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_cloud.c @@ -0,0 +1,407 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_cloud_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_provision_cloud_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_internal.h" + +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/cloud/oc_cloud_manager_internal.h" +#include "oc_api.h" +#include "oc_cloud.h" +#include "oc_cloud_access.h" +#include "oc_core_res.h" +#include "oc_rep.h" +#include "oc_ri.h" +#include "security/oc_pstat_internal.h" + +#include + +#define PLGD_DPS_CLOUD_URI "/api/v1/provisioning/cloud-configuration" + +bool +dps_register_cloud_fill_data(const oc_rep_t *payload, cloud_conf_t *cloud) +{ + const oc_rep_t *act = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, DPS_CLOUD_ACCESSTOKEN, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ACCESSTOKEN)); + if (act == NULL) { + DPS_ERR("key(%s) missing in %s response", DPS_CLOUD_ACCESSTOKEN, + PLGD_DPS_CLOUD_URI); + return false; + } + const oc_rep_t *apn = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, DPS_CLOUD_AUTHPROVIDER, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_AUTHPROVIDER)); + if (apn == NULL) { + DPS_ERR("key(%s) missing in %s response", DPS_CLOUD_AUTHPROVIDER, + PLGD_DPS_CLOUD_URI); + return false; + } + const oc_rep_t *cis = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, DPS_CLOUD_CISERVER, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_CISERVER)); + if (cis == NULL) { + DPS_ERR("key(%s) missing in %s response", DPS_CLOUD_CISERVER, + PLGD_DPS_CLOUD_URI); + return false; + } + const oc_rep_t *sid = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, DPS_CLOUD_SERVERID, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_SERVERID)); + if (sid == NULL) { + DPS_ERR("key(%s) missing in %s response", DPS_CLOUD_SERVERID, + PLGD_DPS_CLOUD_URI); + return false; + } + cloud->access_token = &act->value.string; + cloud->auth_provider = &apn->value.string; + cloud->ci_server = &cis->value.string; + cloud->sid = &sid->value.string; + + const oc_rep_t *servers = oc_rep_get_by_type_and_key( + payload, OC_REP_OBJECT_ARRAY, DPS_CLOUD_ENDPOINTS, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ENDPOINTS)); + if (servers != NULL) { + cloud->ci_servers = servers->value.object_array; +#if DPS_DBG_IS_ENABLED + // GCOVR_EXCL_START + for (const oc_rep_t *server = cloud->ci_servers; server != NULL; + server = server->next) { + const oc_rep_t *rep = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, DPS_CLOUD_ENDPOINT_URI, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ENDPOINT_URI)); + oc_string_view_t uriv = { 0 }; + if (rep != NULL) { + uriv = oc_string_view2(&rep->value.string); + } + rep = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, DPS_CLOUD_ENDPOINT_ID, + OC_CHAR_ARRAY_LEN(DPS_CLOUD_ENDPOINT_ID)); + oc_string_view_t idv = { 0 }; + if (rep != NULL) { + idv = oc_string_view2(&rep->value.string); + } + DPS_DBG("cloud server: uri(%s) id(%s)", + uriv.data != NULL ? uriv.data : "NULL", + idv.data != NULL ? idv.data : "NULL"); + } + // GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + } + + return true; +} + +static void +cloud_deregister_handler(oc_client_response_t *resp) +{ +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + DPS_DBG("cloud deregister handler"); + if (resp->code == OC_STATUS_DELETED) { + DPS_DBG("cloud deregistered"); + } else { + DPS_ERR("cloud deregister failed"); + } + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + // close hijacked session + oc_close_session(resp->endpoint); +} + +static bool +cloud_deregister(const oc_cloud_context_t *cloud_ctx, uint16_t timeout) +{ + DPS_DBG("try deregister device %zu by DELETE request", + oc_cloud_get_device(cloud_ctx)); + + const oc_endpoint_t *cloud_ep = oc_cloud_get_server(cloud_ctx); + assert(cloud_ep != NULL); // should always be allocated at this point + if (oc_endpoint_is_empty(cloud_ep)) { + return false; + } + oc_cloud_access_conf_t conf = { + .endpoint = cloud_ep, + .device = oc_cloud_get_device(cloud_ctx), + .selected_identity_cred_id = oc_cloud_get_identity_cert_chain(cloud_ctx), + .handler = cloud_deregister_handler, + .user_data = NULL, + .timeout = timeout, + }; + return oc_cloud_access_deregister( + conf, oc_string(*oc_cloud_get_user_id(cloud_ctx)), NULL); +} + +plgd_dps_error_t +dps_handle_set_cloud_response(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; + cloud_conf_t cloud; + memset(&cloud, 0, sizeof(cloud)); + if (!dps_register_cloud_fill_data(data->payload, &cloud)) { + DPS_ERR("cannot parse configure cloud response for device(%zu)", + ctx->device); + return PLGD_DPS_ERROR_RESPONSE; + } + + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("cannot get cloud context for device(%zu)", ctx->device); + return PLGD_DPS_ERROR_SET_CLOUD; + } + + if ((oc_cloud_get_status(cloud_ctx) & OC_CLOUD_LOGGED_IN) != 0) { + const oc_string_t *cloud_ctx_cis = oc_cloud_get_server_uri(cloud_ctx); + const oc_uuid_t *cloud_ctx_sid = oc_cloud_get_server_id(cloud_ctx); + if (cloud_ctx_cis == NULL || cloud_ctx_sid == NULL) { + DPS_ERR("cannot get cloud server for device(%zu)", ctx->device); + return PLGD_DPS_ERROR_SET_CLOUD; + } + oc_string_view_t sidv = oc_string_view2(cloud.sid); + oc_uuid_t sid; + oc_str_to_uuid_v1(sidv.data, sidv.length, &sid); + const oc_string_t *cloud_apn = + oc_cloud_get_authorization_provider_name(cloud_ctx); + if (dps_is_equal_string(*cloud_ctx_cis, *cloud.ci_server) && + oc_uuid_is_equal(*cloud_ctx_sid, sid) && cloud_apn != NULL && + dps_is_equal_string(*cloud_apn, *cloud.auth_provider)) { + DPS_DBG("cloud configuration is already set for device(%zu)", + ctx->device); + return PLGD_DPS_OK; + } + + // deregister device from old cloud + if (!oc_uuid_is_equal(*cloud_ctx_sid, sid)) { + DPS_DBG("deregister device(%zu) from old cloud", ctx->device); + if (cloud_deregister(cloud_ctx, 3)) { + // connection is closed in cloud_deregister_handler, so we hijack the + // connection and reset the cloud context + DPS_DBG("deregister has been sent, hijack the connection"); + cloud_ctx->cloud_ep_state = OC_SESSION_DISCONNECTED; + memset(cloud_ctx->cloud_ep, 0, sizeof(oc_endpoint_t)); + oc_cloud_context_clear(cloud_ctx, true); + } else { + DPS_ERR("failed to deregister device(%zu) from old cloud", ctx->device); + } + } else { + DPS_DBG("deregister device(%zu) from old cloud is not executed because: " + "cloud id(%s) has not been changed", + ctx->device, sidv.data != NULL ? sidv.data : "(NULL)"); + } + } + + // stop the cloud, otherwise oc_cloud_provision_conf_resource would restart + // the cloud automatically, with the new configuration, but we want to wait + // for the DPS reprovisioning to be done and then start it manually + oc_cloud_manager_stop_v1(cloud_ctx, false); + dps_cloud_observer_deinit(ctx); + + const char *ci_server = oc_string(*cloud.ci_server); + const char *access_token = oc_string(*cloud.access_token); + const char *sid = oc_string(*cloud.sid); + const char *auth_provider = oc_string(*cloud.auth_provider); + DPS_DBG("cloud configuration:"); + DPS_DBG("\tserver: %s", ci_server != NULL ? ci_server : ""); + DPS_DBG("\taccess_token: %s", access_token != NULL ? access_token : ""); + DPS_DBG("\tsid: %s", sid != NULL ? sid : ""); + DPS_DBG("\tauth_provider: %s", auth_provider != NULL ? auth_provider : ""); + + if (oc_cloud_provision_conf_resource(cloud_ctx, ci_server, access_token, sid, + auth_provider) != 0) { + DPS_ERR("failed to configure cloud for device(%zu)", ctx->device); + return PLGD_DPS_ERROR_SET_CLOUD; + } + dps_cloud_add_servers(cloud_ctx, cloud.ci_servers); + return PLGD_DPS_OK; +} + +bool +dps_has_cloud_configuration(size_t device) +{ + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(device); + if (cloud_ctx == NULL) { + return false; + } + bool has_server = oc_cloud_get_server_uri(cloud_ctx) != NULL; + bool has_access_token = + !oc_string_is_empty(oc_cloud_get_access_token(cloud_ctx)); + return has_server && has_access_token; +} + +void +dps_set_cloud_handler(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; +#if DPS_DBG_IS_ENABLED + dps_print_status("set cloud handler: ", ctx->status); +#endif /* DPS_DBG_IS_ENABLED */ + // we check only for PLGD_DPS_FAILURE flag, because retry will be rescheduled + // if necessary + if ((ctx->status & (PLGD_DPS_HAS_CLOUD | PLGD_DPS_FAILURE)) == + PLGD_DPS_HAS_CLOUD) { + DPS_DBG("skipping duplicit call of set cloud handler"); + return; + } + + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + ctx->status &= ~PLGD_DPS_PROVISIONED_ERROR_FLAGS; + + plgd_dps_error_t err = PLGD_DPS_ERROR_SET_CLOUD; + const uint32_t expected_status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | + PLGD_DPS_HAS_OWNER | PLGD_DPS_GET_CLOUD; + if (ctx->status != expected_status) { +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(ctx->status, str, sizeof(str)); + DPS_ERR("invalid status(%u:%s) in set cloud handler", (unsigned)ctx->status, + ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + goto error; + } + + int ret = dps_provisioning_check_response(ctx, data->code, data->payload); + if (ret != 0) { + DPS_ERR("invalid %s response", PLGD_DPS_CLOUD_URI); + // ctx->status and ctx->last_error are set in + // dps_provisioning_check_response + dps_provisioning_handle_failure(ctx, data->code, /*schedule_retry*/ true); + return; + } + + err = dps_handle_set_cloud_response(data); + if (err != PLGD_DPS_OK) { + goto error; + } + + DPS_INFO("Cloud configuration set successfully"); + dps_set_ps_and_last_error( + ctx, PLGD_DPS_HAS_CLOUD, + PLGD_DPS_GET_CLOUD | PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + + // go to next step -> get credentials + dps_provisioning_schedule_next_step(ctx); + return; + +error: + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, 0, err); +} + +bool +dps_provisioning_set_cloud_encode_selected_gateway( + const plgd_dps_context_t *ctx) +{ + const oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + if (cloud_ctx == NULL) { + DPS_ERR("cannot get cloud context for device(%zu)", ctx->device); + return false; + } + + const oc_endpoint_address_t *selected_gateway = + oc_cloud_selected_server_address(cloud_ctx); + if (selected_gateway == NULL) { + return true; + } + + const oc_string_t *selected_uri = oc_endpoint_address_uri(selected_gateway); + assert(selected_uri != NULL); + + const oc_uuid_t *selected_uuid = oc_endpoint_address_uuid(selected_gateway); + assert(selected_uuid != NULL); + char uuid[OC_UUID_LEN] = { 0 }; + int uuid_len = oc_uuid_to_str_v1(selected_uuid, uuid, OC_UUID_LEN); + assert(uuid_len > 0); + + // { + // uri: ${uri}, + // id: ${uuid} + // } + oc_rep_open_object(root, selectedGateway); + oc_rep_set_text_string_v1(selectedGateway, uri, oc_string(*selected_uri), + oc_string_len_unsafe(*selected_uri)); + oc_rep_set_text_string_v1(selectedGateway, id, uuid, (size_t)uuid_len); + oc_rep_close_object(root, selectedGateway); + return oc_rep_get_cbor_errno() == CborNoError; +} + +bool +dps_provisioning_set_cloud_encode_payload(const plgd_dps_context_t *ctx) +{ + const oc_uuid_t *device_id = oc_core_get_device_id(ctx->device); + if (device_id == NULL) { + DPS_ERR("failed to get device id"); + return false; + } + char uuid[OC_UUID_LEN] = { 0 }; + int uuid_len = oc_uuid_to_str_v1(device_id, uuid, OC_UUID_LEN); + assert(uuid_len > 0); + + oc_rep_start_root_object(); + oc_rep_set_text_string_v1(root, di, uuid, uuid_len); + if (!dps_provisioning_set_cloud_encode_selected_gateway(ctx)) { + return false; + } + oc_rep_end_root_object(); + + return oc_rep_get_cbor_errno() == CborNoError; +} + +bool +dps_provisioning_set_cloud(plgd_dps_context_t *ctx) +{ + DPS_INFO("Get cloud configuration"); + assert(ctx->endpoint != NULL); + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFNOP) { + DPS_ERR("device is not in RFNOP state"); + return false; + } + + if (!oc_init_post(PLGD_DPS_CLOUD_URI, ctx->endpoint, NULL, + dps_set_cloud_handler, LOW_QOS, ctx)) { + DPS_ERR("could not init POST request to %s", PLGD_DPS_CLOUD_URI); + return false; + } + + if (!dps_provisioning_set_cloud_encode_payload(ctx)) { + DPS_ERR("could not encode payload for POST request to %s", + PLGD_DPS_CLOUD_URI); + return false; + } + + dps_setup_tls(ctx); + if (!oc_do_post_with_timeout(dps_retry_get_timeout(&ctx->retry))) { + dps_reset_tls(); + DPS_ERR("failed to dispatch POST request to %s", PLGD_DPS_CLOUD_URI); + return false; + } + dps_set_ps_and_last_error(ctx, PLGD_DPS_GET_CLOUD, 0, PLGD_DPS_OK); + return true; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h b/api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h new file mode 100644 index 0000000000..9a5cd12387 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h @@ -0,0 +1,107 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_PROVISION_CLOUD_INTERNAL_H +#define PLGD_DPS_PROVISION_CLOUD_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_config.h" +#include "oc_rep.h" +#include "util/oc_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Configure cloud resource. + * + * @param ctx device context (cannot be NULL) + * @return true on success + * @return false on failure + */ +bool dps_provisioning_set_cloud(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** @brief Handle POST response from PLGD_DPS_CLOUD_URI. */ +void dps_set_cloud_handler(oc_client_response_t *data) OC_NONNULL(); + +/** @brief Check if cloud configuration has been set. */ +bool dps_has_cloud_configuration(size_t device) OC_NONNULL(); + +typedef struct cloud_conf_t +{ + const oc_string_t + *access_token; /**< Access Token resolved with an auth code. */ + const oc_string_t *auth_provider; /**< Auth Provider ID*/ + const oc_string_t *ci_server; /**< Selected Cloud Interface Server URL to + which an Enrollee is going to register. */ + const oc_string_t + *sid; /**< OCF Cloud Identity as defined in OCF CNC 2.0 Spec. */ + const oc_rep_t *ci_servers; /**< List of all Cloud Interface Server URLs. */ +} cloud_conf_t; + +/** + * @brief Parse payload into cloud configuration. + * + * @param[in] payload payload to parse (cannot be NULL) + * @param[out] cloud output cloud configuration (cannot be NULL) + * @return true on success + * @return false otherwise + */ +bool dps_register_cloud_fill_data(const oc_rep_t *payload, cloud_conf_t *cloud) + OC_NONNULL(); + +/** + * @brief Encode selected gateway. + * + * @param ctx device context (cannot be NULL) + * @return true on success + * @return false otherwise + */ +bool dps_provisioning_set_cloud_encode_selected_gateway( + const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Encode cloud configuration request payload. + * + * @param ctx device context (cannot be NULL) + * @return true on success + * @return false otherwise + */ +bool dps_provisioning_set_cloud_encode_payload(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Handle cloud configuration response. + * + * @param data response data (cannot be NULL) + * @return plgd_dps_error_t + */ +plgd_dps_error_t dps_handle_set_cloud_response(oc_client_response_t *data) + OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_PROVISION_CLOUD_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision_internal.h b/api/plgd/device-provisioning-client/plgd_dps_provision_internal.h new file mode 100644 index 0000000000..e991aed9ef --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_internal.h @@ -0,0 +1,104 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_PROVISION_INTERNAL_H +#define PLGD_DPS_PROVISION_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_client_state.h" // oc_client_response_t +#include "oc_config.h" +#include "util/oc_compiler.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Check DPS service response to a request during provisioning. + * + * @param ctx device context (cannot be NULL) + * @param code response status code + * @param payload payload to check for redirect + * @return 0 on success + * @return -1 on error + */ +OC_NO_DISCARD_RETURN +int dps_provisioning_check_response(plgd_dps_context_t *ctx, oc_status_t code, + const oc_rep_t *payload) OC_NONNULL(1); + +/** + * @brief Starting executing missing DPS provisioning steps. + * + * @param ctx device provisioning context (cannot be NULL) + */ +void dps_provisioning_start(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Callback to start executing DPS provisioning. +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_provisioning_start_async(void *user_data) + OC_NONNULL(); + +/** + * @brief Schedule next step in DPS provisioning. + * + * @param ctx device provisioning context (cannot be NULL) + */ +void dps_provisioning_schedule_next_step(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Callback to start executing next step DPS provisioning. +OC_NO_DISCARD_RETURN +oc_event_callback_retval_t dps_provision_next_step_async(void *user_data) + OC_NONNULL(); + +/** + * @brief Finish DPS provisioning and start cloud manager. + * + * @param ctx device provisioning context (cannot be NULL) + * @return true on success + * @return false on error + */ +OC_NO_DISCARD_RETURN +bool dps_provisioning_start_cloud(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Check if all provisioning steps have been successfully executed so +/// cloud can be started. +OC_NO_DISCARD_RETURN +bool dps_is_provisioned(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Check if provisioning of the device is finished and cloud is started. +OC_NO_DISCARD_RETURN +bool dps_is_provisioned_with_cloud_started(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/// @brief Handle failure of a provisioning step +void dps_provisioning_handle_failure(plgd_dps_context_t *ctx, oc_status_t code, + bool schedule_retry) OC_NONNULL(); + +/// @brief Return next provisioning step to be executed. +plgd_dps_status_t dps_provision_get_next_action(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_PROVISION_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision_owner.c b/api/plgd/device-provisioning-client/plgd_dps_provision_owner.c new file mode 100644 index 0000000000..bd69492cad --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_owner.c @@ -0,0 +1,208 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_provision_owner_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_internal.h" + +#include "oc_acl.h" +#include "oc_api.h" +#include "oc_client_state.h" +#include "oc_cred.h" +#include "oc_helpers.h" +#include "oc_rep.h" +#include "oc_uuid.h" +#include "security/oc_doxm_internal.h" +#include "security/oc_pstat_internal.h" +#include "util/oc_macros_internal.h" + +#include + +static int +dps_handle_get_owner_response(oc_client_response_t *data) +{ + const char *owner_str = NULL; + oc_rep_t *rep = data->payload; + while (rep != NULL) { + if (dps_is_property(rep, OC_REP_STRING, "devowneruuid", + OC_CHAR_ARRAY_LEN("devowneruuid"))) { + owner_str = oc_string(rep->value.string); + rep = rep->next; + continue; + } + + DPS_ERR("unexpected property(%s)", oc_string(rep->name)); + return -1; + } + + if (owner_str == NULL) { + DPS_ERR("owner not found"); + return -1; + } + + oc_uuid_t owner; + oc_str_to_uuid(owner_str, &owner); + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; + if (!dps_set_owner(ctx, &owner)) { + DPS_ERR("cannot own device"); + return -1; + } + DPS_DBG("device owner set to %s", owner_str); + return 0; +} + +static void +dps_get_owner_handler(oc_client_response_t *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data->user_data; +#if DPS_DBG_IS_ENABLED + dps_print_status("get owner handler: ", ctx->status); +#endif /* DPS_DBG_IS_ENABLED */ + // we check only for PLGD_DPS_FAILURE flag, because retry will be rescheduled + // if necessary + if ((ctx->status & (PLGD_DPS_HAS_OWNER | PLGD_DPS_FAILURE)) == + PLGD_DPS_HAS_OWNER) { + DPS_DBG("skipping duplicit call of get owner handler"); + return; + } + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + ctx->status &= ~PLGD_DPS_PROVISIONED_ERROR_FLAGS; + + uint32_t expected_status = + PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_GET_OWNER; + if (ctx->status != expected_status) { +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(ctx->status, str, sizeof(str)); + DPS_ERR("invalid status(%u:%s) in get owner handler", (unsigned)ctx->status, + ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + goto error; + } + + int ret = dps_provisioning_check_response(ctx, data->code, data->payload); + if (ret != 0) { + DPS_ERR("invalid %s response(code=%d)", PLGD_DPS_OWNERSHIP_URI, data->code); + // ctx->status and ctx->last_error are set in + // dps_provisioning_check_response + goto finish; + } + + ret = dps_handle_get_owner_response(data); + if (ret != 0) { + goto error; + } + + DPS_INFO("Owner set successfully"); + dps_set_ps_and_last_error( + ctx, PLGD_DPS_HAS_OWNER, + PLGD_DPS_GET_OWNER | PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + +#if DPS_DBG_IS_ENABLED + dps_print_owner(ctx->device); +#endif /* DPS_DBG_IS_ENABLED */ + + // go to next step -> get cloud configuration + dps_provisioning_schedule_next_step(ctx); + return; + +error: + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, PLGD_DPS_HAS_OWNER, + PLGD_DPS_ERROR_GET_OWNER); +finish: + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) != 0) { + dps_provisioning_handle_failure(ctx, data->code, /*schedule_retry*/ true); + } +} + +/** + * @brief Request ownership UUID. + * + * Prepare and send GET request to PLGD_DPS_OWNERSHIP_URI and register + * handler for response with ownership data. + * + * @param ctx device registration context + * @return true POST request successfully dispatched + * @return false on failure + */ +bool +dps_get_owner(plgd_dps_context_t *ctx) +{ + DPS_INFO("Get owner"); + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFNOP) { + return false; + } + + dps_setup_tls(ctx); + if (!oc_do_get_with_timeout(PLGD_DPS_OWNERSHIP_URI, ctx->endpoint, NULL, + dps_retry_get_timeout(&ctx->retry), + dps_get_owner_handler, LOW_QOS, ctx)) { + DPS_ERR("failed to dispatch GET request to %s", PLGD_DPS_OWNERSHIP_URI); + dps_reset_tls(); + return false; + } + dps_set_ps_and_last_error(ctx, PLGD_DPS_GET_OWNER, + PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + return true; +} + +#if DPS_DBG_IS_ENABLED + +void +dps_print_owner(size_t device) +{ + // GCOVR_EXCL_START + DPS_DBG("owner:"); + const oc_sec_doxm_t *doxm = oc_sec_get_doxm(device); + char deviceuuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&doxm->deviceuuid, deviceuuid, sizeof(deviceuuid)); + char devowneruuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&doxm->devowneruuid, devowneruuid, sizeof(devowneruuid)); + char rowneruuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&doxm->rowneruuid, rowneruuid, sizeof(rowneruuid)); + DPS_DBG("\tdoxm: deviceuuid=%s devowneruuid=%s rowneruuid=%s", deviceuuid, + devowneruuid, rowneruuid); + + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(device); + oc_uuid_to_str(&pstat->rowneruuid, rowneruuid, sizeof(rowneruuid)); + DPS_DBG("\tpstat: rowneruuid=%s", rowneruuid); + + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + oc_uuid_to_str(&creds->rowneruuid, rowneruuid, sizeof(rowneruuid)); + DPS_DBG("\tcreds: rowneruuid=%s", rowneruuid); + + const oc_sec_acl_t *acls = oc_sec_get_acl(device); + oc_uuid_to_str(&acls->rowneruuid, rowneruuid, sizeof(rowneruuid)); + DPS_DBG("\tacls: rowneruuid=%s", rowneruuid); + // GCOVR_EXCL_STOP +} + +#endif /* DPS_DBG_IS_ENABLED */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h b/api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h new file mode 100644 index 0000000000..54e325bc5b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h @@ -0,0 +1,59 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_PROVISION_OWNER_INTERNAL_H +#define PLGD_DPS_PROVISION_OWNER_INTERNAL_H + +#include "plgd/plgd_dps.h" +#include "plgd_dps_log_internal.h" + +#include "util/oc_compiler.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PLGD_DPS_OWNERSHIP_URI "/api/v1/provisioning/ownership" + +/** + * @brief Request ownership UUID. + * + * Prepare and send GET request to PLGD_DPS_OWNERSHIP_URI and register + * handler for response with ownership data. + * + * @param ctx device registration context (cannot be NULL) + * @return true POST request successfully dispatched + * @return false on failure + */ +bool dps_get_owner(plgd_dps_context_t *ctx) OC_NONNULL(); + +#if DPS_DBG_IS_ENABLED + +/// @brief Print owner of device, pstat and doxm resources, acls and +/// credentials. +void dps_print_owner(size_t device); + +#endif /* DPS_DBG_IS_ENABLED */ + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_PROVISION_OWNER_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_resource.c b/api/plgd/device-provisioning-client/plgd_dps_resource.c new file mode 100644 index 0000000000..68454a5206 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_resource.c @@ -0,0 +1,592 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_endpoints_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_resource_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_store_internal.h" // dps_store_dump_async +#include "plgd_dps_internal.h" + +#include "api/cloud/oc_cloud_schedule_internal.h" +#include "oc_api.h" +#include "oc_config.h" +#include "oc_core_res.h" +#include "oc_pki.h" +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_compiler.h" +#include "util/oc_list.h" +#include "util/oc_macros_internal.h" +#include "util/oc_memb.h" + +#include +#include +#include +#include +#include +#include + +#define PLGD_DPS_RES_TYPE "x.plgd.dps.conf" +#define PLGD_DPS_ENDPOINT "endpoint" /**< endpoint */ +#define PLGD_DPS_ENDPOINT_NAME \ + "endpointName" /**< name associated with the endpoint */ +#define PLGD_DPS_ENDPOINTS "endpoints" /**< list of endpoints */ +#define PLGD_DPS_LAST_ERROR_CODE "lastErrorCode" /**< last error code */ +#define PLGD_DPS_FORCE_REPROVISION \ + "forceReprovision" /**< connect to dps service and reprovision time, owner, \ + cloud configuration credentials and acls .*/ + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + +#define PLGD_DPS_TEST_PROPERTIES "test" /**< test properties */ +#define PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER \ + "cloudStatusObserver" /**< cloud status observer configuration */ +#define PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER_MAX_COUNT \ + "maxCount" /**< max count */ +#define PLGD_DPS_TEST_IOTIVITY "iotivity" /**< iotivity configuration */ +#define PLGD_DPS_TEST_IOTIVITY_RETRY "retry" /**< retry configuration */ + +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + +typedef struct +{ + const oc_string_t *endpoint; + const oc_string_t *endpointName; + const oc_rep_t *endpoints; + bool restart; +} dps_conf_update_t; + +typedef struct dps_conf_update_data_t +{ + struct dps_update_conf_data_t *next; + + size_t device; + bool factory_reset; +} dps_conf_update_data_t; + +#ifndef OC_DYNAMIC_ALLOCATION +OC_MEMB(g_dps_conf_update_data_s, dps_conf_update_data_t, 2); +#endif /* !OC_DYNAMIC_ALLOCATION */ + +OC_LIST(g_dps_update_data_list); + +static dps_conf_update_data_t *dps_update_list_insert(size_t device, + bool factory_reset); + +const char * +dps_status_to_str(uint32_t status) +{ + if ((status & PLGD_DPS_FAILURE) != 0) { + return kPlgdDpsStatusFailure; + } + if ((status & PLGD_DPS_TRANSIENT_FAILURE) != 0) { + return kPlgdDpsStatusTransientFailure; + } + if (status == 0) { + return kPlgdDpsStatusUninitialized; + } + if (status == PLGD_DPS_INITIALIZED) { + return kPlgdDpsStatusInitialized; + } + if (status == (PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME)) { + return kPlgdDpsStatusGetTime; + } + const uint32_t has_time = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME; + if (status == has_time) { + return kPlgdDpsStatusHasTime; + } + if (status == (has_time | PLGD_DPS_GET_OWNER)) { + return kPlgdDpsStatusGetOwner; + } + const uint32_t has_owner = has_time | PLGD_DPS_HAS_OWNER; + if (status == has_owner) { + return kPlgdDpsStatusHasOwner; + } + if (status == (has_owner | PLGD_DPS_GET_CLOUD)) { + return kPlgdDpsStatusGetCloud; + } + const uint32_t has_cloud = has_owner | PLGD_DPS_HAS_CLOUD; + if (status == has_cloud) { + return kPlgdDpsStatusHasCloud; + } + if (status == (has_cloud | PLGD_DPS_GET_CREDENTIALS)) { + return kPlgdDpsStatusGetCredentials; + } + const uint32_t has_creds = has_cloud | PLGD_DPS_HAS_CREDENTIALS; + if (status == has_creds) { + return kPlgdDpsStatusHasCredentials; + } + if (status == (has_creds | PLGD_DPS_GET_ACLS)) { + return kPlgdDpsStatusGetAcls; + } + const uint32_t has_acls = has_creds | PLGD_DPS_HAS_ACLS; + if (status == has_acls) { + return kPlgdDpsStatusHasAcls; + } + const uint32_t cloud_started = has_acls | PLGD_DPS_CLOUD_STARTED; + if (status == cloud_started) { + return kPlgdDpsStatusProvisioned; + } + if (status == (cloud_started | PLGD_DPS_RENEW_CREDENTIALS)) { + return kPlgdDpsStatusRenewCredentials; + } + return NULL; +} + +static void +dps_resource_encode_endpoints(CborEncoder *encoder, + const oc_endpoint_addresses_t *endpoints) +{ + const oc_endpoint_address_t *selected = + oc_endpoint_addresses_selected(endpoints); + oc_string_view_t epname_key = OC_STRING_VIEW(PLGD_DPS_ENDPOINT_NAME); + oc_endpoint_address_view_t eav; + if (selected != NULL) { + eav = oc_endpoint_address_view(selected); + } else { + epname_key = OC_STRING_VIEW_NULL; // ignore endpointName + eav = oc_endpoint_address_make_view_with_name(OC_STRING_VIEW_NULL, + OC_STRING_VIEW_NULL); + } + g_err |= + oc_endpoint_address_encode(encoder, OC_STRING_VIEW(PLGD_DPS_ENDPOINT), + OC_STRING_VIEW_NULL, epname_key, eav); + g_err |= oc_endpoint_addresses_encode( + encoder, endpoints, OC_STRING_VIEW(PLGD_DPS_ENDPOINTS), true); +} + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + +static void +dps_resource_encode_cloud_observer(CborEncoder *encoder, + const dps_resource_test_data_t *rtd) +{ + oc_string_view_t key = OC_STRING_VIEW(PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER); + g_err |= oc_rep_encode_text_string(encoder, key.data, key.length); + oc_rep_begin_object(encoder, cloudStatusObserver); + oc_rep_set_int(cloudStatusObserver, maxCount, + rtd->cloud_status_observer.max_count); + oc_rep_set_int(cloudStatusObserver, interval, + rtd->cloud_status_observer.interval_s); + oc_rep_end_object(encoder, cloudStatusObserver); +} + +static void +dps_resource_encode_iotivity_retry_timeouts(CborEncoder *encoder, + const dps_resource_test_data_t *rtd) +{ + oc_string_view_t retryKey = OC_STRING_VIEW(PLGD_DPS_TEST_IOTIVITY_RETRY); + g_err |= oc_rep_encode_text_string(encoder, retryKey.data, retryKey.length); + oc_rep_begin_array(encoder, retry); + for (size_t i = 0; i < DPS_ARRAY_SIZE(rtd->iotivity.retry_timeout); ++i) { + if (rtd->iotivity.retry_timeout[i] == 0) { + break; + } + oc_rep_add_int(retry, rtd->iotivity.retry_timeout[i]); + } + oc_rep_end_array(encoder, retry); +} + +static void +dps_resource_encode_iotivity(CborEncoder *encoder, + const dps_resource_test_data_t *rtd) +{ + oc_string_view_t iotKey = OC_STRING_VIEW(PLGD_DPS_TEST_IOTIVITY); + g_err |= oc_rep_encode_text_string(encoder, iotKey.data, iotKey.length); + oc_rep_begin_object(encoder, iotivity); + dps_resource_encode_iotivity_retry_timeouts(oc_rep_object(iotivity), rtd); + oc_rep_end_object(encoder, iotivity); +} + +static void +dps_resource_encode_test_properties(CborEncoder *encoder, + const dps_resource_test_data_t *rtd) +{ + dps_resource_encode_cloud_observer(encoder, rtd); + dps_resource_encode_iotivity(encoder, rtd); +} + +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + +void +dps_resource_encode(oc_interface_mask_t interface, + const oc_resource_t *resource, + const dps_resource_data_t *data) +{ + oc_rep_start_root_object(); + switch (interface) { + case OC_IF_BASELINE: + oc_process_baseline_interface(resource); + OC_FALLTHROUGH; + case OC_IF_R: + oc_rep_set_int(root, lastErrorCode, (int64_t)data->last_error); + if (data->provision_status != NULL) { + oc_rep_set_text_string_v1(root, provisionStatus, data->provision_status, + data->provision_status_length); + } + OC_FALLTHROUGH; + case OC_IF_RW: { + dps_resource_encode_endpoints(oc_rep_object(root), data->endpoints); + oc_rep_set_boolean(root, forceReprovision, data->forceReprovision); +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + oc_rep_open_object(root, test); + dps_resource_encode_test_properties(oc_rep_object(test), &data->test); + oc_rep_close_object(root, test); +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + OC_FALLTHROUGH; + } + default: + break; + } + oc_rep_end_root_object(); +} + +static void +dps_resource_encode_response(const plgd_dps_context_t *ctx, + oc_interface_mask_t interface) +{ + const char *status = dps_status_to_str(ctx->status); + size_t status_length = 0; + if (status != NULL) { + status_length = strlen(status); + } + dps_resource_data_t data = { + .last_error = ctx->last_error, + .provision_status = status, + .provision_status_length = status_length, + .endpoints = &ctx->store.endpoints, + .forceReprovision = false, + }; + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + data.test.cloud_status_observer = ctx->cloud_observer.cfg; + oc_cloud_get_retry_timeouts(&data.test.iotivity.retry_timeout[0], + DPS_ARRAY_SIZE(data.test.iotivity.retry_timeout)); +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + + dps_resource_encode(interface, ctx->conf, &data); +} + +static void +get_dps(oc_request_t *request, oc_interface_mask_t interface, void *user_data) +{ + (void)user_data; + (void)interface; + const plgd_dps_context_t *ctx = + plgd_dps_get_context(request->resource->device); + if (ctx == NULL) { + oc_send_response(request, OC_STATUS_INTERNAL_SERVER_ERROR); + return; + } + DPS_DBG("GET request received"); + dps_resource_encode_response(ctx, interface); + oc_send_response(request, OC_STATUS_OK); +} + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + +static void +dps_update_iotivity(const oc_rep_t *iot) +{ + DPS_DBG("DPS test properties iotivity update"); + int64_t *retry = NULL; + size_t retry_size; + if (oc_rep_get_int_array(iot, PLGD_DPS_TEST_IOTIVITY_RETRY, &retry, + &retry_size)) { + uint16_t retry_timeout[DPS_CLOUD_RETRY_TIMEOUTS_SIZE] = { 0 }; + for (size_t i = 0; i < retry_size && i < DPS_ARRAY_SIZE(retry_timeout); + ++i) { + retry_timeout[i] = (uint16_t)retry[i]; + } + if (!oc_cloud_set_retry_timeouts(retry_timeout, (uint8_t)retry_size)) { + DPS_ERR("failed to set retry timeouts"); + } + } +} + +static void +dps_update_cloud_observer(plgd_dps_context_t *ctx, const oc_rep_t *cso) +{ + DPS_DBG("DPS test properties cloud status observer update"); + int64_t val; + if (oc_rep_get_int(cso, PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER_MAX_COUNT, + &val)) { + plgd_dps_set_cloud_observer_configuration( + ctx, (uint8_t)val, ctx->cloud_observer.cfg.interval_s); + } +} + +static bool +dps_update_test_properties(plgd_dps_context_t *ctx, const oc_rep_t *payload) +{ + const oc_rep_t *testProps = + oc_rep_get_by_type_and_key(payload, OC_REP_OBJECT, PLGD_DPS_TEST_PROPERTIES, + OC_CHAR_ARRAY_LEN(PLGD_DPS_TEST_PROPERTIES)); + if (testProps == NULL) { + return false; + } + DPS_DBG("DPS test properties update"); + + const oc_rep_t *cso = oc_rep_get_by_type_and_key( + testProps->value.object, OC_REP_OBJECT, PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER, + OC_CHAR_ARRAY_LEN(PLGD_DPS_TEST_CLOUD_STATUS_OBSERVER)); + if (cso != NULL) { + dps_update_cloud_observer(ctx, cso->value.object); + } + + const oc_rep_t *iot = oc_rep_get_by_type_and_key( + testProps->value.object, OC_REP_OBJECT, PLGD_DPS_TEST_IOTIVITY, + OC_CHAR_ARRAY_LEN(PLGD_DPS_TEST_IOTIVITY)); + if (iot != NULL) { + dps_update_iotivity(iot->value.object); + } + return true; +} + +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + +static oc_event_callback_retval_t +dps_update_async(void *data) +{ + assert(data != NULL); + oc_list_remove(g_dps_update_data_list, data); + + dps_conf_update_data_t *update = (dps_conf_update_data_t *)data; + if (update->factory_reset) { + if (dps_factory_reset(update->device, false) != 0) { + DPS_ERR("failed to reset device(%zu) ownership", update->device); + } + goto finish; + } + + const plgd_dps_context_t *ctx = plgd_dps_get_context(update->device); + if (ctx == NULL) { + DPS_ERR("failed to get DPS context for device(%zu)", update->device); + goto finish; + } + if (dps_store_dump(&ctx->store, ctx->device) != 0) { + DPS_ERR("failed to dump storage in async handler"); + goto finish; + } + +finish: +#ifdef OC_DYNAMIC_ALLOCATION + free(update); +#else /* !OC_DYNAMIC_ALLOCATION */ + oc_memb_free(&g_dps_conf_update_data_s, update); +#endif /* OC_DYNAMIC_ALLOCATION */ + return OC_EVENT_DONE; +} + +typedef struct +{ + bool success; + bool asyncUpdate; +} dps_update_endpoint_t; + +static dps_update_endpoint_t +dps_update_endpoint_from_request(plgd_dps_context_t *ctx, + const oc_rep_t *payload, + dps_conf_update_t *data) +{ + dps_update_endpoint_t res = { + .success = false, + .asyncUpdate = false, + }; + + const oc_rep_t *prop = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, PLGD_DPS_ENDPOINT, + OC_CHAR_ARRAY_LEN(PLGD_DPS_ENDPOINT)); + if (prop == NULL) { + return res; + } + data->endpoint = &prop->value.string; + if (oc_string_len_unsafe(*data->endpoint) == 0) { + DPS_DBG("got forced deregister via provisioning of empty endpoint"); + dps_context_reset(ctx); + res.asyncUpdate = true; + dps_conf_update_data_t *update = dps_update_list_insert(ctx->device, true); + if (update == NULL) { + return res; + } + oc_set_delayed_callback(update, dps_update_async, 0); + res.success = true; + return res; + } + prop = + oc_rep_get_by_type_and_key(payload, OC_REP_STRING, PLGD_DPS_ENDPOINT_NAME, + OC_CHAR_ARRAY_LEN(PLGD_DPS_ENDPOINT_NAME)); + if (prop != NULL) { + data->endpointName = &prop->value.string; + } + prop = + oc_rep_get_by_type_and_key(payload, OC_REP_OBJECT_ARRAY, PLGD_DPS_ENDPOINTS, + OC_CHAR_ARRAY_LEN(PLGD_DPS_ENDPOINTS)); + if (prop != NULL) { + data->endpoints = prop->value.object_array; + } + res.success = + dps_set_endpoints(ctx, data->endpoint, data->endpointName, data->endpoints); + return res; +} + +static bool +dps_update_reprovision_from_request(const oc_rep_t *payload, + dps_conf_update_t *data) +{ + if (oc_rep_get_bool(payload, PLGD_DPS_FORCE_REPROVISION, &data->restart) && + data->restart) { + DPS_DBG("DPS property(%s) was set", PLGD_DPS_FORCE_REPROVISION); + return true; + } + return false; +} + +static oc_event_callback_retval_t +dps_update_manager_restart(void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + if (plgd_dps_manager_restart(ctx) != 0) { + DPS_ERR("failed to restart DPS"); + } + return OC_EVENT_DONE; +} + +static bool +dps_update_from_request(plgd_dps_context_t *ctx, const oc_request_t *request) +{ +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + if (dps_update_test_properties(ctx, request->request_payload)) { + return true; + } +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + + dps_conf_update_t data; + memset(&data, 0, sizeof(data)); + + dps_update_endpoint_t res = + dps_update_endpoint_from_request(ctx, request->request_payload, &data); + if (res.asyncUpdate) { + return res.success; + } + + bool changed = + dps_update_reprovision_from_request(request->request_payload, &data) || + res.success; + + if (changed) { + dps_conf_update_data_t *update = dps_update_list_insert(ctx->device, false); + if (update == NULL) { + return false; + } + dps_reset_delayed_callback(update, dps_update_async, 0); + + plgd_dps_force_reprovision(ctx); + dps_reset_delayed_callback(ctx, dps_update_manager_restart, 0); + } + return changed; +} + +static void +post_dps(oc_request_t *request, oc_interface_mask_t interface, void *user_data) +{ + (void)user_data; + plgd_dps_context_t *ctx = plgd_dps_get_context(request->resource->device); + if (ctx == NULL) { + oc_send_response(request, OC_STATUS_INTERNAL_SERVER_ERROR); + return; + } + DPS_DBG("POST request received"); + bool changed = dps_update_from_request(ctx, request); + dps_resource_encode_response(ctx, interface); + oc_send_response(request, + changed ? OC_STATUS_CHANGED : OC_STATUS_BAD_REQUEST); +} + +oc_resource_t * +dps_create_dpsconf_resource(size_t device) +{ + DPS_DBG("plgd_dps_resource: initializing DPS resource"); + oc_resource_t *res = oc_new_resource(NULL, PLGD_DPS_URI, 1, device); + if (!res) { + DPS_ERR("plgd_dps_resource: cannot create resource"); + return NULL; + } + oc_resource_bind_resource_type(res, PLGD_DPS_RES_TYPE); + oc_resource_bind_resource_interface(res, OC_IF_BASELINE | OC_IF_R | OC_IF_RW); + oc_resource_set_default_interface(res, OC_IF_BASELINE); + oc_resource_set_discoverable(res, true); + oc_resource_set_observable(res, true); + oc_resource_set_request_handler(res, OC_GET, get_dps, NULL); + oc_resource_set_request_handler(res, OC_POST, post_dps, NULL); + oc_add_resource(res); + oc_cloud_add_resource(res); + return res; +} + +void +dps_delete_dpsconf_resource(oc_resource_t *res) +{ + if (res == NULL) { + return; + } + DPS_DBG("plgd_dps_resource: destroying DPS resource"); + oc_cloud_delete_resource(res); + oc_delete_resource(res); +} + +void +dps_update_list_init(void) +{ + oc_list_init(g_dps_update_data_list); +} + +void +dps_update_list_cleanup(void) +{ + dps_conf_update_data_t *upd; + while ((upd = oc_list_pop(g_dps_update_data_list)) != NULL) { +#ifdef OC_DYNAMIC_ALLOCATION + free(upd); +#else /* !OC_DYNAMIC_ALLOCATION */ + oc_memb_free(&g_dps_conf_update_data_s, upd); +#endif /* OC_DYNAMIC_ALLOCATION */ + } +} + +static dps_conf_update_data_t * +dps_update_list_insert(size_t device, bool factory_reset) +{ +#ifdef OC_DYNAMIC_ALLOCATION + dps_conf_update_data_t *update = + (dps_conf_update_data_t *)calloc(1, sizeof(dps_conf_update_data_t)); +#else /* !OC_DYNAMIC_ALLOCATION */ + dps_conf_update_data_t *update = + (dps_conf_update_data_t *)oc_memb_alloc(&g_dps_conf_update_data_s); +#endif /* OC_DYNAMIC_ALLOCATION */ + if (update == NULL) { + DPS_ERR("failed to allocate update item"); + return NULL; + } + update->device = device; + update->factory_reset = factory_reset; + oc_list_add(g_dps_update_data_list, update); + return update; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_resource_internal.h b/api/plgd/device-provisioning-client/plgd_dps_resource_internal.h new file mode 100644 index 0000000000..a564a3a74d --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_resource_internal.h @@ -0,0 +1,89 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_RESOURCE_INTERNAL_H +#define PLGD_DPS_RESOURCE_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include "oc_api.h" +#include "oc_config.h" +#include "util/oc_compiler.h" +#include "util/oc_endpoint_address_internal.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// @brief Create /plgd/dps resource +oc_resource_t *dps_create_dpsconf_resource(size_t device); + +/// @brief Delete /plgd/dps resource +void dps_delete_dpsconf_resource(oc_resource_t *res); + +/// @brief Convert DPS status to provisioning status +const char *dps_status_to_str(uint32_t status); + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + +enum { DPS_CLOUD_RETRY_TIMEOUTS_SIZE = 6 }; + +typedef struct +{ + uint16_t retry_timeout[DPS_CLOUD_RETRY_TIMEOUTS_SIZE]; +} dps_resource_iotivity_data_t; + +typedef struct +{ + plgd_cloud_status_observer_configuration_t cloud_status_observer; + dps_resource_iotivity_data_t iotivity; +} dps_resource_test_data_t; + +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + +typedef struct +{ + plgd_dps_error_t last_error; + const char *provision_status; + size_t provision_status_length; + const oc_endpoint_addresses_t *endpoints; + bool forceReprovision; +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + dps_resource_test_data_t test; +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ +} dps_resource_data_t; + +/// @brief Encode DPS data to root payload +void dps_resource_encode(oc_interface_mask_t interface, + const oc_resource_t *resource, + const dps_resource_data_t *data); + +/// @brief Initialize DPS update data list +void dps_update_list_init(void); + +/// @brief Clean-up DPS update data list +void dps_update_list_cleanup(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_RESOURCE_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_retry.c b/api/plgd/device-provisioning-client/plgd_dps_retry.c new file mode 100644 index 0000000000..2154aff9a2 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_retry.c @@ -0,0 +1,272 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_context_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_retry_internal.h" + +#include "port/oc_random.h" +#include "util/oc_endpoint_address_internal.h" + +#include +#include +#include + +// NOLINTNEXTLINE(modernize-*) +#define MIN_DELAYED_VALUE_MS (256) + +#if DPS_DBG_IS_ENABLED +static void +dps_retry_print_configuration(const uint8_t cfg[PLGD_DPS_MAX_RETRY_VALUES_SIZE]) +{ + // GCOVR_EXCL_START + DPS_DBG("retry configuration:"); + for (size_t i = 0; i < PLGD_DPS_MAX_RETRY_VALUES_SIZE && cfg[i] != 0; ++i) { + DPS_DBG("\t%d: %ds", (int)i, (int)cfg[i]); + } + // GCOVR_EXCL_STOP +} +#endif /* DPS_DBG_IS_ENABLED */ + +void +dps_retry_init(plgd_dps_retry_t *ret) +{ + memset(ret, 0, sizeof(plgd_dps_retry_t)); + uint8_t default_message_timeout[] = { + 10, 20, 40, 80, 120 + }; // NOLINT(readability-magic-numbers) + memcpy(&ret->default_cfg, &default_message_timeout, + sizeof(default_message_timeout)); +#if DPS_DBG_IS_ENABLED + dps_retry_print_configuration(ret->default_cfg); +#endif /* DPS_DBG_IS_ENABLED */ +} + +bool +plgd_dps_set_retry_configuration(plgd_dps_context_t *ctx, const uint8_t cfg[], + size_t cfg_size) +{ + assert(ctx != NULL); + if (cfg == NULL || cfg_size == 0 || + cfg_size > PLGD_DPS_MAX_RETRY_VALUES_SIZE) { + return false; + } + + for (size_t i = 0; i < cfg_size; ++i) { + if (cfg[i] == 0) { + return false; + } + } + + memset(&ctx->retry.default_cfg, 0, + sizeof(cfg[0]) * PLGD_DPS_MAX_RETRY_VALUES_SIZE); + memcpy(&ctx->retry.default_cfg, cfg, sizeof(cfg[0]) * cfg_size); +#if DPS_DBG_IS_ENABLED + dps_retry_print_configuration(ctx->retry.default_cfg); +#endif /* DPS_DBG_IS_ENABLED */ + return true; +} + +int +plgs_dps_get_retry_configuration(const plgd_dps_context_t *ctx, uint8_t *buffer, + size_t buffer_size) +{ + assert(ctx != NULL); + assert(buffer != NULL); + + int cfg_size = 0; + for (size_t i = 0; i < PLGD_DPS_MAX_RETRY_VALUES_SIZE; ++i) { + if (ctx->retry.default_cfg[i] == 0) { + break; + } + ++cfg_size; + } + + if (buffer_size < (size_t)cfg_size) { + return -1; + } + + memcpy(buffer, &ctx->retry.default_cfg[0], + sizeof(ctx->retry.default_cfg[0]) * cfg_size); + return cfg_size; +} + +uint8_t +dps_retry_size(const plgd_dps_retry_t *ret) +{ + uint8_t index; + for (index = 0; index < (uint8_t)PLGD_DPS_MAX_RETRY_VALUES_SIZE; ++index) { + if (ret->default_cfg[index] == 0) { + break; + } + } + return index; +} + +// for delay use timeout/2 value + random [0, timeout/2] +static uint64_t +get_delay_from_timeout(uint16_t timeout) +{ + if (timeout == 0) { + return oc_random_value() % MIN_DELAYED_VALUE_MS; + } + uint64_t delay = (uint64_t)timeout * MILLISECONDS_PER_SECOND / 2; + // Include a random delay to prevent multiple devices from attempting to + // connect or make requests simultaneously. + delay += oc_random_value() % delay; + return delay; +} + +static bool +default_schedule_action(plgd_dps_context_t *ctx, uint8_t retry_count, + uint64_t *delay, uint16_t *timeout) +{ + if (retry_count >= dps_retry_size(&ctx->retry)) { + // we have made all attempts, try to select next server + DPS_DBG("retry loop over, selecting next DPS endpoint"); + oc_endpoint_addresses_select_next(&ctx->store.endpoints); + return false; + } + *timeout = ctx->retry.default_cfg[retry_count]; + *delay = get_delay_from_timeout(*timeout); + return true; +} + +#if DPS_DBG_IS_ENABLED +static const char * +plgd_dps_status_to_str(plgd_dps_status_t action) +{ + // GCOVR_EXCL_START + if (action == 0) { + return "reinitialize"; + } + return dps_status_flag_to_str(action); + // GCOVR_EXCL_STOP +} +#endif /* DPS_DBG_IS_ENABLED */ + +static bool +on_action_response_set_retry(plgd_dps_context_t *ctx, plgd_dps_status_t action, + uint8_t retry_count, uint64_t *delay, + uint16_t *timeout) +{ + bool success = false; + if (ctx->retry.schedule_action.on_schedule_action != NULL) { + success = ctx->retry.schedule_action.on_schedule_action( + ctx, action, retry_count, delay, timeout, + ctx->retry.schedule_action.user_data); + } else { + success = default_schedule_action(ctx, retry_count, delay, timeout); + } + if (!success) { + DPS_DBG("for retry(%d), action(%s) is not scheduled", retry_count, + plgd_dps_status_to_str(action)); + return false; + } + DPS_DBG("for retry(%d), action(%s) is delayed for %llu milliseconds with and " + "set with %u seconds timeout", + retry_count, plgd_dps_status_to_str(action), + (long long unsigned)*delay, ctx->retry.schedule_action.timeout); + return true; +} + +static bool +dps_schedule_action(plgd_dps_context_t *ctx, uint8_t count, + plgd_dps_status_t action) +{ + uint64_t delay = 0; + uint16_t timeout = 0; + + if (!on_action_response_set_retry(ctx, action, count, &delay, &timeout)) { + if (count == 0) { + // To prevent an infinite loop, we check if count is 0, indicating that + // dps_retry_reset has already been called. In such cases, we return false + // since calling it again is not allowed. The responsibility of handling + // this situation and resetting to default values lies with + // dps_retry_reset. + return false; + } + dps_retry_reset(ctx, action); + return true; + } + ctx->retry.schedule_action.delay = delay; + ctx->retry.schedule_action.timeout = timeout; + return true; +} + +void +dps_retry_increment(plgd_dps_context_t *ctx, plgd_dps_status_t action) +{ + DPS_DBG("retry counter increment"); + ++ctx->retry.count; + dps_schedule_action(ctx, ctx->retry.count, action); +} + +void +dps_retry_reset(plgd_dps_context_t *ctx, plgd_dps_status_t action) +{ + DPS_DBG("retry counter reset"); + ctx->retry.count = 0; + if (!dps_schedule_action(ctx, ctx->retry.count, action)) { + // reset must be always set timeout and delay + ctx->retry.schedule_action.timeout = DEFAULT_RESET_TIMEOUT; + ctx->retry.schedule_action.delay = + get_delay_from_timeout(ctx->retry.schedule_action.timeout); + } +} + +uint16_t +dps_retry_get_timeout(const plgd_dps_retry_t *ret) +{ + assert(ret->count < PLGD_DPS_MAX_RETRY_VALUES_SIZE); + uint16_t val = ret->schedule_action.timeout; + if (val == 0) { + val = DEFAULT_RESET_TIMEOUT; + } + return val; +} + +uint64_t +dps_retry_get_delay(const plgd_dps_retry_t *ret) +{ + assert(ret->count < PLGD_DPS_MAX_RETRY_VALUES_SIZE); + uint64_t val = ret->schedule_action.delay; + if (val == 0) { + val = get_delay_from_timeout(DEFAULT_RESET_TIMEOUT); + } + return val; +} + +static void +dps_set_schedule_action(plgd_dps_retry_t *ret, + plgd_dps_schedule_action_cb_t on_schedule_action, + void *user_data) +{ + assert(ret != NULL); + ret->schedule_action.on_schedule_action = on_schedule_action; + ret->schedule_action.user_data = user_data; +} + +void +plgd_dps_set_schedule_action(plgd_dps_context_t *ctx, + plgd_dps_schedule_action_cb_t on_schedule_action, + void *user_data) +{ + assert(ctx != NULL); + dps_set_schedule_action(&ctx->retry, on_schedule_action, user_data); +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_retry_internal.h b/api/plgd/device-provisioning-client/plgd_dps_retry_internal.h new file mode 100644 index 0000000000..83492f9ed7 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_retry_internal.h @@ -0,0 +1,83 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_RETRY_INTERNAL_H +#define PLGD_DPS_RETRY_INTERNAL_H + +#include "plgd/plgd_dps.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define DEFAULT_RESET_TIMEOUT (2) +// NOLINTNEXTLINE(modernize-*) +#define MILLISECONDS_PER_SECOND (1000) + +typedef struct schedule_action_t +{ + plgd_dps_schedule_action_cb_t + on_schedule_action; ///< callback to schedule action + void *user_data; ///< user data + uint16_t timeout; ///< timeout in seconds + uint64_t delay; ///< delay in milliseconds +} schedule_action_t; + +/** + * @brief Retry configuration and current value. + * + * The configuration of the retry counter consists of non-zero integer values + * which will be interpretet as timeout values (in seconds). + */ +typedef struct plgd_dps_retry_t +{ + uint8_t default_cfg[PLGD_DPS_MAX_RETRY_VALUES_SIZE]; ///< retry counter + ///< configuration + uint8_t count; ///< current retry counter value + schedule_action_t schedule_action; ///< schedule action +} plgd_dps_retry_t; + +/// @brief Initialize retry counter configuration with default values. +void dps_retry_init(plgd_dps_retry_t *ret); + +/// @brief Get size of the timeout default_cfg array. +uint8_t dps_retry_size(const plgd_dps_retry_t *ret); + +/** + * @brief Increment retry counter value by 1. + * + * @note if counter reaches max value it is reset back to 0. + */ +void dps_retry_increment(plgd_dps_context_t *ctx, plgd_dps_status_t action); + +/// @brief Reset retry counter value to 0. +void dps_retry_reset(plgd_dps_context_t *ctx, plgd_dps_status_t action); + +/// @brief Get timeout value based on the current retry counter value. +uint16_t dps_retry_get_timeout(const plgd_dps_retry_t *ret); + +/// @brief Get delay value based on the current retry counter value. +uint64_t dps_retry_get_delay(const plgd_dps_retry_t *ret); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_RETRY_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_security.c b/api/plgd/device-provisioning-client/plgd_dps_security.c new file mode 100644 index 0000000000..f4d185a136 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_security.c @@ -0,0 +1,519 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_context_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_pki_internal.h" +#include "plgd_dps_security_internal.h" +#include "plgd_dps_tag_internal.h" + +#include "oc_acl.h" +#include "oc_core_res.h" +#include "oc_cred.h" +#include "oc_store.h" +#include "oc_uuid.h" +#include "security/oc_acl_internal.h" +#include "security/oc_doxm_internal.h" +#include "security/oc_pstat_internal.h" +#include "security/oc_tls_internal.h" + +#include +#include +#include +#include + +enum { + /* vendor specific constant 0xFF01 for DPS device */ + DPS_OXMTYPE_PLGD = 0xFF01, +}; + +bool +dps_is_dos_owned(size_t device) +{ + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(device); + return (pstat->s == OC_DOS_RFPRO || pstat->s == OC_DOS_RFNOP); +} + +static bool +dps_is_owned(const plgd_dps_context_t *ctx, const oc_uuid_t *owner) +{ + char owner_str[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(owner, owner_str, sizeof(owner_str)); + if ((oc_string_len(ctx->store.owner) != OC_UUID_LEN - 1) || + strncmp(oc_string(ctx->store.owner), owner_str, OC_UUID_LEN - 1) != 0) { + return false; + } + + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (memcmp(pstat->rowneruuid.id, owner->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + if (!pstat->isop) { + return false; + } + + const oc_sec_doxm_t *doxm = oc_sec_get_doxm(ctx->device); + if (!doxm->owned) { + return false; + } + if (doxm->oxmsel != DPS_OXMTYPE_PLGD) { + return false; + } + if (memcmp(doxm->rowneruuid.id, owner->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + if (memcmp(doxm->devowneruuid.id, owner->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + + const oc_sec_creds_t *creds = oc_sec_get_creds(ctx->device); + if (memcmp(creds->rowneruuid.id, owner->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + + const oc_sec_acl_t *acls = oc_sec_get_acl(ctx->device); + if (memcmp(acls->rowneruuid.id, owner->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + + return true; +} + +bool +dps_is_self_owned(const plgd_dps_context_t *ctx) +{ + const oc_uuid_t *uuid = oc_core_get_device_id(ctx->device); + const oc_sec_doxm_t *doxm = oc_sec_get_doxm(ctx->device); + if (memcmp(doxm->deviceuuid.id, uuid->id, OC_UUID_ID_SIZE) != 0) { + return false; + } + + return dps_is_owned(ctx, uuid); +} + +static void +dps_clear_credentials(size_t device) +{ + oc_sec_creds_t *creds = oc_sec_get_creds(device); + oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + while (cred != NULL) { + oc_sec_cred_t *c_next = cred->next; + bool skipDelete = cred->credtype == OC_CREDTYPE_CERT && + (cred->credusage == OC_CREDUSAGE_MFG_CERT || + cred->credusage == OC_CREDUSAGE_MFG_TRUSTCA) && + !dps_is_dps_cred(cred); + if (!skipDelete) { + oc_sec_remove_cred(cred, device); + } + cred = c_next; + } +} + +bool +dps_endpoint_peer_is_server(const oc_tls_peer_t *peer, void *user_data) +{ + (void)user_data; + bool is_server = peer->role == MBEDTLS_SSL_IS_SERVER; +#if DPS_DBG_IS_ENABLED + // GCOVR_EXCL_START + if (is_server) { + oc_string_t ep_str; + if (oc_endpoint_to_string(&peer->endpoint, &ep_str) == 0) { + DPS_DBG("remove peer endpoint: %s", oc_string(ep_str)); + oc_free_string(&ep_str); + } + } + // GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + return is_server; +} + +static bool +dps_own_device(plgd_dps_context_t *ctx, const oc_uuid_t *owner) +{ + oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFOTM) { + DPS_ERR("cannot own device: device(%zu) is not in RFOTM state", + ctx->device); + return false; + } + + char owner_str[OC_UUID_LEN] = { 0 }; + int owner_str_len = oc_uuid_to_str_v1(owner, owner_str, sizeof(owner_str)); + assert(owner_str_len > 0); + oc_set_string(&ctx->store.owner, owner_str, (size_t)owner_str_len); + +#if DPS_DBG_IS_ENABLED + DPS_DBG("own device by %s", owner_str); +#endif /*DPS_DBG_IS_ENABLED*/ + + memcpy(pstat->rowneruuid.id, owner->id, OC_UUID_ID_SIZE); + pstat->tm = pstat->cm = 4; + pstat->isop = true; + pstat->s = OC_DOS_RFNOP; + oc_sec_dump_pstat(ctx->device); + + oc_sec_doxm_t *doxm = oc_sec_get_doxm(ctx->device); + memcpy(doxm->devowneruuid.id, owner->id, OC_UUID_ID_SIZE); + memcpy(doxm->rowneruuid.id, owner->id, OC_UUID_ID_SIZE); + doxm->owned = true; + doxm->oxmsel = DPS_OXMTYPE_PLGD; + oc_sec_dump_doxm(ctx->device); + + DPS_DBG("clear credentials"); + dps_clear_credentials(ctx->device); + oc_sec_creds_t *creds = oc_sec_get_creds(ctx->device); + memcpy(creds->rowneruuid.id, owner->id, OC_UUID_ID_SIZE); + oc_sec_dump_cred(ctx->device); + + DPS_DBG("clear acls"); + oc_sec_acl_clear(ctx->device, NULL, NULL); + if (!oc_sec_acl_add_bootstrap_acl(ctx->device)) { + DPS_ERR("failed to boostrap ACLs"); + return false; + } + oc_sec_acl_t *acls = oc_sec_get_acl(ctx->device); + memcpy(acls->rowneruuid.id, owner->id, OC_UUID_ID_SIZE); + oc_sec_dump_acl(ctx->device); +#if DPS_DBG_IS_ENABLED + dps_print_certificates(ctx->device); + dps_print_acls(ctx->device); + dps_print_peers(); +#endif /*DPS_DBG_IS_ENABLED*/ + + // must be called after assignment pstat->s = OC_DOS_RFNOP + oc_tls_close_peers(dps_endpoint_peer_is_server, NULL); + return true; +} + +bool +dps_set_owner(plgd_dps_context_t *ctx, const oc_uuid_t *owner) +{ + if (dps_is_dos_owned(ctx->device) && dps_is_owned(ctx, owner)) { + DPS_DBG("set owner skipped: already set"); + return true; + } + oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + pstat->s = OC_DOS_RFOTM; + return dps_own_device(ctx, owner); +} + +bool +dps_set_self_owned(plgd_dps_context_t *ctx) +{ + if (dps_is_dos_owned(ctx->device) && dps_is_self_owned(ctx)) { + return true; + } + const oc_uuid_t *uuid = oc_core_get_device_id(ctx->device); + oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + pstat->s = OC_DOS_RFOTM; + return dps_own_device(ctx, uuid); +} + +bool +dps_has_owner(const plgd_dps_context_t *ctx) +{ + if (!dps_is_dos_owned(ctx->device) || dps_is_self_owned(ctx)) { + return false; + } + + const oc_sec_doxm_t *doxm = oc_sec_get_doxm(ctx->device); + oc_uuid_t owner; + memcpy(owner.id, doxm->devowneruuid.id, OC_UUID_ID_SIZE); + return dps_is_owned(ctx, &owner); +} + +int +dps_factory_reset(size_t device, bool force) +{ + assert(dps_is_dos_owned(device)); + return oc_reset_device_v1(device, force) ? 0 : -1; +} + +static bool +dps_is_dps_ace(const oc_sec_ace_t *ace) +{ + return oc_string_len(ace->tag) == DPS_TAG_LEN && + strcmp(oc_string(ace->tag), DPS_TAG) == 0; +} + +bool +dps_has_acls(size_t device) +{ + const oc_sec_acl_t *acl = oc_sec_get_acl(device); + const oc_sec_ace_t *ace = oc_list_head(acl->subjects); + while (ace != NULL) { + if (dps_is_dps_ace(ace)) { + return true; + } + ace = ace->next; + } + + return false; +} + +bool +dps_is_dps_cred(const oc_sec_cred_t *cred) +{ + assert(cred != NULL); + return oc_string_len(cred->tag) == DPS_TAG_LEN && + strcmp(oc_string(cred->tag), DPS_TAG) == 0; +} + +static bool +is_identity_cred(const oc_sec_cred_t *cred) +{ + assert(cred != NULL); + return cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_IDENTITY_CERT; +} + +static bool +is_dps_identity_cred(const oc_sec_cred_t *cred) +{ + return is_identity_cred(cred) && dps_is_dps_cred(cred); +} + +static bool +is_trust_ca_cred(const oc_sec_cred_t *cred) +{ + assert(cred != NULL); + return cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_TRUSTCA; +} + +typedef struct +{ + dps_pki_configuration_t cfg; + uint64_t valid_from; + uint64_t valid_to; +} dps_verify_certificate_data_t; + +static bool +dps_verify_certificate_data(const oc_sec_certs_data_t *data, void *user_data) +{ + if (data == NULL) { + return false; + } + + dps_verify_certificate_data_t *udata = + (dps_verify_certificate_data_t *)user_data; + int ret = + dps_pki_validate_certificate(udata->cfg, data->valid_from, data->valid_to); + if (ret == -1) { + return false; + } + dps_certificate_state_t cert_state = (dps_certificate_state_t)ret; + if (cert_state != DPS_CERTIFICATE_VALID) { + DPS_ERR("invalid certificate: %s", + dps_pki_certificate_state_to_str(cert_state)); + return false; + } + + udata->valid_to = data->valid_to; + udata->valid_from = data->valid_from; + return true; +} + +typedef struct +{ + uint64_t valid_from; + uint64_t valid_to; +} dps_certificate_validity_t; + +static bool +dps_check_credentials(const plgd_dps_context_t *ctx, + dps_certificate_validity_t *min_validity) +{ + oc_remove_delayed_callback(NULL, dps_pki_renew_certificates_async); + + bool all_valid = true; + bool has_identity = false; + bool has_trust_anchor = false; + uint64_t valid_to = UINT64_MAX; + uint64_t valid_from = 0; + const oc_sec_creds_t *creds = oc_sec_get_creds(ctx->device); + oc_sec_cred_t *cred = oc_list_head(creds->creds); + while (cred != NULL) { + oc_sec_cred_t *cred_next = cred->next; + if (!dps_is_dps_cred(cred) || cred->credtype == OC_CREDTYPE_PSK) { + cred = cred_next; + continue; + } + DPS_DBG("check certificates for cred(credid=%d):", cred->credid); + dps_verify_certificate_data_t data = { + .cfg = ctx->pki, + }; + int ret = oc_cred_verify_certificate_chain( + cred, dps_verify_certificate_data, &data); + if (ret != 0) { + if (ret == -1) { + DPS_ERR("failed to get certificate data for cred(credid=%d)", + cred->credid); + } + if (ret == 1) { + DPS_DBG("removing credential with expired certificate"); + oc_sec_remove_cred(cred, ctx->device); + } + all_valid = false; + cred = cred_next; + continue; // go through all credentials, so we remove all expired + // certificates + } + has_identity = is_identity_cred(cred) ? true : has_identity; + has_trust_anchor = is_trust_ca_cred(cred) ? true : has_trust_anchor; + + if (data.valid_to < valid_to) { + valid_from = data.valid_from; + valid_to = data.valid_to; + } + cred = cred_next; + } + + if (!all_valid || !has_identity || !has_trust_anchor) { + return false; + } + DPS_DBG("earliest expiring certificate(valid-from: %lu, valid-to: %lu)", + valid_from, valid_to); + if (min_validity != NULL) { + min_validity->valid_from = valid_from; + min_validity->valid_to = valid_to; + } + return true; +} + +bool +dps_check_credentials_and_schedule_renewal(plgd_dps_context_t *ctx, + uint64_t min_interval) +{ + dps_certificate_validity_t min; + if (!dps_check_credentials(ctx, &min)) { + return false; + } + dps_pki_schedule_renew_certificates(ctx, min.valid_to, min_interval); + return true; +} + +int +dps_get_identity_credid(size_t device) +{ + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + if (creds == NULL) { + return -1; + } + for (const oc_sec_cred_t *cred = + (const oc_sec_cred_t *)oc_list_head(creds->creds); + cred != NULL; cred = cred->next) { + if (is_dps_identity_cred(cred)) { + return cred->credid; + } + } + return -1; +} + +#if DPS_DBG_IS_ENABLED +void +dps_print_acls(size_t device) +{ + // GCOVR_EXCL_START + DPS_DBG("acls:"); + const oc_sec_acl_t *acls = oc_sec_get_acl(device); + char rowneruuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&acls->rowneruuid, rowneruuid, sizeof(rowneruuid)); + DPS_DBG("\trowneruuid:%s", rowneruuid); + const oc_sec_ace_t *ace = oc_list_head(acls->subjects); + while (ace != NULL) { + const char *tag = oc_string_len(ace->tag) > 0 ? oc_string(ace->tag) : ""; + const oc_ace_subject_t *subject = &ace->subject; + if (ace->subject_type == OC_SUBJECT_ROLE) { + const char *role = oc_string_len(subject->role.role) > 0 + ? oc_string(subject->role.role) + : ""; + const char *authority = oc_string_len(subject->role.authority) > 0 + ? oc_string(subject->role.authority) + : ""; + DPS_DBG("\taceid:%d subject_type:%d subject.role:%s subject.authority:%s " + "subject.conn:%d permission:%d tag:%s", + ace->aceid, ace->subject_type, role, authority, subject->conn, + ace->permission, tag); + } else { + char uuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&subject->uuid, uuid, sizeof(uuid)); + DPS_DBG("\taceid:%d uuid:%s subject_type:%d subject.conn:%d " + "permission:%d tag:%s", + ace->aceid, uuid, ace->subject_type, subject->conn, + ace->permission, tag); + } + oc_ace_res_t *res = (oc_ace_res_t *)oc_list_head(ace->resources); + if (res != NULL) { + DPS_DBG("\tresources:"); + for (; res != NULL; res = res->next) { + const char *href = + oc_string_len(res->href) > 0 ? oc_string(res->href) : ""; + DPS_DBG("\t\thref:%s wildcard:%d", href, res->wildcard); + } + } + + ace = ace->next; + } + // GCOVR_EXCL_STOP +} + +void +dps_print_certificates(size_t device) +{ + // GCOVR_EXCL_START + DPS_DBG("certificates:"); + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + const oc_sec_cred_t *cred = (const oc_sec_cred_t *)oc_list_head(creds->creds); + while (cred != NULL) { + char uuid[OC_UUID_LEN] = { 0 }; + oc_uuid_to_str(&cred->subjectuuid, uuid, sizeof(uuid)); + const char *tag = oc_string_len(cred->tag) > 0 ? oc_string(cred->tag) : ""; + DPS_DBG("\tcredid: %d, credtype: %d, credusage: %d, subjectuuid:%s, tag:%s", + cred->credid, cred->credtype, cred->credusage, uuid, tag); + cred = cred->next; + } + // GCOVR_EXCL_STOP +} + +void +dps_print_peers(void) +{ + // GCOVR_EXCL_START + const oc_tls_peer_t *peer = oc_tls_get_peer(NULL); + DPS_DBG("peers:"); + if (peer == NULL) { + DPS_DBG("\tno peers were found"); + return; + } + + while (peer != NULL) { + oc_string_t ep_str; + if (oc_endpoint_to_string(&peer->endpoint, &ep_str) == 0) { + bool is_server = peer->role == MBEDTLS_SSL_IS_SERVER; + DPS_DBG("\tendpoint: %s, server: %d", oc_string(ep_str), + is_server ? 1 : 0); + oc_free_string(&ep_str); + } + peer = peer->next; + } + // GCOVR_EXCL_STOP +} + +#endif /* DPS_DBG_IS_ENABLED */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_security_internal.h b/api/plgd/device-provisioning-client/plgd_dps_security_internal.h new file mode 100644 index 0000000000..b384b7dfab --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_security_internal.h @@ -0,0 +1,101 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_SECURITY_INTERNAL_H +#define PLGD_DPS_SECURITY_INTERNAL_H + +#include "plgd/plgd_dps.h" +#include "plgd_dps_log_internal.h" + +#include "oc_cred.h" +#include "oc_ri.h" +#include "oc_uuid.h" +#include "security/oc_tls_internal.h" + +#if DPS_DBG_IS_ENABLED +#include "mbedlts/build_info.h" +#include "mbedtls/md.h" +#endif /* DPS_DBG_IS_ENABLED */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// @brief Check if device is in owned onboarding state. +bool dps_is_dos_owned(size_t device); + +/// @brief Check if device is self-owned. +bool dps_is_self_owned(const plgd_dps_context_t *ctx); + +/// @brief Set device owner. +bool dps_set_owner(plgd_dps_context_t *ctx, const oc_uuid_t *owner); + +/// @brief Set device as self-owned. +bool dps_set_self_owned(plgd_dps_context_t *ctx); + +/// @brief Check if DPS has a valid, non-self owner. +bool dps_has_owner(const plgd_dps_context_t *ctx); + +/// @brief Reset self-owned device to default state. +/// +/// @param device index of the logical device to reset +/// @param force true to reset immediately, false to reset after the 2 second +/// to terminate the connections (eg cloud deregistration) +int dps_factory_reset(size_t device, bool force); + +/// @brief Check if ACLs from DPS exists (ACLs must contain at least one ACE +/// from DPS) +bool dps_has_acls(size_t device); + +/// @brief Check if credential is annotated with the DPS_TAG. +bool dps_is_dps_cred(const oc_sec_cred_t *cred); + +/// @brief Check credentials list for valid DPS credentials (list must contain +/// at least one valid identity cert and at least one valid trust anchor; all +/// DPS credentials must be valid). and schedule certificate renewal with +/// min_interval in milliseconds. +bool dps_check_credentials_and_schedule_renewal(plgd_dps_context_t *ctx, + uint64_t min_interval); + +/// @brief Get credid of a identity cert retrieved from DPS service. +int dps_get_identity_credid(size_t device); + +/// @brief Check that the peer is a server. +bool dps_endpoint_peer_is_server(const oc_tls_peer_t *peer, void *user_data); + +#if DPS_DBG_IS_ENABLED + +/// @brief Print device's acls. +void dps_print_acls(size_t device); + +/// @brief Print data of device's certificates. +void dps_print_certificates(size_t device); + +/// @brief Print basic peer data. +void dps_print_peers(void); + +#endif /* DPS_DBG_IS_ENABLED */ + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_SECURITY_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_store.c b/api/plgd/device-provisioning-client/plgd_dps_store.c new file mode 100644 index 0000000000..63cb5bf93f --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_store.c @@ -0,0 +1,374 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_endpoints_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_store_internal.h" + +#include "api/oc_rep_internal.h" +#include "oc_helpers.h" // oc_string, oc_string_len +#include "port/oc_connectivity.h" // OC_MAX_APP_DATA_SIZE +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_macros_internal.h" + +#include +#include + +#ifdef OC_DYNAMIC_ALLOCATION +#include +#endif /* OC_DYNAMIC_ALLOCATION */ + +#ifndef OC_STORAGE +#error OC_STORAGE is not defined check oc_config.h and make sure OC_STORAGE is defined +#endif + +#define DPS_STORE_NAME "dps" +#define DPS_STORE_ENDPOINT ep +#define DPS_STORE_ENDPOINT_NAME epname +#define DPS_STORE_ENDPOINTS eps +#define DPS_STORE_ENDPOINTS_URI uri +#define DPS_STORE_ENDPOINTS_NAME name +#define DPS_STORE_OWNER owner +#define DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET hasBeenProvisionedSinceReset + +#define DPS_STR(s) #s +#define DPS_XSTR(s) DPS_STR(s) +#define DPS_XSTRING_VIEW(s) OC_STRING_VIEW(DPS_XSTR(s)) + +// NOLINTNEXTLINE(modernize-*) +#define DPS_TAG_MAX (32) + +oc_event_callback_retval_t +dps_store_dump_handler(void *data) +{ + const plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; + if (dps_store_dump(&ctx->store, ctx->device) != 0) { + DPS_ERR("[DPS_STORE] failed to dump storage in async handler"); + } + return OC_EVENT_DONE; +} + +void +dps_store_dump_async(plgd_dps_context_t *ctx) +{ + dps_reset_delayed_callback(ctx, dps_store_dump_handler, 0); + _oc_signal_event_loop(); +} + +void +dps_store_init(plgd_dps_store_t *store, + on_selected_endpoint_address_change_fn_t on_dps_endpoint_change, + void *on_dps_endpoint_change_data) +{ + dps_store_deinit(store); + dps_endpoints_init(&store->endpoints, on_dps_endpoint_change, + on_dps_endpoint_change_data); +} + +void +dps_store_deinit(plgd_dps_store_t *store) +{ + oc_endpoint_addresses_deinit(&store->endpoints); + oc_set_string(&store->owner, NULL, 0); + store->has_been_provisioned_since_reset = false; +} + +bool +dps_store_set_endpoints(plgd_dps_store_t *store, + const oc_string_t *selected_uri, + const oc_string_t *selected_name, + const oc_rep_t *endpoints) +{ + if (!oc_endpoint_addresses_reinit( + &store->endpoints, + oc_endpoint_address_make_view_with_name( + oc_string_view2(selected_uri), oc_string_view2(selected_name)))) { + return false; + } + if (endpoints == NULL) { + return true; + } + + for (const oc_rep_t *ep = endpoints; ep != NULL; ep = ep->next) { + const oc_rep_t *rep = oc_rep_get_by_type_and_key( + ep->value.object, OC_REP_STRING, DPS_XSTR(DPS_STORE_ENDPOINTS_URI), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_ENDPOINTS_URI))); + if (rep == NULL) { + DPS_ERR("[DPS_STORE] invalid endpoint element: uri missing"); + continue; + } + oc_string_view_t uri = oc_string_view2(&rep->value.string); + + oc_string_view_t name = OC_STRING_VIEW_NULL; + rep = oc_rep_get_by_type_and_key( + ep->value.object, OC_REP_STRING, DPS_XSTR(DPS_STORE_ENDPOINTS_NAME), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_ENDPOINTS_NAME))); + if (rep != NULL) { + name = oc_string_view2(&rep->value.string); + } + + if (oc_endpoint_addresses_contains(&store->endpoints, uri)) { + DPS_DBG("[DPS_STORE] cannot add endpoint:uri(%s) already exists", + uri.data); + continue; + } + + if (!oc_endpoint_addresses_add( + &store->endpoints, + oc_endpoint_address_make_view_with_name(uri, name))) { + return false; + } + DPS_DBG("[DPS_STORE] added endpoint [uri=%s, name=%s]", uri.data, + name.data != NULL ? name.data : "(null)"); + } + + return true; +} + +void +dps_store_decode(const oc_rep_t *rep, plgd_dps_store_t *store) +{ + typedef struct + { + const oc_rep_t *endpoints; + const oc_string_t *endpoint; + const oc_string_t *endpoint_name; + const oc_string_t *owner; + const bool *has_been_provisioned_since_reset; + } dps_store_data_t; + dps_store_data_t dsd; + memset(&dsd, 0, sizeof(dps_store_data_t)); + + for (const oc_rep_t *store_rep = rep; store_rep != NULL; + store_rep = store_rep->next) { + if (dps_is_property(store_rep, OC_REP_OBJECT_ARRAY, + DPS_XSTR(DPS_STORE_ENDPOINTS), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_ENDPOINTS)))) { + dsd.endpoints = store_rep->value.object_array; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, DPS_XSTR(DPS_STORE_ENDPOINT), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_ENDPOINT)))) { + dsd.endpoint = &store_rep->value.string; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, + DPS_XSTR(DPS_STORE_ENDPOINT_NAME), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_ENDPOINT_NAME)))) { + dsd.endpoint_name = &store_rep->value.string; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, DPS_XSTR(DPS_STORE_OWNER), + OC_CHAR_ARRAY_LEN(DPS_XSTR(DPS_STORE_OWNER)))) { + dsd.owner = &store_rep->value.string; + continue; + } + if (dps_is_property(store_rep, OC_REP_BOOL, + DPS_XSTR(DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET), + OC_CHAR_ARRAY_LEN(DPS_XSTR( + DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET)))) { + dsd.has_been_provisioned_since_reset = &store_rep->value.boolean; + continue; + } + DPS_ERR("[DPS_STORE] Unknown property %s", oc_string(store_rep->name)); + } + +#if DPS_DBG_IS_ENABLED + // GCOVR_EXCL_START + oc_string_view_t endpointv = oc_string_view2(dsd.endpoint); + oc_string_view_t endpoint_namev = oc_string_view2(dsd.endpoint_name); + oc_string_view_t ownerv = oc_string_view2(dsd.owner); + DPS_DBG("[DPS_STORE] endpoint: %s, endpoint_name: %s, owner: %s, " + "has_been_provisioned_since_reset: %s", + endpointv.length > 0 ? endpointv.data : "(null)", + endpoint_namev.length > 0 ? endpoint_namev.data : "(null)", + ownerv.length > 0 ? ownerv.data : "(null)", + dsd.has_been_provisioned_since_reset != NULL + ? (*dsd.has_been_provisioned_since_reset ? "true" : "false") + : "(null)"); + // GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ + + if ((dsd.endpoints != NULL || !oc_string_is_null_or_empty(dsd.endpoint)) && + !dps_store_set_endpoints(store, dsd.endpoint, dsd.endpoint_name, + dsd.endpoints)) { + DPS_WRN("[DPS_STORE] failed to set endpoints"); + } + if (!oc_string_is_null_or_empty(dsd.owner)) { + oc_copy_string(&store->owner, dsd.owner); + } + if (dsd.has_been_provisioned_since_reset != NULL) { + store->has_been_provisioned_since_reset = + *dsd.has_been_provisioned_since_reset; + } +} + +static void +dps_store_gen_tag(const char *name, size_t device, char *dps_tag) +{ + int dps_tag_len = snprintf(dps_tag, DPS_TAG_MAX, "%s_%zd", name, device); + dps_tag_len = + (dps_tag_len < DPS_TAG_MAX - 1) ? dps_tag_len + 1 : DPS_TAG_MAX - 1; + dps_tag[dps_tag_len] = '\0'; +} + +static long +dps_store_get_storage(size_t device, uint8_t *buffer, size_t buffer_size) +{ + char dps_tag[DPS_TAG_MAX]; + dps_store_gen_tag(DPS_STORE_NAME, device, dps_tag); + return oc_storage_read(dps_tag, buffer, buffer_size); +} + +static void +dps_store_rep_set_text_string(CborEncoder *object_map, const char *key, + const char *value) +{ + g_err |= oc_rep_encode_text_string(object_map, key, strlen(key)); + if (value != NULL) { + g_err |= oc_rep_encode_text_string(object_map, value, strlen(value)); + } else { + g_err |= oc_rep_encode_text_string(object_map, "", 0); + } +} + +static void +dps_store_rep_set_bool(CborEncoder *object_map, const char *key, size_t keylen, + bool value) +{ + g_err |= oc_rep_encode_text_string(object_map, key, keylen); + g_err |= oc_rep_encode_boolean(object_map, value); +} + +static void +dps_store_encode_with_map(CborEncoder *object_map, + const plgd_dps_store_t *store) +{ + const oc_endpoint_address_t *selected = + oc_endpoint_addresses_selected(&store->endpoints); + if (selected != NULL) { + oc_endpoint_address_encode(object_map, DPS_XSTRING_VIEW(DPS_STORE_ENDPOINT), + OC_STRING_VIEW_NULL, + DPS_XSTRING_VIEW(DPS_STORE_ENDPOINT_NAME), + oc_endpoint_address_view(selected)); + } + g_err |= oc_endpoint_addresses_encode( + object_map, &store->endpoints, DPS_XSTRING_VIEW(DPS_STORE_ENDPOINTS), true); + dps_store_rep_set_text_string(object_map, DPS_XSTR(DPS_STORE_OWNER), + oc_string(store->owner)); + dps_store_rep_set_bool( + object_map, DPS_XSTR(DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET), + sizeof(DPS_XSTR(DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET)) - 1, + store->has_been_provisioned_since_reset); +} + +int +dps_store_load(plgd_dps_store_t *store, size_t device) +{ +#ifdef OC_DYNAMIC_ALLOCATION + uint8_t *buf = malloc(OC_MAX_APP_DATA_SIZE); + if (buf == NULL) { + DPS_ERR("[DPS_STORE] alloc failed!"); + return -1; + } +#else /* OC_DYNAMIC_ALLOCATION */ + uint8_t buf[OC_MAX_APP_DATA_SIZE] = { 0 }; +#endif /* !OC_DYNAMIC_ALLOCATION */ + long size = dps_store_get_storage(device, buf, OC_MAX_APP_DATA_SIZE); + if (size <= 0) { + dps_store_deinit(store); +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ + return -2; + } + + OC_MEMB_LOCAL(rep_objects, oc_rep_t, OC_MAX_NUM_REP_OBJECTS); + struct oc_memb *pool = oc_rep_reset_pool(&rep_objects); + oc_rep_t *rep = oc_parse_rep(buf, (size_t)size); + dps_store_decode(rep, store); + oc_free_rep(rep); + oc_rep_set_pool(pool); // Reset representation pool +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ + return 0; +} + +bool +dps_store_encode(const plgd_dps_store_t *store) +{ + oc_rep_start_root_object(); + dps_store_encode_with_map(&root_map, store); + oc_rep_end_root_object(); + return oc_rep_get_cbor_errno() == CborNoError; +} + +static int +dps_store_dump_internal(const char *store_name, const plgd_dps_store_t *store) +{ + assert(store_name != NULL); + assert(store != NULL); + +#ifdef OC_DYNAMIC_ALLOCATION + uint8_t *buf = malloc(OC_MIN_APP_DATA_SIZE); + if (buf == NULL) { + return -1; + } + oc_rep_new_realloc_v1(&buf, OC_MIN_APP_DATA_SIZE, OC_MAX_APP_DATA_SIZE); +#else /* OC_DYNAMIC_ALLOCATION */ + uint8_t buf[OC_MIN_APP_DATA_SIZE]; + oc_rep_new_v1(buf, OC_MIN_APP_DATA_SIZE); +#endif /* !OC_DYNAMIC_ALLOCATION */ + + // Dumping dps and accesspoint information. + if (!dps_store_encode(store)) { +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ + return -1; + } + +#ifdef OC_DYNAMIC_ALLOCATION + buf = oc_rep_shrink_encoder_buf(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ + long size = oc_rep_get_encoded_payload_size(); + if (size > 0) { + size = oc_storage_write(store_name, buf, size); + } + +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ + + if (size >= 0) { + return 0; + } + return (int)size; +} + +int +dps_store_dump(const plgd_dps_store_t *store, size_t device) +{ + char dps_tag[DPS_TAG_MAX]; + dps_store_gen_tag(DPS_STORE_NAME, device, dps_tag); + // Calling dump for dps and access point info + return dps_store_dump_internal(dps_tag, store); +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_store_internal.h b/api/plgd/device-provisioning-client/plgd_dps_store_internal.h new file mode 100644 index 0000000000..115d37cd28 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_store_internal.h @@ -0,0 +1,107 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_STORE_INTERNAL_H +#define PLGD_DPS_STORE_INTERNAL_H + +#include "plgd_dps_context_internal.h" +#include "plgd_dps_internal.h" + +#include "oc_rep.h" +#include "util/oc_compiler.h" +#include "util/oc_endpoint_address_internal.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize store with empty values. + * + * @param store store to initialize + */ +void dps_store_init( + plgd_dps_store_t *store, + on_selected_endpoint_address_change_fn_t on_dps_endpoint_change, + void *on_dps_endpoint_change_data) OC_NONNULL(1); + +/** + * @brief Rewrite store with empty values. + * + * @param store store to deinit + */ +void dps_store_deinit(plgd_dps_store_t *store) OC_NONNULL(); + +/** + * @brief Load store from oc_storage. + * + * @param store store to load data in + * @param device index of the device + * @return 0 on success + * < 0 on failure to load store + */ +OC_NO_DISCARD_RETURN +int dps_store_load(plgd_dps_store_t *store, size_t device) OC_NONNULL(); + +/** + * @brief Encode store to root encoder. + * + * @param store store to encode + */ +bool dps_store_encode(const plgd_dps_store_t *store) OC_NONNULL(); + +/** + * @brief Decode store from oc_rep_t. + * + * @param rep representation to decode + * @param store store with decoded data + */ +void dps_store_decode(const oc_rep_t *rep, plgd_dps_store_t *store) + OC_NONNULL(); + +/** + * @brief Save store to oc_storage. + * + * @param store store to save + * @param device index of the device + * @return 0 on success + * < 0 on failure to save store + */ +OC_NO_DISCARD_RETURN +int dps_store_dump(const plgd_dps_store_t *store, size_t device) OC_NONNULL(); + +/// @brief dump store in async handler +oc_event_callback_retval_t dps_store_dump_handler(void *data); + +/// @brief Schedule asynchronous execution of dps_store_dump. +void dps_store_dump_async(plgd_dps_context_t *ctx) OC_NONNULL(); + +/// @brief Set list of DPS endpoints. +bool dps_store_set_endpoints(plgd_dps_store_t *store, + const oc_string_t *selected_uri, + const oc_string_t *selected_name, + const oc_rep_t *endpoints) OC_NONNULL(1); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_STORE_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_tag.c b/api/plgd/device-provisioning-client/plgd_dps_tag.c new file mode 100644 index 0000000000..c4cc30e3a9 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_tag.c @@ -0,0 +1,122 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_tag_internal.h" + +#include "oc_acl.h" +#include "oc_cred.h" + +static bool +dps_has_tag(oc_string_t value, const char *tag, size_t taglen) +{ + return oc_string_len(value) == taglen && (strcmp(oc_string(value), tag) == 0); +} + +void +dps_acls_set_stale_tag(size_t device) +{ + DPS_DBG("adding tags to acls:"); + oc_sec_ace_t *ace = oc_list_head(oc_sec_get_acl(device)->subjects); + for (; ace != NULL; ace = ace->next) { + if (dps_has_tag(ace->tag, DPS_TAG, DPS_TAG_LEN)) { + DPS_DBG("\ttag(%s) added to aceid=%d", DPS_STALE_TAG, ace->aceid); + oc_set_string(&ace->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN); + continue; + } + } +} + +void +dps_acls_remove_stale_tag(size_t device) +{ + DPS_DBG("removing tags from acls"); + oc_sec_ace_t *ace = oc_list_head(oc_sec_get_acl(device)->subjects); + for (; ace != NULL; ace = ace->next) { + if (dps_has_tag(ace->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN)) { + DPS_DBG("\ttag(%s) removed from aceid=%d", DPS_STALE_TAG, ace->aceid); + oc_set_string(&ace->tag, DPS_TAG, DPS_TAG_LEN); + continue; + } + } +} + +void +dps_credentials_set_stale_tag(size_t device) +{ + DPS_DBG("adding stale tag to credentials:"); + oc_sec_creds_t *creds = oc_sec_get_creds(device); + oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + for (; cred != NULL; cred = cred->next) { + if (dps_has_tag(cred->tag, DPS_TAG, DPS_TAG_LEN)) { + DPS_DBG("\ttag(%s) added to credid=%d", DPS_STALE_TAG, cred->credid); + oc_set_string(&cred->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN); + continue; + } + } +} + +void +dps_credentials_remove_stale_tag(size_t device) +{ + DPS_DBG("removing stale tag from credentials"); + oc_sec_creds_t *creds = oc_sec_get_creds(device); + oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + for (; cred != NULL; cred = cred->next) { + if (dps_has_tag(cred->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN)) { + DPS_DBG("\ttag(%s) removed from credid=%d", DPS_STALE_TAG, cred->credid); + oc_set_string(&cred->tag, DPS_TAG, DPS_TAG_LEN); + continue; + } + } +} + +void +dps_remove_stale_acls(size_t device) +{ + DPS_DBG("removing tagged acls:"); + oc_sec_ace_t *ace = oc_list_head(oc_sec_get_acl(device)->subjects); + while (ace != NULL) { + oc_sec_ace_t *next = ace->next; + if (dps_has_tag(ace->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN)) { + DPS_DBG("\tstale aceid=%d removed", ace->aceid); + oc_sec_remove_ace(ace, device); + } + ace = next; + } +} + +int +dps_remove_stale_credentials(size_t device) +{ + DPS_DBG("removing stale credentials:"); + int count = 0; + oc_sec_creds_t *creds = oc_sec_get_creds(device); + oc_sec_cred_t *cred = (oc_sec_cred_t *)oc_list_head(creds->creds); + while (cred != NULL) { + oc_sec_cred_t *next = cred->next; + if (dps_has_tag(cred->tag, DPS_STALE_TAG, DPS_STALE_TAG_LEN)) { + DPS_DBG("\tstale credid=%d removed", cred->credid); + oc_sec_remove_cred(cred, device); + ++count; + } + cred = next; + } + return count; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_tag_internal.h b/api/plgd/device-provisioning-client/plgd_dps_tag_internal.h new file mode 100644 index 0000000000..393c00b553 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_tag_internal.h @@ -0,0 +1,84 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_TAG_INTERNAL_H +#define PLGD_DPS_TAG_INTERNAL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// annotation string written to tag of identity certificates or ACLs retrieved +/// from device-provisioning-service +#define DPS_TAG "dps" +#define DPS_TAG_LEN (sizeof(DPS_TAG) - 1) +// annotation string written to tag of identity certificates that have been +// added in a previous step of provisioning +#define DPS_STALE_TAG "dps-stale" +#define DPS_STALE_TAG_LEN (sizeof(DPS_STALE_TAG) - 1) + +/** + * @brief Set stale tag to DPS ACLs. + * + * @param device index of the device + */ +void dps_acls_set_stale_tag(size_t device); + +/** + * @brief Remove stale tag from DPS ACLs. + * + * @param device index of the device + */ +void dps_acls_remove_stale_tag(size_t device); + +/** + * @brief Set stale tag to DPS credentials. + * + * @param device index of the device + */ +void dps_credentials_set_stale_tag(size_t device); + +/** + * @brief Remove stale tag from DPS credentials. + * + * @param device index of the device + */ +void dps_credentials_remove_stale_tag(size_t device); + +/** + * @brief Remove acls tagged with the stale tag. + * + * @param device index of the device + */ +void dps_remove_stale_acls(size_t device); + +/** + * @brief Remove credentials tagged with the stale tag. + * + * @param device index of the device + * @return the number of removed credentials + */ +int dps_remove_stale_credentials(size_t device); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_TAG_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_time.c b/api/plgd/device-provisioning-client/plgd_dps_time.c new file mode 100644 index 0000000000..377a9155e6 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_time.c @@ -0,0 +1,227 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_endpoint_internal.h" +#include "plgd_dps_manager_internal.h" +#include "plgd_dps_provision_internal.h" +#include "plgd_dps_time_internal.h" +#include "plgd_dps_verify_certificate_internal.h" +#include "plgd/plgd_time.h" + +#include "oc_api.h" +#include "oc_clock_util.h" +#include "port/oc_clock.h" +#include "port/oc_connectivity.h" +#include "security/oc_pstat_internal.h" + +#include + +#define DPS_ONE_HOUR ((oc_clock_time_t)(60 * 60) * OC_CLOCK_SECOND) + +struct +{ + oc_clock_time_t delta; // the minimal difference between the system clock and + // the time calculated by plgd-time required for the + // system time to be considered unreliable + // TODO: make this configurable +} g_dps_time_cfg = { + .delta = DPS_ONE_HOUR, +}; + +bool +dps_has_plgd_time(void) +{ + return plgd_time_is_active(); +} + +static bool +dps_system_time_is_synchronized(oc_clock_time_t system_time, + oc_clock_time_t plgd_time) +{ + return system_time > plgd_time || + plgd_time - system_time <= g_dps_time_cfg.delta; +} + +oc_clock_time_t +dps_time(void) +{ + oc_clock_time_t now = oc_clock_time(); + if (!plgd_time_is_active()) { + return now; + } + oc_clock_time_t plgd_now = plgd_time(); + return dps_system_time_is_synchronized(now, plgd_now) ? now : plgd_now; +} + +static int +dps_set_time(oc_clock_time_t time) +{ +#if DPS_DBG_IS_ENABLED || DPS_INFO_IS_ENABLED + oc_clock_time_t now = oc_clock_time(); +#endif +#if DPS_DBG_IS_ENABLED +// GCOVR_EXCL_START +#define RFC3339_BUFFER_SIZE (64) + char system_ts[RFC3339_BUFFER_SIZE] = { 0 }; + oc_clock_encode_time_rfc3339(now, system_ts, sizeof(system_ts)); + char server_ts[RFC3339_BUFFER_SIZE] = { 0 }; + oc_clock_encode_time_rfc3339(time, server_ts, sizeof(server_ts)); + DPS_DBG("set time: system_time=%s, server time=%s", system_ts, server_ts); +// GCOVR_EXCL_STOP +#endif /* DPS_DBG_IS_ENABLED */ +#if DPS_INFO_IS_ENABLED + // GCOVR_EXCL_START + if (!dps_system_time_is_synchronized(now, time)) { + DPS_INFO("System time desynchronization detected"); + } + // GCOVR_EXCL_STOP +#endif /* DPS_INFO_IS_ENABLED */ + return plgd_time_set_time(time); +} + +static void +dps_get_time_handler(oc_status_t code, oc_clock_time_t time, void *data) +{ + plgd_dps_context_t *ctx = (plgd_dps_context_t *)data; +#if DPS_DBG_IS_ENABLED + dps_print_status("get time handler: ", ctx->status); +#endif /* DPS_DBG_IS_ENABLED */ + + // we check only for PLGD_DPS_FAILURE flag, because retry will be rescheduled + // if necessary + if ((ctx->status & (PLGD_DPS_HAS_TIME | PLGD_DPS_FAILURE)) == + PLGD_DPS_HAS_TIME) { + DPS_DBG("skipping duplicit call of get time handler"); + return; + } + // execute status callback right after this handler ends + dps_reset_delayed_callback(ctx, dps_status_callback_handler, 0); + oc_remove_delayed_callback(ctx, dps_manager_provision_retry_async); + ctx->status &= ~PLGD_DPS_PROVISIONED_ERROR_FLAGS; + + const uint32_t expected_status = PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME; + if (ctx->status != expected_status) { +#if DPS_ERR_IS_ENABLED + // GCOVR_EXCL_START + char str[256]; // NOLINT + int ret = dps_status_to_logstr(ctx->status, str, sizeof(str)); + DPS_ERR("invalid status(%u:%s) in get time handler", (unsigned)ctx->status, + ret >= 0 ? str : "(NULL)"); + // GCOVR_EXCL_STOP +#endif /* DPS_ERR_IS_ENABLED */ + goto error; + } + + plgd_dps_error_t err = dps_provisioning_check_response(ctx, code, NULL); + if (err != PLGD_DPS_OK) { + DPS_ERR("invalid %s response(code=%d)", PLGD_DPS_TIME_URI, code); + // ctx->status and ctx->last_error are set in + // dps_provisioning_check_response + goto finish; + } + + if (dps_set_time(time) != 0) { + DPS_ERR("cannot set time"); + goto error; + } + + dps_set_ps_and_last_error( + ctx, PLGD_DPS_HAS_TIME, + PLGD_DPS_GET_TIME | PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + dps_retry_reset(ctx, dps_provision_get_next_action(ctx)); + ctx->transient_retry_count = 0; + + // if we are waiting for an insecure TCP session to close the next step will + // be scheduled from the session disconnect handler + if ((ctx->endpoint_state == OC_SESSION_DISCONNECTED) || + !ctx->closing_insecure_peer) { + // go to next step -> get owner + dps_provisioning_schedule_next_step(ctx); + } + return; + +error: + dps_set_ps_and_last_error(ctx, PLGD_DPS_FAILURE, PLGD_DPS_HAS_TIME, + PLGD_DPS_ERROR_GET_TIME); +finish: + if ((ctx->status & PLGD_DPS_PROVISIONED_ERROR_FLAGS) != 0) { + // when waiting to close insecure peer the scheduling of retry is handled by + // the session disconnected handler + dps_provisioning_handle_failure( + ctx, code, + (ctx->endpoint_state == OC_SESSION_DISCONNECTED) || + !ctx->closing_insecure_peer); + } +} + +bool +dps_get_plgd_time(plgd_dps_context_t *ctx) +{ + assert(ctx != NULL); + DPS_INFO("Get time"); + const oc_sec_pstat_t *pstat = oc_sec_get_pstat(ctx->device); + if (pstat->s != OC_DOS_RFNOP) { + return false; + } + + oc_tls_select_cloud_ciphersuite(); + + plgd_time_fetch_config_t fetch_cfg; + if (ctx->skip_verify) { + dps_verify_certificate_data_t *vcd = dps_verify_certificate_data_new( + oc_tls_peer_pki_default_verification_params()); + if (vcd == NULL) { + return false; + } + oc_pki_user_data_t verify_data = { + .data = vcd, + .free = dps_verify_certificate_data_free, + }; + fetch_cfg = plgd_time_fetch_config_with_custom_verification( + ctx->endpoint, PLGD_DPS_TIME_URI, dps_get_time_handler, ctx, + dps_retry_get_timeout(&ctx->retry), + PLGD_DPS_DISABLE_SELECT_IDENTITY_CERT_CHAIN, dps_verify_certificate, + verify_data); + } else { + fetch_cfg = plgd_time_fetch_config( + ctx->endpoint, PLGD_DPS_TIME_URI, dps_get_time_handler, ctx, + dps_retry_get_timeout(&ctx->retry), + PLGD_DPS_DISABLE_SELECT_IDENTITY_CERT_CHAIN, true); + } + + unsigned flags = 0; + if (!plgd_time_fetch(fetch_cfg, &flags)) { + DPS_ERR("failed to dispatch get time from endpoint"); + dps_reset_tls(); + return false; + } + DPS_DBG("Get time: flags=%u", flags); + if ((flags & PLGD_TIME_FETCH_FLAG_TCP_SESSION_OPENED) != 0) { + ctx->closing_insecure_peer = true; + } + +#if DPS_DBG_IS_ENABLED + dps_endpoint_print_peers(ctx->endpoint); +#endif /* DPS_DBG_IS_ENABLED */ + + dps_set_ps_and_last_error(ctx, PLGD_DPS_GET_TIME, + PLGD_DPS_PROVISIONED_ERROR_FLAGS, PLGD_DPS_OK); + return true; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_time_internal.h b/api/plgd/device-provisioning-client/plgd_dps_time_internal.h new file mode 100644 index 0000000000..a3e69d76f6 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_time_internal.h @@ -0,0 +1,78 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_TIME_INTERNAL_H +#define PLGD_DPS_TIME_INTERNAL_H + +#include "plgd_dps_internal.h" + +#include "port/oc_clock.h" +#include "util/oc_compiler.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief URI to retrieve time + * + * Expected response: + * { + * time: + * } + */ +#define PLGD_DPS_TIME_URI "/x.plgd.dev/time" + +/** + * @brief Check if the time of the device was synchronized previously. + * + * @return true time has been synchronize at least once + * @return false time has not been yet synchronized + */ +bool dps_has_plgd_time(void); + +/** + * @brief Request current time from server. + * + * Prepare and send a GET request to PLGD_DPS_TIME_URI and register handler for + * a response with the current server time. + * + * @param ctx device registration context (cannot be NULL) + * @return true GET request was successfully dispatched + * @return false on failure + */ +bool dps_get_plgd_time(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Get current time. + * + * If the plgd-time feature is active the function will return its current time + * approximation. Otherwise time returned by oc_clock_time() is used. + * + * @return current time on success + * @return (oc_clock_time_t)-1 on error + */ +oc_clock_time_t dps_time(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_TIME_INTERNAL_H */ diff --git a/api/plgd/device-provisioning-client/plgd_dps_verify_certificate.c b/api/plgd/device-provisioning-client/plgd_dps_verify_certificate.c new file mode 100644 index 0000000000..b2e7682bff --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_verify_certificate.c @@ -0,0 +1,218 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#include "plgd_dps_apis_internal.h" +#include "plgd_dps_context_internal.h" +#include "plgd_dps_log_internal.h" +#include "plgd_dps_verify_certificate_internal.h" + +#include "oc_config.h" +#include "util/oc_memb.h" + +OC_MEMB(g_dps_verify_certificate_data_pool, dps_verify_certificate_data_t, + OC_MAX_NUM_DEVICES); + +dps_verify_certificate_data_t * +dps_verify_certificate_data_new(oc_tls_pki_verification_params_t orig_verify) +{ + dps_verify_certificate_data_t *vcd = + (dps_verify_certificate_data_t *)oc_memb_alloc( + &g_dps_verify_certificate_data_pool); + if (vcd == NULL) { + DPS_ERR("oc_memb_alloc verify_certificate_data failed"); + return NULL; + } + vcd->fingerprint_verified = false; + vcd->orig_verify = orig_verify; + return vcd; +} + +void +dps_verify_certificate_data_free(void *data) +{ + if (data == NULL) { + return; + } + dps_verify_certificate_data_t *verify_data = + (dps_verify_certificate_data_t *)data; + if (verify_data->orig_verify.user_data.free != NULL) { + verify_data->orig_verify.user_data.free( + verify_data->orig_verify.user_data.data); + } + oc_memb_free(&g_dps_verify_certificate_data_pool, verify_data); +} + +#if DPS_DBG_IS_ENABLED + +void +dps_print_fingerprint(mbedtls_md_type_t md_type, + const unsigned char *fingerprint, size_t fingerprint_size) +{ + // GCOVR_EXCL_START + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(md_type); + if (md_info == NULL) { + DPS_ERR("dps_print_fingerprint - failed to get md_info from type %d", + md_type); + return; + } + +#define MD_NAME_SIZE 100 + char md_name[MD_NAME_SIZE] = { 0 }; + const char *md_name_tmp = mbedtls_md_get_name(md_info); + if (md_name_tmp == NULL) { + DPS_ERR("dps_print_fingerprint - failed to get md_name from md_info"); + return; + } + size_t md_name_tmp_len = strlen(md_name_tmp); + size_t md_name_len = md_name_tmp_len > sizeof(md_name) - 1 + ? sizeof(md_name) - 1 + : md_name_tmp_len; + memcpy(md_name, md_name_tmp, md_name_len); + md_name[md_name_len] = '\0'; + +#define BUFFER_SIZE \ + (sizeof(md_name) - 1 + (size_t)(3 * MBEDTLS_MD_MAX_SIZE) + 1) + char buffer[BUFFER_SIZE] = { 0 }; + size_t buffer_size = sizeof(buffer); + snprintf(buffer, buffer_size, "%s", md_name); + char prefix = ' '; + for (size_t i = 0, j = md_name_len; i < fingerprint_size; i++, j += 3) { + snprintf(buffer + j, buffer_size - j, "%c%02X", prefix, fingerprint[i]); + prefix = ':'; + } + DPS_DBG("fingerprint: %s", buffer); + // GCOVR_EXCL_STOP +} + +#endif /* DPS_DBG_IS_ENABLED */ + +static bool +calculate_fingerprint(const plgd_dps_context_t *ctx, + const mbedtls_x509_crt *crt, unsigned char *fingerprint, + size_t *fingerprint_size) +{ + assert(ctx); + assert(crt); + assert(fingerprint); + assert(fingerprint_size); + + if (ctx->certificate_fingerprint.md_type == MBEDTLS_MD_NONE) { + return true; + } + const mbedtls_md_info_t *md_info = + mbedtls_md_info_from_type(ctx->certificate_fingerprint.md_type); + if (md_info == NULL) { + DPS_ERR("calculate certificate fingerprint algorithm not found"); + return false; + } + + int ret = mbedtls_md(md_info, crt->raw.p, crt->raw.len, fingerprint); + if (ret != 0) { + DPS_ERR("calculate certificate fingerprint failed %x", ret); + return false; + } + *fingerprint_size = mbedtls_md_get_size(md_info); +#if DPS_DBG_IS_ENABLED + dps_print_fingerprint(ctx->certificate_fingerprint.md_type, fingerprint, + *fingerprint_size); +#endif /* DPS_DBG_IS_ENABLED */ + return true; +} + +int +dps_verify_certificate(oc_tls_peer_t *peer, const mbedtls_x509_crt *crt, + int depth, uint32_t *flags) +{ + DPS_DBG("verifying certificate at depth %d, flags %u", depth, *flags); + + const plgd_dps_context_t *ctx = plgd_dps_get_context(peer->endpoint.device); + if (ctx == NULL) { + DPS_ERR("verifying certificate - context is NULL"); + return -1; + } + + unsigned char fingerprint[MBEDTLS_MD_MAX_SIZE] = { 0 }; + /* buffer is max length of returned hash, which is 64 in case we use sha-512 + */ + size_t fingerprint_size = 0; + if (!calculate_fingerprint(ctx, crt, fingerprint, &fingerprint_size)) { + return -1; + } + dps_verify_certificate_data_t *cb_data = + (dps_verify_certificate_data_t *)peer->user_data.data; + if (cb_data == NULL) { + DPS_ERR("verifying certificate - cb_data is NULL"); + return -1; + } + + // check fingerprint every time + if (ctx->certificate_fingerprint.md_type != MBEDTLS_MD_NONE && + dps_is_equal_string_len(ctx->certificate_fingerprint.data, + (const char *)fingerprint, fingerprint_size)) { + DPS_DBG("verifying certificate - fingerprint matches"); + cb_data->fingerprint_verified = true; + } + + oc_tls_pki_verification_params_t dps_verify = { + .user_data = peer->user_data, + .verify_certificate = peer->verify_certificate, + }; + // set original parameters on the peer + peer->verify_certificate = cb_data->orig_verify.verify_certificate; + peer->user_data = cb_data->orig_verify.user_data; + int ret = peer->verify_certificate(peer, crt, depth, flags); + // restore dps configuration + peer->verify_certificate = dps_verify.verify_certificate; + peer->user_data = dps_verify.user_data; + if (ret == 0 && (flags == NULL || *flags == 0)) { + DPS_DBG("verifying certificate - orig_verify_certificate returned 0 and " + "flags is 0 - accept connection"); + return 0; + } + if (ctx->certificate_fingerprint.md_type != MBEDTLS_MD_NONE) { + DPS_DBG("verifying certificate - verifying fingerprint"); + if (depth > 0) { + DPS_DBG("verifying certificate - continue check"); + *flags = 0; + return 0; + } + if (cb_data->fingerprint_verified) { + DPS_DBG("verifying certificate - fingerprint valid - accept connection"); + if (flags != NULL) { + *flags = 0; + } + if (peer->user_data.free != NULL) { + peer->user_data.free(peer->user_data.data); + } + peer->user_data.data = NULL; + peer->user_data.free = NULL; + return 0; + } + DPS_ERR( + "verifying certificate - fingerprint is invalid - reject connection"); + return -1; + } + if (ctx->skip_verify) { + DPS_DBG("verifying certificate - skip verify"); + if (flags != NULL) { + *flags = 0; + } + return 0; + } + return ret; +} diff --git a/api/plgd/device-provisioning-client/plgd_dps_verify_certificate_internal.h b/api/plgd/device-provisioning-client/plgd_dps_verify_certificate_internal.h new file mode 100644 index 0000000000..1882b656bd --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_verify_certificate_internal.h @@ -0,0 +1,96 @@ +/**************************************************************************** + * + * Copyright (c) 2022-2024 plgd.dev, s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_VERIFY_CERTIFICATE_INTERNAL_H +#define PLGD_DPS_VERIFY_CERTIFICATE_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "plgd_dps_log_internal.h" + +#include "oc_pki.h" +#include "security/oc_tls_internal.h" // oc_tls_peer_t +#include "util/oc_compiler.h" + +#include "mbedtls/build_info.h" +#include "mbedtls/md.h" + +#include +#include + +/** + * @brief User data for custom certificate verification function that stores the + * original verification function with data + */ +typedef struct +{ + oc_tls_pki_verification_params_t orig_verify; + bool fingerprint_verified; +} dps_verify_certificate_data_t; + +/** + * @brief Allocate and initialize custom user data for dps_verify_certificate + * + * @param orig_verify original vertification parameters + * @return dps_verify_certificate_data_t* on success allocated and initialized + * data + * @return NULL on failure + */ +dps_verify_certificate_data_t *dps_verify_certificate_data_new( + oc_tls_pki_verification_params_t orig_verify); + +/** + * @brief Free previously allocated dps_verify_certificate_data_t + * + * @note void* is used to match oc_pki_user_data_t::free signature + * + * @param data dps_verify_certificate_data_t* + */ +void dps_verify_certificate_data_free(void *data); + +/** + * @brief Certificate verification function that invokes the original + * verification function stored in the peers user data. If the original + * verification fails then fingerprint verification runs if is enabled. + * + * @param peer (D)TLS peer (cannot be NULL) + * @param crt certificate (cannot be NULL) + * @param depth depth of the certificate within the certificate chain + * @param[out] flags verification flags + * @return 0 on success + * @return != 0 on failure + */ +int dps_verify_certificate(oc_tls_peer_t *peer, const mbedtls_x509_crt *crt, + int depth, uint32_t *flags) OC_NONNULL(1, 2); + +#if DPS_DBG_IS_ENABLED + +/// @brief Print fingerprint. +void dps_print_fingerprint(mbedtls_md_type_t md_type, + const unsigned char *fingerprint, + size_t fingerprint_size); + +#endif /* DPS_DBG_IS_ENABLED */ + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_VERIFY_CERTIFICATE_INTERNAL_H */ diff --git a/api/plgd/plgd.cmake b/api/plgd/plgd.cmake new file mode 100644 index 0000000000..58576f9002 --- /dev/null +++ b/api/plgd/plgd.cmake @@ -0,0 +1,5 @@ +include_guard(GLOBAL) + +file(GLOB PLGD_DPS_SRC + ${PROJECT_SOURCE_DIR}/api/plgd/device-provisioning-client/*.c +) diff --git a/include/plgd/plgd_dps.h b/include/plgd/plgd_dps.h new file mode 100644 index 0000000000..bbbf2eb8c1 --- /dev/null +++ b/include/plgd/plgd_dps.h @@ -0,0 +1,885 @@ +/**************************************************************************** + * + * Copyright (c) 2022 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +#ifndef PLGD_DPS_H +#define PLGD_DPS_H + +#include "oc_config.h" + +#ifndef OC_SECURITY +#error "OC_SECURITY must be defined" +#endif + +#ifndef OC_PKI +#error "OC_PKI must be defined" +#endif + +#ifndef OC_CLOUD +#error "OC_CLOUD must be defined" +#endif + +#ifndef OC_IPV4 +#error "OC_IPV4 must be defined" +#endif + +#ifndef OC_STORAGE +#error "OC_STORAGE must be defined" +#endif + +#include "oc_export.h" +#include "oc_client_state.h" +#include "oc_cloud.h" +#include "oc_ri.h" +#include "oc_session_events.h" +#include "util/oc_compiler.h" + +#include "mbedtls/md.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Log component determines the source of the message. + */ +typedef enum { + PLGD_DPS_LOG_COMPONENT_DEFAULT = 1 << 0, ///< default, non-specific component +} plgd_dps_log_component_t; + +/** + * @brief Custom logging function + * + * @param level log level of the message + * @param component log component of the message + * @param file file of the log message call + * @param line line of the log message call in \p file + * @param func_name function name in which the log message call is invoked + * @param format format of the log message + */ +typedef void (*plgd_dps_print_log_fn_t)(oc_log_level_t level, + plgd_dps_log_component_t component, + const char *file, int line, + const char *func_name, + const char *format, ...) + OC_PRINTF_FORMAT(6, 7) OC_NONNULL(); + +/// @brief Set global logging function +OC_API +void plgd_dps_set_log_fn(plgd_dps_print_log_fn_t log_fn); + +/// @brief Get global logging function +OC_API +plgd_dps_print_log_fn_t plgd_dps_get_log_fn(void) OC_RETURNS_NONNULL; + +/** + * @brief Set log level of the global logger, logs with lower importance will be + * ignored. It is thread safe. + * + * @param level Log level + * @note If log level is not set, the default log level is OC_LOG_LEVEL_INFO. + */ +OC_API +void plgd_dps_log_set_level(oc_log_level_t level); + +/** + * @brief Get log level of the global logger. It is thread safe. + * + * @return Log level + */ +OC_API +oc_log_level_t plgd_dps_log_get_level(void); + +/** + * @brief Set enabled log components of the global logger. It is thread safe. + * + * @param components mask of enabled log components + */ +OC_API +void plgd_dps_log_set_components(uint32_t components); + +/** + * @brief Get enabled log components of the global logger. It is thread safe. + * + * @return uint32_t mask of enabled log components + */ +OC_API +uint32_t plgd_dps_log_get_components(void); + +typedef struct plgd_dps_context_t plgd_dps_context_t; + +/** + * @brief DPS provisioning status flags. + */ +typedef enum { + /* UNINITIALIZED = 0 */ + PLGD_DPS_INITIALIZED = 1 << 0, + PLGD_DPS_GET_CREDENTIALS = 1 << 1, + PLGD_DPS_HAS_CREDENTIALS = 1 << 2, + PLGD_DPS_GET_ACLS = 1 << 3, + PLGD_DPS_HAS_ACLS = 1 << 4, + PLGD_DPS_GET_CLOUD = 1 << 6, + PLGD_DPS_HAS_CLOUD = 1 << 7, + PLGD_DPS_CLOUD_STARTED = 1 << 8, + PLGD_DPS_RENEW_CREDENTIALS = 1 << 9, + PLGD_DPS_GET_OWNER = 1 << 10, + PLGD_DPS_HAS_OWNER = 1 << 11, + PLGD_DPS_GET_TIME = 1 << 12, + PLGD_DPS_HAS_TIME = 1 << 13, + PLGD_DPS_TRANSIENT_FAILURE = 1 << 29, + PLGD_DPS_FAILURE = 1 << 30, +} plgd_dps_status_t; + +/** + * @brief DPS errors. + */ +typedef enum { + PLGD_DPS_OK = 0, + PLGD_DPS_ERROR_RESPONSE = 1, + PLGD_DPS_ERROR_CONNECT = 2, + PLGD_DPS_ERROR_GET_CREDENTIALS = 3, + PLGD_DPS_ERROR_GET_ACLS = 4, + PLGD_DPS_ERROR_SET_CLOUD = 5, + PLGD_DPS_ERROR_START_CLOUD = 6, + PLGD_DPS_ERROR_GET_OWNER = 7, + PLGD_DPS_ERROR_GET_TIME = 8, +} plgd_dps_error_t; + +/** + @brief A function pointer for handling the dps status. + @param ctx dps context + @param status Current status of the dps. + @param data user data provided to the callback +*/ +typedef void (*plgd_dps_on_status_change_cb_t)(plgd_dps_context_t *ctx, + plgd_dps_status_t status, + void *data); + +/** + * @brief Allocate and initialize data. + * + * @return int 0 on success + * <0 on failure + */ +OC_API +int plgd_dps_init(void); + +/** + * @brief Stop all devices and deallocate data. + */ +OC_API +void plgd_dps_shutdown(void); + +/// Get context for given device +OC_API +plgd_dps_context_t *plgd_dps_get_context(size_t device); + +/** + * @brief Get device from context. + * + * @param ctx dps context (cannot be NULL) + * + * @return size_t index of device + */ +OC_API +size_t plgd_dps_get_device(const plgd_dps_context_t *ctx) OC_NONNULL(); + +typedef struct +{ + plgd_dps_on_status_change_cb_t + on_status_change; ///< callback executed on DPS status change + void * + on_status_change_data; ///< user data provided to DPS status change callback + oc_cloud_cb_t + on_cloud_status_change; ///< callback executed when cloud status change + void *on_cloud_status_change_data; ///< user data provided to cloud status + ///< change callback +} plgd_dps_manager_callbacks_t; + +/** + * @brief Set DPS manager callbacks. + * + * @param ctx dps context (cannot be NULL) + * @param callbacks callbacks with data + * + * Example of plgd_dps_on_status_change_cb_t function: + * @code{.c} + * static void + * on_change_cb(plgd_dps_context_t *ctx, plgd_dps_status_t status, void + * *on_change_data) { printf("DPS Manager Status:\n"); if (status & + * PLGD_DPS_INITIALIZED) { printf("\t-Initialized\n"); + * } + * ... + * } + * @endcode + * + * Example of oc_cloud_cb_t function: + * @code{.c} + * static void + * on_cloud_change_cb(oc_cloud_context_t *ctx, oc_cloud_status_t status, void + * *on_cloud_change_data) { printf("Cloud Manager Status:\n"); if (status & + * OC_CLOUD_REGISTERED) { printf("\t-Registered\n"); + * } + * ... + * } + * @endcode + */ +OC_API +void plgd_dps_set_manager_callbacks(plgd_dps_context_t *ctx, + plgd_dps_manager_callbacks_t callbacks) + OC_NONNULL(1); + +/** + * @brief Start DPS manager to provision device. + * + * Setup context, global session handlers and start DPS manager. + * + * Starting DPS also starts the retry mechanism, which will remain active until + * the device is successfully provisioned. If a provisioning step fails, it will + * be tried again after a time interval. The time interval depends on the retry + * counter (which is incremented on each retry) and uses the following values [ + * 10, 20, 40, 80, 120 ] in seconds. Meaning that the first retry is scheduled + * after 10 seconds after a failure, the second retry after 20 seconds, etc. + * After the interval reaches the maximal value (120 seconds) it resets back to + * the first value (10 seconds). + * + * @note Before starting the DPS manager, an endpoint must be added by + * plgd_dps_add_endpoint_address (if you add multiple endpoints then use + * plgd_dps_select_endpoint_address to select the endpoint that will be used to + * provision). Without an endpoint selected the provisioning will not start. + * + * @note The function examines the state of storage and some provisioning steps + * might be skipped if the stored data is evaluated as still valid. To force + * full reprovisioning call plgd_force_reprovision before this function. At the + * end of this call forced reprovisioning is disabled. + * @see plgd_force_reprovision + * + * @param ctx dps context (cannot be NULL) + * @return 0 on success + * @return -1 on failure + */ +OC_API +int plgd_dps_manager_start(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Check whether DPS manager has been started. + * + * @param ctx dps context (cannot be NULL) + * @return true DPS manager has been started + * @return false DPS manager has not been started + * + * @see plgd_dps_manager_start + */ +OC_API +bool plgd_dps_manager_is_started(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Stop DPS manager. + * + * Deregister handlers, clear context, stop DPS manager, close connection to DPS + * endpoint and remove identity certificates retrieved from DPS endpoint. + * + * @param ctx dps context (cannot be NULL) + */ +OC_API +void plgd_dps_manager_stop(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Restart DPS manager to provision device by given server. + * + * A convenience function equivalent to calling plgd_dps_manager_stop and + * plgd_dps_manager_start. + * + * @param ctx dps context (cannot be NULL) + * @return 0 on success + * @return -1 on failure + * + * @see plgd_dps_manager_start + * @see plgd_dps_manager_stop + */ +OC_API +int plgd_dps_manager_restart(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Start cloud manager with previously set server and callbacks. + * + * @param ctx dps context (cannot be NULL) + * @return true on success + * @return false otherwise + */ +OC_API +bool plgd_cloud_manager_start(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Clean-up of DPS provisioning on factory reset. + * + * The function must be called from the factory reset handler to clean-up data + * that has been invalidated by a factory reset. The clean-up includes: + * - stopping of DPS provisioning and resetting the provisioning status + * - disconnecting from DPS endpoint and resetting the endpoint address + * - resetting data in storage and committing the empty data to storage files + * - removing identifiers of identity certificates that have been deleted by + * factory reset + * + * @param ctx dps context (cannot be NULL) + * @return 0 on success + * @return -1 on failure + */ +OC_API +int plgd_dps_on_factory_reset(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Controls whether a dps client verifies the device provision service's + * certificate chain against trust anchor in the device. To set skip verify, it + * must be called before plgd_dps_manager_start. + * + * @param ctx dps context (cannot be NULL) + * @param skip_verify skip verification of the DPS service + */ +OC_API +void plgd_dps_set_skip_verify(plgd_dps_context_t *ctx, bool skip_verify) + OC_NONNULL(); + +/** + * @brief Get `skip verify` value from context. + * + * @param ctx dps context (cannot be NULL) + * @return true `skip verify` is enabled + * @return false `skip verify` is disabled + * + * @see plgd_dps_set_skip_verify + */ +OC_API +bool plgd_dps_get_skip_verify(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Force all steps of the provisioning process to be executed. + * + * A step that was successfully executed stores data in the storage and on the + * next start this data is still valid the step would be automatically skipped. + * + * @param ctx dps context (cannot be NULL) + * + * @see plgd_dps_manager_start + */ +OC_API +void plgd_dps_force_reprovision(plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Check if force reprovision flag is set. + * + * @param ctx dps context (cannot be NULL) + * @return true force reprovision is set + * @return false force reprovision is not set + */ +OC_API +bool plgd_dps_has_forced_reprovision(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Configuration resource + * + * Description: + * - Resource type: x.plgd.dps.conf + * - Resource structure in json format: + * { + * endpoint: string; + * lastErrorCode: int; + * provisionStatus: string; + * forceReprovision: bool; + * } + */ +#define PLGD_DPS_URI "/plgd/dps" + +/** + * @brief Controls whether a dps client creates configuration resource for + * managing dps client via COAPs API. + * + * @param ctx dps context (cannot be NULL) + * @param create set true for creating resource. set false to free memory of + * created resource. + */ +OC_API +void plgd_dps_set_configuration_resource(plgd_dps_context_t *ctx, bool create) + OC_NONNULL(); + +/** + * @brief Maximal size of the retry configuration array + */ +enum { PLGD_DPS_MAX_RETRY_VALUES_SIZE = 8 }; + +/** + * @brief Configure retry counter. + * + * @param ctx dps context (cannot be NULL) + * @param cfg array with new timeout values (must have [1, + * PLGD_DPS_MAX_RETRY_VALUES_SIZE> number of non-zero values) + * @param cfg_size size of the array with timeout values + * @return true on success + * @return false on failure + */ +OC_API +bool plgd_dps_set_retry_configuration(plgd_dps_context_t *ctx, + const uint8_t cfg[], size_t cfg_size) + OC_NONNULL(1); + +/** + * @brief Callback invoked by the dps manager when the dps wants to schedule + * an action. + * + * @param ctx dps context + * @param action One of PLGD_DPS_GET actions or PLGD_DPS_RENEW_CREDENTIALS to + * schedule, or 0 for reinitialization. + * @param retry_count Retries count - 0 means the first attempt to perform the + * action. + * @param delay Delay the action in milliseconds before executing it. + * @param timeout Timeout in seconds for the action. + * @param user_data User data passed from the caller. + * + * @return true if the dps manager should continue to schedule the action, + * false if the dps manager should restarts from the beginning. + */ +typedef bool (*plgd_dps_schedule_action_cb_t)( + plgd_dps_context_t *ctx, plgd_dps_status_t action, uint8_t retry_count, + uint64_t *delay, uint16_t *timeout, void *user_data) OC_NONNULL(1, 4, 5); + +/** + * @brief Set a custom scheduler for actions in the cloud manager. By default, + * the cloud manager uses its own scheduler. + * + * This function allows you to set a custom scheduler to define delay and + * timeout for actions. + * + * @param ctx Cloud context to update. Must not be NULL. + * @param on_schedule_action Callback invoked by the cloud manager when the + * cloud wants to schedule an action. + * @param user_data User data passed from the caller to be provided during the + * callback. + * + * @note The provided cloud context (`ctx`) must not be NULL. + * @see oc_cloud_schedule_action_cb_t + */ +OC_API +void plgd_dps_set_schedule_action( + plgd_dps_context_t *ctx, plgd_dps_schedule_action_cb_t on_schedule_action, + void *user_data) OC_NONNULL(1); + +/** + * @brief Get retry counter configuration. + * + * @param ctx dps context (cannot be NULL) + * @param[out] buffer output buffer into which the configuration will be copied + * (cannot be NULL, and must be large enough to contain the current + * configuration) + * @param buffer_size size of the output buffer + * @return >0 the size of the configuration array copied to buffer + * @return <0 on failure + */ +OC_API +int plgs_dps_get_retry_configuration(const plgd_dps_context_t *ctx, + uint8_t *buffer, size_t buffer_size) + OC_NONNULL(); + +/** + * @brief Get last provisioning error. + * + * @param ctx dps context (cannot be NULL) + * @return plgd_dps_error_t last provisioning error + */ +OC_API +plgd_dps_error_t plgd_dps_get_last_error(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Get provision status. + * + * @param ctx dps context (cannot be NULL) + * @return uint16_t current provision status + */ +OC_API +uint32_t plgd_dps_get_provision_status(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Check whether the device has been provisioned at least once since the + * last DPS reset initiated by a factory reset or by setting the endpoint to an + * empty value in the DPS resource. + * + * @param ctx dps context (cannot be NULL) + * @return true if DPS has been successfully provisioned at least once since the + * DPS context reset. + * @return false for otherwise + */ +OC_API +bool plgd_dps_has_been_provisioned_since_reset(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +typedef struct +{ + uint8_t + max_count; ///< the maximal number of retries with the same endpoint before + ///< retrying is stopped; if a previously untried endpoint is + ///< available then it is selected and the retrying is restarted + ///< with it; if no previously untried endpoint is available then + ///< a full reprovisioning of the client is triggered (default: + ///< 30) + uint8_t interval_s; ///< retry interval in seconds (default: 1) +} plgd_cloud_status_observer_configuration_t; + +/** + * @brief Configure cloud observer. + * + * @param ctx dps context (cannot be NULL) + * @param max_retry_count maximal number of retries, set to 0 to disable cloud + * status observer + * @param retry_interval_s retry interval in seconds (must be >0) + * @return true on success + * @return false on error caused by invalid parameters + */ +OC_API +bool plgd_dps_set_cloud_observer_configuration(plgd_dps_context_t *ctx, + uint8_t max_retry_count, + uint8_t retry_interval_s) + OC_NONNULL(); + +/** + * @brief Get cloud observer configuration + * + * @param ctx dps context (cannot be NULL) + * @return plgd_cloud_status_observer_configuration_t current cloud observer + * configuration + */ +OC_API +plgd_cloud_status_observer_configuration_t +plgd_dps_get_cloud_observer_configuration(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Set expiring-in limit of DPS certificates. + * + * If a certificate's valid-to timestamp is within the expiring-in limit + * (current time < valid_to and current time + expiring-in limit > valid_to) + * then the certificate is considered as expiring. Expiring certificates are not + * accepted during the get credentials step of DPS provisioning. If a expiring + * certificates is received then the step is retried to receive a newer + * certificate with longer expiration. + * + * @param ctx dps context (cannot be NULL) + * @param expiring_limit limit value in seconds + */ +OC_API +void plgd_dps_pki_set_expiring_limit(plgd_dps_context_t *ctx, + uint16_t expiring_limit) OC_NONNULL(); + +/** + * @brief Get expiring-in limit of DPS certificates + * + * @param ctx dps context (cannot be NULL) + * @return expiring-in limit in seconds + */ +OC_API +uint16_t plgd_dps_pki_get_expiring_limit(const plgd_dps_context_t *ctx) + OC_NONNULL(); + +/** + * @brief Set certificate fingerprint of the provisioning server. + * + * If the fingerprint is set then the DPS client + * will verify the fingerprint of the provisioning server certificate during the + * TLS handshake. If any certificate matching the fingerprint in the chain is + * found then the handshake is successful. + * + * @param ctx dps context (cannot be NULL) + * @param md_type hash algorithm used for fingerprint + * @param fingerprint fingerprint of the provisioning server certificate + * @param size size of the fingerprint + * @return true on success + */ +OC_API +bool plgd_dps_set_certificate_fingerprint(plgd_dps_context_t *ctx, + mbedtls_md_type_t md_type, + const uint8_t *fingerprint, + size_t size) OC_NONNULL(1); + +/** + * @brief Copy certificate fingerprint of the DPS service to output buffer. + * + * @param ctx dps context (cannot be NULL) + * @param[out] md_type hash algorithm used for fingerprint + * @param[out] buffer output buffer (cannot be NULL and must be large enough to + * contain the endpoint in a string format) + * @param buffer_size size of output buffer + * @return >0 on success, number of copied bytes to buffer + * @return 0 endpoint is not set, thus nothing was copied + * @return <0 on error + */ +OC_API +int plgd_dps_get_certificate_fingerprint(const plgd_dps_context_t *ctx, + mbedtls_md_type_t *md_type, + uint8_t *buffer, size_t buffer_size) + OC_NONNULL(); + +/** + * @brief Set the vendor encapsulated option code for the DPS endpoint. Used + * during call + * plgd_dps_set_dhcp_vendor_encapsulated_option_code_dps_certificate_fingerprint. + * + * @param ctx dps context (cannot be NULL) + * @param code vendor encapsulated option code for the DPS endpoint + */ +OC_API +void plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_endpoint( + plgd_dps_context_t *ctx, uint8_t code) OC_NONNULL(); + +/** + * @brief Get the vendor encapsulated option code for the DPS endpoint. Used + * during call + * plgd_dps_set_dhcp_vendor_encapsulated_option_code_dps_certificate_fingerprint. + * + * @param ctx dps context (cannot be NULL) + * @return uint8_t vendor encapsulated option code for the DPS endpoint + */ +OC_API +uint8_t plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_endpoint( + const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Set the vendor encapsulated option code for the DPS certificate + * fingerprint. Used during call + * plgd_dps_set_dhcp_vendor_encapsulated_option_code_dps_certificate_fingerprint. + * + * @param ctx dps context (cannot be NULL) + * @param code vendor encapsulated option code for the DPS certificate + * fingerprint. + */ +OC_API +void +plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_certificate_fingerprint( + plgd_dps_context_t *ctx, uint8_t code) OC_NONNULL(); + +/** + * @brief Get the vendor encapsulated option code for the DPS certificate + * fingerprint. Used during call + * plgd_dps_set_dhcp_vendor_encapsulated_option_code_dps_certificate_fingerprint. + * + * @param ctx dps context (cannot be NULL) + * @return uint8_t vendor encapsulated option code for the DPS certificate + * fingerprint. + */ +OC_API +uint8_t +plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_certificate_fingerprint( + const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Convert isc-dhcp leases file vendor encapsulated options to byte + * array. + * + * @param hex_string input hex string (cannot be NULL) in format "01:a:3:14" or + * "010a0314" + * @param hex_string_size vendor encapsulated options size in dhcp leases file. + * @param buffer output buffer into which the byte array will be copied or NULL + * to get the needed size + * @param buffer_size size of the output buffer + * @return >0 the size of used or needed to copy to buffer, -1 on error + */ +OC_API +ssize_t plgd_dps_hex_string_to_bytes(const char *hex_string, + size_t hex_string_size, uint8_t *buffer, + size_t buffer_size) OC_NONNULL(1); + +/** + * @brief DPS dhcp plgd_dps_dhcp_set_values_from_vendor_encapsulated_options + * return values. + */ +typedef enum { + PLGD_DPS_DHCP_SET_VALUES_ERROR = -1, // error or parsing values failed + PLGD_DPS_DHCP_SET_VALUES_NOT_CHANGED = 0, // nothing changed + PLGD_DPS_DHCP_SET_VALUES_UPDATED = 1, // just updated + PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION = + 2, // need to force reprovision with restart manager +} plgd_dps_dhcp_set_values_t; + +/** + * @brief Set DPS endpoint and certificate fingerprint that will be used in + * establishment of secure connection. + * + * @param ctx dps context (cannot be NULL) + * @param vendor_encapsulated_options vendor encapsulated options in byte array + * @param vendor_encapsulated_options_size vendor encapsulated options size in + * byte array + * @return one of plgd_dps_dhcp_set_values_t + */ +OC_API +plgd_dps_dhcp_set_values_t +plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + plgd_dps_context_t *ctx, const uint8_t *vendor_encapsulated_options, + size_t vendor_encapsulated_options_size) OC_NONNULL(); + +/** + * \defgroup dps_endpoints Support for multiple DPS endpoint addresses + * @{ + */ + +/** + * @brief Set endpoint address of the DPS service. + * + * Expected format of the endpoint is "coaps+tcp://${HOST}:${PORT}". For + * example: coaps+tcp://localhost:40030 + * + * If there are multiple endpoint addresses set then a successful call to this + * function will remove all other endpoint addresses and set the new endpoint + * address as the only one in the list of DPS endpoint addresses. + * + * @param ctx dps context (cannot be NULL) + * @param endpoint endpoint of the provisioning server (cannot be NULL) + * + * @deprecated Use plgd_dps_add_endpoint_address instead. + */ +OC_API +void plgd_dps_set_endpoint(plgd_dps_context_t *ctx, const char *endpoint) + OC_NONNULL() OC_DEPRECATED("Use plgd_dps_add_endpoint_address instead."); + +/** + * @brief Copy the selected endpoint address of the DPS service to output + * buffer. + * + * @param ctx dps context (cannot be NULL) + * @param[out] buffer output buffer (cannot be NULL and must be large enough to + * contain the endpoint in a string format) + * @param buffer_size size of output buffer + * @return >0 on success, number of copied bytes to buffer + * @return 0 endpoint is not set, thus nothing was copied + * @return <0 on error + */ +OC_API +int plgd_dps_get_endpoint(const plgd_dps_context_t *ctx, char *buffer, + size_t buffer_size) OC_NONNULL(); + +/** + * @brief Check if no DPS service endpoint is set. + * + * @param ctx dps context (cannot be NULL) + * @return true if no endpoint is set + * @return false otherwise + */ +OC_API +bool plgd_dps_endpoint_is_empty(const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** + * @brief Allocate and add an address to the list of DPS endpoint addresses. + * + * @param ctx dps context (cannot be NULL) + * @param uri endpoint address (cannot be NULL; the uri must be at least 1 + * character long and less than OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH characters + * long, otherwise the call will fail) + * @param uri_len length of \p uri + * @param name name of the DPS endpoint + * @param name_len length of \p name + * + * @return oc_endpoint_address_t* pointer to the allocated DPS endpoint address + * @return NULL on failure + */ +OC_API +oc_endpoint_address_t *plgd_dps_add_endpoint_address( + plgd_dps_context_t *ctx, const char *uri, size_t uri_len, const char *name, + size_t name_len) OC_NONNULL(1, 2); + +/** + * @brief Remove an address from the list of DPS endpoint addresses. + * + * @param ctx dps context (cannot be NULL) + * @param address endpoint address to remove + * + * @return true if the endpoint address was removed from the list of DPS + * endpoints + * @return false on failure + * + * @note The endpoints are stored in a list. If the selected server address is + * removed, then next server address in the list will be selected. If the + * selected server address is the last item in the list, then the first server + * address in the list will be selected (if it exists). + * + * @note The server is cached in the DPS context, so if you remove the selected + * endpoint address during provisioning then it might be necessary to restart + * the DPS manager for the change to take effect. + * @see plgd_dps_manager_restart + */ +OC_API +bool plgd_dps_remove_endpoint_address(plgd_dps_context_t *ctx, + const oc_endpoint_address_t *address) + OC_NONNULL(); + +/** + * @brief Iterate over DPS endpoint addresses. + * + * @param ctx dps context (cannot be NULL) + * @param iterate_fn callback function invoked for each DPS endpoint address + * (cannot be NULL) + * @param iterate_fn_data custom user data provided to \p iterate_fn + * + * @note The callback function \p iterate_fn must not modify the list of DPS + * endpoint addresses. + */ +OC_API +void plgd_dps_iterate_server_addresses( + const plgd_dps_context_t *ctx, oc_endpoint_addresses_iterate_fn_t iterate_fn, + void *iterate_fn_data) OC_NONNULL(1, 2); + +/** + * @brief Select an address from the list of DPS endpoint addresses. + * + * @param ctx dps context (cannot be NULL) + * @param address DPS endpoint address to select (cannot be NULL; must be in the + * list of DPS endpoints) + * + * @return true if the address was selected + * @return false on failure to select the address, because it is not in the list + * of DPS endpoint addresses + * + * @note The server is cached in the DPS context, so if you remove the selected + * endpoint address during provisioning then it might be necessary to restart + * the DPS manager for the change to take effect. + * @see plgd_dps_manager_restart + */ +OC_API +bool plgd_dps_select_endpoint_address(plgd_dps_context_t *ctx, + const oc_endpoint_address_t *address) + OC_NONNULL(); + +/** + * @brief Get the selected DPS endpoint address. + * + * @param ctx dps context (cannot be NULL) + * @return oc_endpoint_address_t* pointer to the selected DPS endpoint address + * @return NULL if no DPS endpoint address is selected + */ +OC_API +const oc_endpoint_address_t *plgd_dps_selected_endpoint_address( + const plgd_dps_context_t *ctx) OC_NONNULL(); + +/** @} */ // end of dps_endpoints + +#ifdef __cplusplus +} +#endif + +#endif /* PLGD_DPS_H */ diff --git a/messaging/coap/observe.c b/messaging/coap/observe.c index 5a83f93f37..0d9f8cf328 100644 --- a/messaging/coap/observe.c +++ b/messaging/coap/observe.c @@ -106,9 +106,19 @@ enum { #define OC_MAX_OBSERVE_SIZE OC_MAX_APP_DATA_SIZE #endif +#ifndef OC_DYNAMIC_ALLOCATION +#if OC_MAX_OBSERVE_SIZE == OC_MIN_APP_DATA_SIZE +#define OC_MIN_OBSERVE_SIZE OC_MIN_APP_DATA_SIZE +#else /* OC_MAX_OBSERVE_SIZE != OC_MIN_APP_DATA_SIZE */ +#define OC_MIN_OBSERVE_SIZE \ + (OC_MIN_APP_DATA_SIZE < OC_MAX_OBSERVE_SIZE ? OC_MIN_APP_DATA_SIZE \ + : OC_MAX_OBSERVE_SIZE) +#endif /* OC_MAX_OBSERVE_SIZE == OC_MIN_APP_DATA_SIZE */ +#else /* OC_DYNAMIC_ALLOCATION */ #define OC_MIN_OBSERVE_SIZE \ (OC_MIN_APP_DATA_SIZE < OC_MAX_OBSERVE_SIZE ? OC_MIN_APP_DATA_SIZE \ : OC_MAX_OBSERVE_SIZE) +#endif /* !OC_DYNAMIC_ALLOCATION */ #if defined(OC_RES_BATCH_SUPPORT) && defined(OC_DISCOVERY_RESOURCE_OBSERVABLE) diff --git a/security/oc_tls.c b/security/oc_tls.c index cdd6b58399..12471a2ada 100644 --- a/security/oc_tls.c +++ b/security/oc_tls.c @@ -94,11 +94,11 @@ #define TLS_LOG_MBEDTLS_ERROR(mbedtls_func_name, mbedtls_err) \ do { \ - if (mbedtls_err == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { \ + if ((mbedtls_err) == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { \ OC_TRACE("oc_tls: %s Close-Notify received", mbedtls_func_name); \ break; \ } \ - if (mbedtls_err == MBEDTLS_ERR_SSL_CLIENT_RECONNECT) { \ + if ((mbedtls_err) == MBEDTLS_ERR_SSL_CLIENT_RECONNECT) { \ OC_TRACE("oc_tls: %s Client wants to reconnect", mbedtls_func_name); \ break; \ } \ diff --git a/tools/utils.cmake b/tools/utils.cmake new file mode 100644 index 0000000000..230bf606b8 --- /dev/null +++ b/tools/utils.cmake @@ -0,0 +1,71 @@ +include_guard(GLOBAL) + +include(CheckCCompilerFlag) +include(CheckCXXCompilerFlag) + +# function oc_add_compile_options([GLOBAL] [IX_CXX] FLAGS [flags...]) +# +# Arguments: +# GLOBAL (option) flags are added as global compilation options +# FLAGS list of flags to check and add for both C and C++ +# CFLAGS list of flags to check and add for C +# CXXFLAGS list of flags to check and add for C++ +# +# Side-effect: C_COMPILER_SUPPORTS_${flag_name} / CXX_COMPILER_SUPPORTS_${flag_name} +# is created and set to ON/OFF based on the result of the check. This variable +# can be used in the context of the caller. +function(oc_add_compile_options) + set(options GLOBAL) + set(oneValueArgs) + set(multiValueArgs CFLAGS CXXFLAGS FLAGS) + cmake_parse_arguments(OC_ADD_COMPILE_OPTIONS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(flag IN LISTS OC_ADD_COMPILE_OPTIONS_FLAGS OC_ADD_COMPILE_OPTIONS_CFLAGS) + string(REPLACE "-" "_" flag_name ${flag}) + string(REPLACE "=" "_" flag_name ${flag_name}) + string(TOUPPER ${flag_name} flag_name) + set(flag_name "C_COMPILER_SUPPORTS${flag_name}") + unset(${flag_name}) + check_c_compiler_flag(${flag} ${flag_name}) + if((OC_ADD_COMPILE_OPTIONS_GLOBAL) AND (${${flag_name}})) + add_compile_options($<$:${flag}>) + endif() + set(${flag_name} ${${flag_name}} PARENT_SCOPE) + endforeach() + + foreach(flag IN LISTS OC_ADD_COMPILE_OPTIONS_FLAGS OC_ADD_COMPILE_OPTIONS_CXXFLAGS) + string(REPLACE "-" "_" flag_name ${flag}) + string(REPLACE "=" "_" flag_name ${flag_name}) + string(TOUPPER ${flag_name} flag_name) + set(flag_name "CXX_COMPILER_SUPPORTS${flag_name}") + unset(${flag_name}) + check_cxx_compiler_flag(${flag} ${flag_name}) + if((OC_ADD_COMPILE_OPTIONS_GLOBAL) AND (${${flag_name}})) + add_compile_options($<$:${flag}>) + endif() + set(${flag_name} ${${flag_name}} PARENT_SCOPE) + endforeach() +endfunction() + +function(oc_set_maximum_log_level level outlevel) + if(${level} STREQUAL "DISABLED") + set(level_int -1) + elseif(${level} STREQUAL "ERROR") + set(level_int 3) + elseif(${level} STREQUAL "WARNING") + set(level_int 4) + elseif(${level} STREQUAL "NOTICE") + set(level_int 5) + elseif(${level} STREQUAL "INFO") + set(level_int 6) + elseif(${level} STREQUAL "DEBUG") + set(level_int 7) + elseif(${level} STREQUAL "TRACE") + set(level_int 8) + else() + message(FATAL_ERROR "Invalid log level string: ${level}") + endif() + + # assign to output variable + set(${outlevel} ${level_int} PARENT_SCOPE) +endfunction() diff --git a/util/oc_memb.h b/util/oc_memb.h index 7345ffd11b..9e0c586e95 100644 --- a/util/oc_memb.h +++ b/util/oc_memb.h @@ -121,9 +121,9 @@ extern "C" { (void *)CC_CONCAT(name, _memb_mem), NULL } #define OC_MEMB_LOCAL(name, structure, num) \ char CC_CONCAT(name, _memb_count)[num]; \ - memset(CC_CONCAT(name, _memb_count), 0, num * sizeof(char)); \ + memset(CC_CONCAT(name, _memb_count), 0, (num) * sizeof(char)); \ structure CC_CONCAT(name, _memb_mem)[num]; \ - memset(CC_CONCAT(name, _memb_mem), 0, num * sizeof(structure)); \ + memset(CC_CONCAT(name, _memb_mem), 0, (num) * sizeof(structure)); \ oc_memb_t name = { sizeof(structure), num, CC_CONCAT(name, _memb_count), \ (void *)CC_CONCAT(name, _memb_mem), NULL }