diff --git a/.dockerignore b/.dockerignore index 6932961868..84e91c7004 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,12 @@ .github/ .clang-format .clang-tidy +**/*.o +**/*.gcno +**/*.gcda sonar-project.properties Dockerfile.* -build/ +build*/ images/ port/esp32/build/ port/esp32/esp-idf/ diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 5a814066ae..471bfa6bf6 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), 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 - args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_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" + # debug on, cloud on (ipv4+tcp on), plgd time off + - 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 @@ -138,7 +138,7 @@ jobs: # install_faketime: true uses: ./.github/workflows/unit-test-with-cfg.yml with: - build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -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 ${{ matrix.args }} + build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -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 -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON ${{ matrix.args }} build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} clang: ${{ ((github.event_name == 'workflow_dispatch' && inputs.clang) || matrix.clang) || false }} coverage: false diff --git a/.github/workflows/docker-build-publish-with-cfg.yml b/.github/workflows/docker-build-publish-with-cfg.yml index a463f29b36..dceaf61b48 100644 --- a/.github/workflows/docker-build-publish-with-cfg.yml +++ b/.github/workflows/docker-build-publish-with-cfg.yml @@ -80,5 +80,5 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | - BUILD_ARGS=-DOC_DEBUG_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON ${{ inputs.build_args }} + BUILD_ARGS=${{ inputs.build_args }} BUILD_TYPE=${{ inputs.build_type }} diff --git a/.github/workflows/docker-build-publish.yml b/.github/workflows/docker-build-publish.yml index 7f8c02a447..e8ed20a69a 100644 --- a/.github/workflows/docker-build-publish.yml +++ b/.github/workflows/docker-build-publish.yml @@ -20,9 +20,6 @@ on: - master workflow_dispatch: -env: - REGISTRY: ghcr.io - jobs: build-and-push-images: strategy: @@ -31,21 +28,41 @@ jobs: include: - name: cloud-server build_type: Release - build_args: "" + build_args: file: docker/apps/Dockerfile.cloud-server - name: cloud-server-debug build_type: Debug - build_args: "" + build_args: file: docker/apps/Dockerfile.cloud-server-debug - name: cloud-server-discovery-resource-observable build_type: Release - build_args: "-DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON" + build_args: -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON file: docker/apps/Dockerfile.cloud-server - name: cloud-server-discovery-resource-observable-debug build_type: Debug - build_args: "-DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON" + build_args: -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON file: docker/apps/Dockerfile.cloud-server-debug uses: ./.github/workflows/docker-build-publish-with-cfg.yml + with: + name: ${{ matrix.name }} + build_type: ${{ matrix.build_type }} + build_args: -DOC_DEBUG_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON ${{ matrix.build_args }} + file: ${{ matrix.file }} + + build-and-push-dps-images: + strategy: + fail-fast: false + matrix: + include: + - name: dps-cloud-server + build_type: Release + build_args: + file: docker/apps/Dockerfile.dps-cloud-server + - name: dps-cloud-server-debug + build_type: Debug + build_args: -DOC_DEBUG_ENABLED=ON -DOC_LOG_MAXIMUM_LOG_LEVEL=DEBUG -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON + file: docker/apps/Dockerfile.dps-cloud-server + uses: ./.github/workflows/docker-build-publish-with-cfg.yml with: name: ${{ matrix.name }} build_type: ${{ matrix.build_type }} diff --git a/.github/workflows/ghcr-check.yml b/.github/workflows/ghcr-check.yml index efaefba1c1..1955b10a91 100644 --- a/.github/workflows/ghcr-check.yml +++ b/.github/workflows/ghcr-check.yml @@ -16,6 +16,8 @@ on: - cloud-server-debug - cloud-server-discovery-resource-observable - cloud-server-discovery-resource-observable-debug + - dps-cloud-server + - dps-cloud-server-debug jobs: check_package: name: Check released packages diff --git a/.github/workflows/plgd-device-test-with-cfg.yml b/.github/workflows/plgd-device-test-with-cfg.yml index e433c812b0..48f7605f4e 100644 --- a/.github/workflows/plgd-device-test-with-cfg.yml +++ b/.github/workflows/plgd-device-test-with-cfg.yml @@ -113,7 +113,7 @@ jobs: if: ${{ inputs.coverage }} id: coverage run: | - SUFFIX=`echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ inputs.build_args }} ${{ inputs.name }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' '` + SUFFIX=$(echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ inputs.build_args }} ${{ inputs.name }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' ') echo "filename=coverage-plgd-device-${SUFFIX}.json" >> $GITHUB_OUTPUT echo "artifact=plgd-device-${SUFFIX}-coverage" >> $GITHUB_OUTPUT diff --git a/.github/workflows/plgd-dps-test-with-cfg.yml b/.github/workflows/plgd-dps-test-with-cfg.yml new file mode 100644 index 0000000000..d9748e00d6 --- /dev/null +++ b/.github/workflows/plgd-dps-test-with-cfg.yml @@ -0,0 +1,176 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Run plgd/hub/dps tests with dps_cloud_server and a single configuration + +on: + workflow_call: + inputs: + args: + description: arguments forwarded to the dps_cloud_server binary + type: string + required: false + default: "" + build_args: + description: build arguments forwarded to CMake in docker/apps/Dockerfile.cloud-server-debug + type: string + required: false + default: "" + build_type: + type: string + required: false + default: Debug + coverage: + description: gather and upload coverage data + type: boolean + required: false + default: false + docker_file: + description: Dockerfile used to build the dps_cloud_server container + type: string + required: false + default: docker/apps/Dockerfile.dps-cloud-server-debug + skip: + description: Skip this run of the job + type: boolean + required: false + default: false +env: + TEST_DPS_IMAGE: ghcr.io/plgd-dev/hub/test-device-provisioning-service:latest + +jobs: + plgd-hub-test-with-cfg: + if: ${{ !inputs.skip }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: "true" + + - name: Configure vm.mmap_rnd_bits for sanitizers + if: contains(inputs.build_args, 'OC_ASAN_ENABLED') || contains(inputs.build_args, 'OC_LSAN_ENABLED') || contains(inputs.build_args, 'OC_TSAN_ENABLED') || contains(inputs.build_args, 'OC_UBSAN_ENABLED') + run: | + sudo sysctl vm.mmap_rnd_bits + sudo sysctl -w vm.mmap_rnd_bits=28 + + - name: Build dps-cloudserver docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + build-args: | + BUILD_ARGS=${{ inputs.build_args }} + BUILD_TYPE=${{ inputs.build_type }} + file: ${{ inputs.docker_file }} + tags: dps-dbg:latest + + - name: Pull device-provisioning-service tests image + run: docker pull ${{ env.TEST_DPS_IMAGE }} + + - name: Prepare test environment + run: > + mkdir -p "/tmp/data/certs/device"; + chmod -R 0777 "/tmp/data"; + + docker run --rm --log-driver local --network=host + -e PREPARE_ENV=true + -e RUN=false + -v /tmp/data:/data + -v /var/run/docker.sock:/var/run/docker.sock + ${{ env.TEST_DPS_IMAGE }} + + - name: Run dps cloud server docker image + run: > + mkdir -p "/tmp/data/coverage"; + chmod -R 0777 "/tmp/data/coverage"; + + docker run --privileged -d --network=host --log-driver local + -v /tmp/data/certs/device:/dps/pki_certs + -v /tmp/data/coverage:/data/coverage + --name dps-devsim + dps-dbg:latest + --create-conf-resource + --no-verify-ca + --cloud-observer-max-retry 10 + --expiration-limit 10 + --retry-configuration 5 + --oc-log-level="info" + --log-level="debug" + --wait-for-reset + "dps-devsim-$(hostname)" + + - name: Run dps cloud server docker image for onboarding + run: > + docker run --privileged -d --network=host --log-driver local + -v /tmp/data/certs/device:/dps/pki_certs + -v /tmp/data/coverage:/data/coverage + --name dps-devsim-obt + dps-dbg:latest + --create-conf-resource + --cloud-observer-max-retry 10 + --expiration-limit 10 + --retry-configuration 5 + --oc-log-level="info" + --log-level="debug" + ${{ inputs.args }} + "dps-devsim-obt-$(hostname)" "" + + - name: Run device-provisioning-service tests image + run: > + docker run --rm --log-driver local --network=host --hostname="$(hostname)" + -e PREPARE_ENV=false + -e RUN=true + -v /tmp/data:/data + -v /var/run/docker.sock:/var/run/docker.sock + --name dps-tests + ${{ env.TEST_DPS_IMAGE }} + + - name: Generate file name and artifact name + if: ${{ inputs.coverage }} + id: coverage + run: | + SUFFIX=$(echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ inputs.build_args }} -DBUILD_TESTING=ON ${{ inputs.args }}" | sha1sum | cut -f 1 -d ' ') + echo "filename=coverage-plgd-dps-${SUFFIX}.json" >> $GITHUB_OUTPUT + echo "filename_obt=coverage-plgd-dps-obt-${SUFFIX}.json" >> $GITHUB_OUTPUT + echo "artifact=plgd-dps-${SUFFIX}-coverage" >> $GITHUB_OUTPUT + + - name: Gather coverage data + if: ${{ inputs.coverage }} + run: | + # stop to generate .gcda files + docker stop dps-devsim + # restart to generate report from the .gcda files + docker start dps-devsim + docker exec --workdir "/device-provisioning-client/tools" dps-devsim /bin/bash -c "./collect-coverage.sh --output /data/coverage/${{ steps.coverage.outputs.filename }}" + docker stop dps-devsim + + # stop to generate .gcda files + docker stop dps-devsim-obt + # restart to generate report from the .gcda files + docker start dps-devsim-obt + docker exec --workdir "/device-provisioning-client/tools" dps-devsim-obt /bin/bash -c "./collect-coverage.sh --output /data/coverage/${{ steps.coverage.outputs.filename_obt }}" + docker stop dps-devsim-obt + + - name: Upload coverage data + if: ${{ inputs.coverage }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.coverage.outputs.artifact }} + path: /tmp/data/coverage/coverage-plgd-dps-*.json + if-no-files-found: error + retention-days: 1 + + - name: Collect dps cloud server logs when the test fails + if: ${{ failure() }} + run: | + docker stop dps-devsim + docker logs dps-devsim + + - name: Collect dps cloud server (obt) logs when the test fails + if: ${{ failure() }} + run: | + docker stop dps-devsim-obt + docker logs dps-devsim-obt diff --git a/.github/workflows/plgd-dps-tests.yml b/.github/workflows/plgd-dps-tests.yml new file mode 100644 index 0000000000..0d15538e20 --- /dev/null +++ b/.github/workflows/plgd-dps-tests.yml @@ -0,0 +1,59 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Run plgd/hub/dps tests with dps_cloud_server + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} + +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [master] + pull_request: + branches: [master] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + build_type: + description: "Type of the build" + type: string + default: "Debug" + +jobs: + plgd-hub-test: + strategy: + fail-fast: false + matrix: + include: + - name: dps-cloud-server + # same configuration as "plgd-dps-tests" in the SonarCloud scan job, skip for events that trigger both jobs + skip: ${{ github.event_name != 'workflow_dispatch' }} + build_args: "" + - name: dps-cloud-server-asan + build_args: "-DOC_ASAN_ENABLED=ON" + docker_file: docker/apps/Dockerfile.dps-cloud-server + - name: dps-cloud-server-tsan + build_args: "-DOC_TSAN_ENABLED=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang" + docker_file: docker/apps/Dockerfile.dps-cloud-server + - name: dps-cloud-server-tsan + build_args: "-DOC_UBSAN_ENABLED=ON" + docker_file: docker/apps/Dockerfile.dps-cloud-server + - name: dps-cloud-server-faketime-system-time + args: --set-system-time + build_args: "-DPLGD_DPS_FAKETIME_ENABLED=ON" + docker_file: docker/apps/Dockerfile.dps-cloud-server-debug + - name: dps-cloud-server-faketime-mbedtls-time + build_args: "-DPLGD_DPS_FAKETIME_ENABLED=ON" + docker_file: docker/apps/Dockerfile.dps-cloud-server-debug + uses: ./.github/workflows/plgd-dps-test-with-cfg.yml + with: + args: ${{ matrix.args || '' }} + build_args: -DOC_DEBUG_ENABLED=ON -DPLGD_DPS_MAXIMUM_LOG_LEVEL=TRACE -DPLGD_DPS_CLOUD_SERVER_DBG_ENABLED=ON ${{ matrix.build_args }} + build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} + docker_file: ${{ matrix.docker_file || 'docker/apps/Dockerfile.dps-cloud-server-debug' }} + skip: ${{ matrix.skip || false }} diff --git a/.github/workflows/plgd-hub-test-with-cfg.yml b/.github/workflows/plgd-hub-test-with-cfg.yml index e33881d812..327b7a3a87 100644 --- a/.github/workflows/plgd-hub-test-with-cfg.yml +++ b/.github/workflows/plgd-hub-test-with-cfg.yml @@ -107,7 +107,7 @@ jobs: if: ${{ inputs.coverage }} id: coverage run: | - SUFFIX=`echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ inputs.build_args }} ${{ inputs.args }} ${{ inputs.docker_args }} ${{ inputs.hub_args }} ${{ inputs.name }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' '` + SUFFIX=$(echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ inputs.build_args }} ${{ inputs.args }} ${{ inputs.docker_args }} ${{ inputs.hub_args }} ${{ inputs.name }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' ') echo "filename=coverage-plgd-hub-${SUFFIX}.json" >> $GITHUB_OUTPUT echo "artifact=plgd-hub-${SUFFIX}-coverage" >> $GITHUB_OUTPUT diff --git a/.github/workflows/sonar-cloud-analysis.yml b/.github/workflows/sonar-cloud-analysis.yml index 30811d9e05..d0607d783f 100644 --- a/.github/workflows/sonar-cloud-analysis.yml +++ b/.github/workflows/sonar-cloud-analysis.yml @@ -25,8 +25,8 @@ jobs: fail-fast: false matrix: include: - # cloud (ipv4+tcp) on, collection create on, push on, rfotm on - - build_args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON" + # cloud (ipv4+tcp) on, collection create on, push on, rfotm on, device provisioning on + - build_args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON" # security off, ipv4 on, collection create on, push on, max num concurrent requests=1 - build_args: "-DOC_SECURITY_ENABLED=OFF -DOC_IPV4_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_DEVICE_MAX_NUM_CONCURRENT_REQUESTS=1" # ipv6 dns on, oscore off, rep realloc on, json encoder on, introspection IDD off @@ -87,6 +87,11 @@ jobs: coverage: true hub_args: ${{ matrix.hub_args }} + plgd-dps-tests: + uses: ./.github/workflows/plgd-dps-test-with-cfg.yml + with: + coverage: true + sonar-cloud-scan: # don't run for forks if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) @@ -94,7 +99,7 @@ jobs: runs-on: ubuntu-22.04 env: BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed - needs: [unit-tests, plgd-device-tests, plgd-hub-tests] + needs: [unit-tests, plgd-device-tests, plgd-hub-tests, plgd-dps-tests] steps: - name: Checkout uses: actions/checkout@v4 @@ -110,7 +115,7 @@ jobs: mkdir build && cd build # sonar-scanner currently cannot handle multi configuration configuration (ie. compilation of the same file with different defines), # so we enable as many features as possible so we get max. amount of code analysis - cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=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 -DOC_JSON_ENCODER_ENABLED=ON -DBUILD_TESTING=ON .. + cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=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 -DOC_JSON_ENCODER_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON -DBUILD_TESTING=ON .. cd .. # for files defined in multiple cmake targets, sonar-scanner seems to take the configuration from the first compilation of the file, # so we force client-server target to be compiled first so we get analysis of code with both OC_CLIENT and OC_SERVER enabled @@ -138,4 +143,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | - sonar-scanner --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + sonar-scanner --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 3fa6d0f15f..f62c5e6818 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -16,6 +16,13 @@ on: jobs: clang-tidy-linux: + strategy: + fail-fast: false + matrix: + include: + - build_args: -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 +47,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/.github/workflows/unit-test-with-cfg.yml b/.github/workflows/unit-test-with-cfg.yml index 0f2abbc42e..886d82a887 100644 --- a/.github/workflows/unit-test-with-cfg.yml +++ b/.github/workflows/unit-test-with-cfg.yml @@ -123,7 +123,7 @@ jobs: if: ${{ inputs.coverage }} id: coverage run: | - SUFFIX=`echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ steps.cmake_flags.outputs.compiler }} ${{ inputs.build_args }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' '` + SUFFIX=$(echo "-DCMAKE_BUILD_TYPE=${{ inputs.build_type }} ${{ steps.cmake_flags.outputs.compiler }} ${{ inputs.build_args }} -DBUILD_TESTING=ON" | sha1sum | cut -f 1 -d ' ') echo "filename=coverage-unix-${SUFFIX}.json" >> $GITHUB_OUTPUT echo "artifact=unit-test-${SUFFIX}-coverage" >> $GITHUB_OUTPUT diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f490b7e3b..cc74744d81 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,11 @@ 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(PLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED OFF CACHE BOOL "Enable plgd's device provisioning feature's test properties.") + 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 +172,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,31 +236,13 @@ 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() - -set(OC_LOG_MAXIMUM_LEVEL ${OC_LOG_MAXIMUM_LOG_LEVEL_INT} CACHE INTERNAL "Maximum supported log level in compile time as integer.") +oc_set_maximum_log_level("${OC_LOG_MAXIMUM_LOG_LEVEL}" OC_LOG_MAXIMUM_LOG_LEVEL_INT) # clang-tidy triggers bugprone-macro-parentheses if the value is not in () -list(APPEND PRIVATE_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LEVEL})") -list(APPEND TEST_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LEVEL})") +list(APPEND PRIVATE_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LOG_LEVEL_INT})") +list(APPEND TEST_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LOG_LEVEL_INT})") if(BUILD_MBEDTLS) - list(APPEND MBEDTLS_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LEVEL})") + list(APPEND MBEDTLS_COMPILE_DEFINITIONS "OC_LOG_MAXIMUM_LEVEL=(${OC_LOG_MAXIMUM_LOG_LEVEL_INT})") endif() if(OC_PUSH_ENABLED) @@ -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,42 @@ 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() + list(APPEND PUBLIC_COMPILE_DEFINITIONS "PLGD_DEV_DEVICE_PROVISIONING") + + set(PLGD_DPS_MAXIMUM_LOG_LEVEL "DISABLED" CACHE STRING "Maximum supported Plgd DPS log level in compile time.") + set(PLGD_DPS_MAXIMUM_LOG_LEVEL_INT) + oc_set_maximum_log_level("${PLGD_DPS_MAXIMUM_LOG_LEVEL}" PLGD_DPS_MAXIMUM_LOG_LEVEL_INT) + list(APPEND PRIVATE_COMPILE_DEFINITIONS "PLGD_DPS_LOG_MAXIMUM_LEVEL=(${PLGD_DPS_MAXIMUM_LOG_LEVEL_INT})") + list(APPEND TEST_COMPILE_DEFINITIONS "PLGD_DPS_LOG_MAXIMUM_LEVEL=(${PLGD_DPS_MAXIMUM_LOG_LEVEL_INT})") +endif() + +if(PLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED) + list(APPEND PRIVATE_COMPILE_DEFINITIONS "PLGD_DPS_RESOURCE_TEST_PROPERTIES") + if(BUILD_TESTING) + list(APPEND TEST_COMPILE_DEFINITIONS "PLGD_DPS_RESOURCE_TEST_PROPERTIES") + endif() +endif() + if(BUILD_TESTING) list(APPEND PRIVATE_COMPILE_DEFINITIONS "OC_TEST") if(BUILD_MBEDTLS) @@ -712,7 +681,13 @@ 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) + file(GLOB PLGD_DPS_SRC + ${PROJECT_SOURCE_DIR}/api/plgd/device-provisioning-client/*.c + ) +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}) @@ -1134,6 +1109,17 @@ if(BUILD_TESTING AND ((WIN32 AND NOT MSVC) OR (UNIX AND HAVE_OC_TIME_H))) oc_package_add_test(TARGET pythonlibtest SOURCES ${PYTHONLIBTEST_SRC}) endif() + if(PLGD_DEV_TIME_ENABLED OR PLGD_DEV_DEVICE_PROVISIONING_ENABLED) + file(GLOB PLGDTEST_SRC api/plgd/unittest/*.cpp) + oc_package_add_test( + TARGET plgdtest + SOURCES ${COMMONTEST_SRC} ${PLGDTEST_SRC} + ENVIRONMENT "ASAN_OPTIONS=${OC_ASAN_OPTIONS}" + "TSAN_OPTIONS=${OC_TSAN_OPTIONS}" + "UBSAN_OPTIONS=${OC_UBSAN_OPTIONS}" + ) + endif() + if(UNIX AND NOT OC_TSAN_ENABLED) # install https://github.com/wolfcw/libfaketime for this test suit to run find_library(FAKETIME_LIBRARY @@ -1290,14 +1276,6 @@ if (OC_INTROSPECTION_ENABLED) ) endif() -if (PLGD_DEV_TIME_ENABLED) - install(DIRECTORY include/plgd - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iotivity-lite COMPONENT dev - FILES_MATCHING - PATTERN "*.h" - ) -endif() - if(OC_ETAG_ENABLED) install(FILES include/oc_etag.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iotivity-lite COMPONENT dev @@ -1334,6 +1312,18 @@ install(FILES ${HEADERS_UTIL_PT} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iotivity-lite/util/pt COMPONENT dev ) +if (PLGD_DEV_TIME_ENABLED) + install(FILES include/plgd/plgd_time.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iotivity-lite/plgd COMPONENT dev + ) +endif() + +if(PLGD_DEV_DEVICE_PROVISIONING_ENABLED) + install(FILES include/plgd/plgd_dps.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iotivity-lite/plgd COMPONENT dev + ) +endif() + # ####### Code formatting ######## if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # Add clang-format target diff --git a/api/oc_endpoint.c b/api/oc_endpoint.c index c2fe2ff271..b42ad6dc68 100644 --- a/api/oc_endpoint.c +++ b/api/oc_endpoint.c @@ -828,7 +828,7 @@ oc_endpoint_log(const char *prefix, const oc_endpoint_t *endpoint) oc_string64_t endpoint_str; oc_endpoint_to_string64(endpoint, &endpoint_str); int64_t session_id = oc_endpoint_session_id(endpoint); - OC_ERR("%sendpoint(addr=%s, session_id=%" PRId64 ")", prefix, + OC_DBG("%sendpoint(addr=%s, session_id=%" PRId64 ")", prefix, oc_string(endpoint_str), session_id); #else /* !OC_DBG_IS_ENABLED */ (void)prefix; diff --git a/api/oc_log.c b/api/oc_log.c index 6679525bff..5317cf5057 100644 --- a/api/oc_log.c +++ b/api/oc_log.c @@ -96,6 +96,10 @@ oc_log_component_name(oc_log_component_t component) #endif /* OC_CLOUD */ case OC_LOG_COMPONENT_COAP: return "coap"; +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + case OC_LOG_COMPONENT_DEVICE_PROVISIONING: + return "dps"; +#endif } return ""; } 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..6d2db25a2b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps.c @@ -0,0 +1,489 @@ +/**************************************************************************** + * + * 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_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_verify_certificate_internal.h" + +#include "api/oc_tcp_internal.h" +#include "oc_certs.h" +#include "oc_core_res.h" +#include "oc_network_monitor.h" + +#include +#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)) { + 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..7a12bbbe2e --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_context.c @@ -0,0 +1,297 @@ +/**************************************************************************** + * + * 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 "api/cloud/oc_cloud_schedule_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(const 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..35c2307753 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_context_internal.h @@ -0,0 +1,154 @@ +/**************************************************************************** + * + * 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_dhcp_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 +{ + 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(const 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..6d3c079212 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_dhcp.c @@ -0,0 +1,353 @@ +/**************************************************************************** + * + * 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_context_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..51e405049b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h @@ -0,0 +1,92 @@ +/**************************************************************************** + * + * 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 "util/oc_compiler.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +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; + +/** + * @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..0996e15dba --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h @@ -0,0 +1,116 @@ +/**************************************************************************** + * + * 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 "plgd_dps_log_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..cca168cdcd --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_endpoints.c @@ -0,0 +1,171 @@ +/**************************************************************************** + * + * 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.h" +#include "util/oc_endpoint_address_internal.h" +#include "util/oc_memb.h" +#include "util/oc_secure_string_internal.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) +{ + size_t len = oc_strnlen(endpoint, OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH); + assert(len < OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH); + dps_set_endpoint(ctx, endpoint, len, /* 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..2e18be0247 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_log.c @@ -0,0 +1,65 @@ +/**************************************************************************** + * + * 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, +}; + +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) +{ + 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); +} 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..2f85ca952e --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_log_internal.h @@ -0,0 +1,105 @@ +/**************************************************************************** + * + * 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_LOG_INTERNAL_H +#define PLGD_DPS_LOG_INTERNAL_H + +#include "plgd/plgd_dps.h" +#include "port/oc_log_internal.h" +#include "util/oc_compiler.h" + +#include "string.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#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, ...) \ + do { \ + if (plgd_dps_log_get_level() >= (log_level)) { \ + plgd_dps_print_log_fn_t _dps_logger_fn = plgd_dps_get_log_fn(); \ + if (_dps_logger_fn != NULL) { \ + _dps_logger_fn((log_level), __FILENAME__, __LINE__, __func__, \ + __VA_ARGS__); \ + break; \ + } \ + OC_LOG_WITH_COMPONENT(log_level, OC_LOG_COMPONENT_DEVICE_PROVISIONING, \ + __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(...) DPS_LOG(OC_LOG_LEVEL_TRACE, __VA_ARGS__) +#else /* !DPS_TRACE_IS_ENABLED */ +#define DPS_TRACE(...) +#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(...) DPS_LOG(OC_LOG_LEVEL_DEBUG, __VA_ARGS__) +#else /* !DPS_DBG_IS_ENABLED */ +#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(...) DPS_LOG(OC_LOG_LEVEL_INFO, __VA_ARGS__) +#else /* !DPS_INFO_IS_ENABLED */ +#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(...) DPS_LOG(OC_LOG_LEVEL_NOTICE, __VA_ARGS__) +#else /* !DPS_NOTE_IS_ENABLED */ +#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(...) DPS_LOG(OC_LOG_LEVEL_WARNING, __VA_ARGS__) +#else /* !DPS_WRN_IS_ENABLED */ +#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(...) DPS_LOG(OC_LOG_LEVEL_ERROR, __VA_ARGS__) +#else /* !DPS_ERR_IS_ENABLED */ +#define DPS_ERR(...) +#endif /* DPS_ERR_IS_ENABLED */ + +#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..06c9afebfa --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_manager.c @@ -0,0 +1,349 @@ +/**************************************************************************** + * + * 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 "security/oc_cred_util_internal.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_mfg_certificate_iterate(const oc_sec_cred_t *cred, void *data) +{ + if (cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_MFG_CERT && !dps_is_dps_cred(cred)) { + (*(const oc_sec_cred_t **)data) = cred; + return false; + } + return true; +} + +static bool +dps_has_mfg_certificate(size_t device) +{ + const oc_sec_creds_t *creds = oc_sec_get_creds(device); + const oc_sec_cred_t *mfg_cred = NULL; + oc_cred_iterate(creds->creds, dps_mfg_certificate_iterate, &mfg_cred); + if (mfg_cred != NULL) { + DPS_DBG("Manufacturer certificate(%d) found", mfg_cred->credid); + return true; + } + return false; +} + +static bool +dps_mfg_trusted_root_ca_iterate(const oc_sec_cred_t *cred, void *data) +{ + if (cred->credtype == OC_CREDTYPE_CERT && + cred->credusage == OC_CREDUSAGE_MFG_TRUSTCA && !dps_is_dps_cred(cred)) { + (*(const oc_sec_cred_t **)data) = cred; + return false; + } + return true; +} + +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); + oc_cred_iterate(creds->creds, dps_mfg_trusted_root_ca_iterate, &trusted_ca); + if (trusted_ca != NULL) { + DPS_DBG("manufacturer trusted root ca(%d) found", trusted_ca->credid); + return true; + } + return false; +} + +provision_and_cloud_observer_flags_t +dps_get_provision_and_cloud_observer_flags(plgd_dps_context_t *ctx) +{ + uint32_t provisionFlags = 0; + uint8_t cloudObserverStatus = 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; + } + cloudObserverStatus |= OC_CLOUD_REGISTERED; + if (dps_cloud_is_logged_in(ctx->device)) { + cloudObserverStatus |= OC_CLOUD_LOGGED_IN; + } + } + return (provision_and_cloud_observer_flags_t){ + .provision_flags = provisionFlags, + .cloud_observer_status = cloudObserverStatus, + }; +} + +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) { + provision_and_cloud_observer_flags_t pacf = + dps_get_provision_and_cloud_observer_flags(ctx); + new_status |= pacf.provision_flags; + ctx->cloud_observer.last_status |= pacf.cloud_observer_status; + } + 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..4253c0419e --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_manager_internal.h @@ -0,0 +1,86 @@ +/**************************************************************************** + * + * 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(); + +typedef struct +{ + uint32_t provision_flags; + uint8_t cloud_observer_status; +} provision_and_cloud_observer_flags_t; + +/// @brief Get provision flags and cloud observer status based on current state +provision_and_cloud_observer_flags_t dps_get_provision_and_cloud_observer_flags( + plgd_dps_context_t *ctx) 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..c06b3bd6c7 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_pki.c @@ -0,0 +1,413 @@ +/**************************************************************************** + * + * 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]; + if (oc_sec_csr_generate(ctx->device, oc_sec_certs_md_signature_algorithm(), + csr_data, sizeof(csr_data)) != 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) +{ + 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); +} + +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..37acd7e53f --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_pki_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 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(); + +/// @brief Try replacing current (expiring) DPS certificates with newer +/// certificates retrieved from the DPS service. +bool dps_pki_try_renew_certificates(plgd_dps_context_t *ctx) 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..cf8754a578 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision.c @@ -0,0 +1,654 @@ +/**************************************************************************** + * + * 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"); +#ifdef OC_SECURITY + if (!oc_device_is_in_dos_state(ctx->device, + OC_PSTAT_DOS_ID_FLAG(OC_DOS_RFNOP))) { + DPS_ERR("device is not in RFNOP state"); + return false; + } +#endif /* OC_SECURITY */ + + 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"); +#ifdef OC_SECURITY + if (!oc_device_is_in_dos_state(ctx->device, + OC_PSTAT_DOS_ID_FLAG(OC_DOS_RFNOP))) { + DPS_ERR("device is not in RFNOP state"); + return false; + } +#endif /* OC_SECURITY */ + 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..6e5025e9f5 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_cloud.c @@ -0,0 +1,409 @@ +/**************************************************************************** + * + * 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); +#ifdef OC_SECURITY + if (!oc_device_is_in_dos_state(ctx->device, + OC_PSTAT_DOS_ID_FLAG(OC_DOS_RFNOP))) { + DPS_ERR("device is not in RFNOP state"); + return false; + } +#endif /* OC_SECURITY */ + + 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..176c73104c --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_owner.c @@ -0,0 +1,211 @@ +/**************************************************************************** + * + * 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 + +int +dps_handle_get_owner_response(oc_client_response_t *data) +{ + const char *owner_str = NULL; + const 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"); +#ifdef OC_SECURITY + if (!oc_device_is_in_dos_state(ctx->device, + OC_PSTAT_DOS_ID_FLAG(OC_DOS_RFNOP))) { + DPS_ERR("device is not in RFNOP state"); + return false; + } +#endif /* OC_SECURITY */ + + 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..afd58aba08 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h @@ -0,0 +1,62 @@ +/**************************************************************************** + * + * 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(); + +/** Handler of get owner response */ +int dps_handle_get_owner_response(oc_client_response_t *data) 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..37abd25e05 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_resource.c @@ -0,0 +1,588 @@ +/**************************************************************************** + * + * 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); + +oc_string_view_t +dps_status_to_str(uint32_t status) +{ + if ((status & PLGD_DPS_FAILURE) != 0) { + return OC_STRING_VIEW(kPlgdDpsStatusFailure); + } + if ((status & PLGD_DPS_TRANSIENT_FAILURE) != 0) { + return OC_STRING_VIEW(kPlgdDpsStatusTransientFailure); + } + if (status == 0) { + return OC_STRING_VIEW(kPlgdDpsStatusUninitialized); + } + if (status == PLGD_DPS_INITIALIZED) { + return OC_STRING_VIEW(kPlgdDpsStatusInitialized); + } + if (status == (PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME)) { + return OC_STRING_VIEW(kPlgdDpsStatusGetTime); + } + const uint32_t has_time = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME; + if (status == has_time) { + return OC_STRING_VIEW(kPlgdDpsStatusHasTime); + } + if (status == (has_time | PLGD_DPS_GET_OWNER)) { + return OC_STRING_VIEW(kPlgdDpsStatusGetOwner); + } + const uint32_t has_owner = has_time | PLGD_DPS_HAS_OWNER; + if (status == has_owner) { + return OC_STRING_VIEW(kPlgdDpsStatusHasOwner); + } + if (status == (has_owner | PLGD_DPS_GET_CLOUD)) { + return OC_STRING_VIEW(kPlgdDpsStatusGetCloud); + } + const uint32_t has_cloud = has_owner | PLGD_DPS_HAS_CLOUD; + if (status == has_cloud) { + return OC_STRING_VIEW(kPlgdDpsStatusHasCloud); + } + if (status == (has_cloud | PLGD_DPS_GET_CREDENTIALS)) { + return OC_STRING_VIEW(kPlgdDpsStatusGetCredentials); + } + const uint32_t has_creds = has_cloud | PLGD_DPS_HAS_CREDENTIALS; + if (status == has_creds) { + return OC_STRING_VIEW(kPlgdDpsStatusHasCredentials); + } + if (status == (has_creds | PLGD_DPS_GET_ACLS)) { + return OC_STRING_VIEW(kPlgdDpsStatusGetAcls); + } + const uint32_t has_acls = has_creds | PLGD_DPS_HAS_ACLS; + if (status == has_acls) { + return OC_STRING_VIEW(kPlgdDpsStatusHasAcls); + } + const uint32_t cloud_started = has_acls | PLGD_DPS_CLOUD_STARTED; + if (status == cloud_started) { + return OC_STRING_VIEW(kPlgdDpsStatusProvisioned); + } + if (status == (cloud_started | PLGD_DPS_RENEW_CREDENTIALS)) { + return OC_STRING_VIEW(kPlgdDpsStatusRenewCredentials); + } + return OC_STRING_VIEW_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 < OC_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) +{ + oc_string_view_t status = dps_status_to_str(ctx->status); + dps_resource_data_t data = { + .last_error = ctx->last_error, + .provision_status = status.data, + .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], + OC_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 < OC_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..71f701a44e --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_resource_internal.h @@ -0,0 +1,90 @@ +/**************************************************************************** + * + * 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 "api/oc_helpers_internal.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 +oc_string_view_t 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..a016231a37 --- /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_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_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..5f58abd4b1 --- /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 "mbedtls/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..b31d43f100 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_store.c @@ -0,0 +1,370 @@ +/**************************************************************************** + * + * 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" + +// 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_STORE_ENDPOINTS_URI, + OC_CHAR_ARRAY_LEN(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_STORE_ENDPOINTS_NAME, + OC_CHAR_ARRAY_LEN(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_STORE_ENDPOINTS, + OC_CHAR_ARRAY_LEN(DPS_STORE_ENDPOINTS))) { + dsd.endpoints = store_rep->value.object_array; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, DPS_STORE_ENDPOINT, + OC_CHAR_ARRAY_LEN(DPS_STORE_ENDPOINT))) { + dsd.endpoint = &store_rep->value.string; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, DPS_STORE_ENDPOINT_NAME, + OC_CHAR_ARRAY_LEN(DPS_STORE_ENDPOINT_NAME))) { + dsd.endpoint_name = &store_rep->value.string; + continue; + } + if (dps_is_property(store_rep, OC_REP_STRING, DPS_STORE_OWNER, + OC_CHAR_ARRAY_LEN(DPS_STORE_OWNER))) { + dsd.owner = &store_rep->value.string; + continue; + } + if (dps_is_property( + store_rep, OC_REP_BOOL, DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET, + OC_CHAR_ARRAY_LEN(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, + size_t key_len, const char *value, + size_t value_len) +{ + g_err |= oc_rep_encode_text_string(object_map, key, key_len); + if (value != NULL) { + g_err |= oc_rep_encode_text_string(object_map, value, value_len); + } 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, OC_STRING_VIEW(DPS_STORE_ENDPOINT), + OC_STRING_VIEW_NULL, + OC_STRING_VIEW(DPS_STORE_ENDPOINT_NAME), + oc_endpoint_address_view(selected)); + } + g_err |= oc_endpoint_addresses_encode( + object_map, &store->endpoints, OC_STRING_VIEW(DPS_STORE_ENDPOINTS), true); + dps_store_rep_set_text_string( + object_map, DPS_STORE_OWNER, OC_CHAR_ARRAY_LEN(DPS_STORE_OWNER), + oc_string(store->owner), oc_string_len(store->owner)); + dps_store_rep_set_bool( + object_map, DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET, + OC_CHAR_ARRAY_LEN(DPS_STORE_HAS_BEEN_PROVISIONED_SINCE_RESET), + 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..e11f72961b --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_time.c @@ -0,0 +1,230 @@ +/**************************************************************************** + * + * 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"); +#ifdef OC_SECURITY + if (!oc_device_is_in_dos_state(ctx->device, + OC_PSTAT_DOS_ID_FLAG(OC_DOS_RFNOP))) { + DPS_ERR("device is not in RFNOP state"); + return false; + } +#endif /* OC_SECURITY */ + + 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..a98cc19114 --- /dev/null +++ b/api/plgd/device-provisioning-client/plgd_dps_verify_certificate.c @@ -0,0 +1,219 @@ +/**************************************************************************** + * + * 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; + } + + 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; + } + + 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; + } + + // 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/unittest/plgd_dps.cpp b/api/plgd/unittest/plgd_dps.cpp new file mode 100644 index 0000000000..f288cadd2f --- /dev/null +++ b/api/plgd/unittest/plgd_dps.cpp @@ -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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "plgd_dps_test.h" + +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_security_internal.h" +#include "oc_api.h" +#include "oc_core_res.h" +#include "oc_uuid.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +TEST(DPSApiTest, SetSkipVerify) +{ + auto ctx = dps::make_unique_context(kDeviceID); + plgd_dps_set_skip_verify(ctx.get(), true); + + EXPECT_TRUE(plgd_dps_get_skip_verify(ctx.get())); +} + +TEST(DPSApiTest, StatusToLogString) +{ + EXPECT_NE(0, dps_status_to_logstr(0, nullptr, 0)); + std::array tooSmall{}; + EXPECT_NE(0, dps_status_to_logstr(0, &tooSmall[0], tooSmall.size())); + + std::vector buffer; + buffer.resize(1024); + EXPECT_EQ(0, dps_status_to_logstr(0, buffer.data(), buffer.capacity())); + EXPECT_STREQ(kPlgdDpsStatusUninitialized, buffer.data()); + + EXPECT_NE(0, dps_status_to_logstr(PLGD_DPS_PROVISIONED_ALL_FLAGS, + &tooSmall[0], tooSmall.size())); + + EXPECT_EQ(0, dps_status_to_logstr(PLGD_DPS_PROVISIONED_ALL_FLAGS, + buffer.data(), buffer.capacity())); + std::unordered_set flags{ + kPlgdDpsStatusInitialized, kPlgdDpsStatusGetTime, + kPlgdDpsStatusHasTime, kPlgdDpsStatusGetOwner, + kPlgdDpsStatusHasOwner, kPlgdDpsStatusGetCredentials, + kPlgdDpsStatusHasCredentials, kPlgdDpsStatusGetAcls, + kPlgdDpsStatusHasAcls, kPlgdDpsStatusGetCloud, + kPlgdDpsStatusHasCloud, kPlgdDpsStatusProvisioned, + kPlgdDpsStatusRenewCredentials, kPlgdDpsStatusTransientFailure, + kPlgdDpsStatusFailure, + }; + + std::stringstream ss{ buffer.data() }; + std::string s; + while (std::getline(ss, s, '|')) { + EXPECT_EQ(1, flags.erase(s)); + } + if (!flags.empty()) { + std::cout << "missing flags: "; + for (const auto &f : flags) { + std::cout << f << " "; + } + std::cout << std::endl; + } + EXPECT_TRUE(flags.empty()); +} + +class TestDPSWithDevice : public testing::Test { +private: + static int AppInit() + { + if (oc_init_platform("Samsung", nullptr, nullptr) != 0) { + return -1; + } + if (oc_add_device("/oic/d", "oic.d.light", "Lamp", "ocf.1.0.0", + "ocf.res.1.0.0", nullptr, nullptr) != 0) { + return -1; + } + return 0; + } + + static void SignalEventLoop() + { + // no-op for tests + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + ASSERT_EQ(0, oc_main_init(&handler)); + ASSERT_EQ(kDeviceID, oc_core_get_num_devices() - 1); + ASSERT_EQ(0, plgd_dps_init()); + } + void TearDown() override + { + plgd_dps_shutdown(); + oc_main_shutdown(); + } +}; + +TEST_F(TestDPSWithDevice, GetContext) +{ + EXPECT_NE(nullptr, plgd_dps_get_context(kDeviceID)); + + size_t invalidDeviceID = 42; + EXPECT_EQ(nullptr, plgd_dps_get_context(invalidDeviceID)); +} + +TEST_F(TestDPSWithDevice, SetSelfOwned) +{ + auto ctx = dps::make_unique_context(kDeviceID); + EXPECT_FALSE(dps_is_self_owned(ctx.get())); + + EXPECT_TRUE(dps_set_self_owned(ctx.get())); + EXPECT_TRUE(dps_is_self_owned(ctx.get())); + EXPECT_FALSE(dps_has_owner(ctx.get())); +} + +TEST_F(TestDPSWithDevice, SetOwned) +{ + auto ctx = dps::make_unique_context(kDeviceID); + EXPECT_FALSE(dps_has_owner(ctx.get())); + + oc_uuid_t owner; + oc_gen_uuid(&owner); + EXPECT_TRUE(dps_set_owner(ctx.get(), &owner)); + EXPECT_FALSE(dps_is_self_owned(ctx.get())); + EXPECT_TRUE(dps_has_owner(ctx.get())); +} + +TEST_F(TestDPSWithDevice, SetDpsResource) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + auto hasDpsResource = [](size_t device) { + return oc_ri_get_app_resource_by_uri(PLGD_DPS_URI, sizeof(PLGD_DPS_URI) - 1, + device) != nullptr; + }; + EXPECT_FALSE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), false); + EXPECT_EQ(nullptr, ctx->conf); + EXPECT_FALSE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), true); + EXPECT_NE(nullptr, ctx->conf); + EXPECT_TRUE(hasDpsResource(kDeviceID)); + + plgd_dps_set_configuration_resource(ctx.get(), false); + EXPECT_EQ(nullptr, ctx->conf); + EXPECT_FALSE(hasDpsResource(kDeviceID)); +} + +TEST_F(TestDPSWithDevice, SetIdentityChain) +{ + // invalid deviceID + EXPECT_FALSE(dps_try_set_identity_chain(42)); +} + +TEST_F(TestDPSWithDevice, CloudAPI) +{ + EXPECT_FALSE(dps_cloud_is_started(kDeviceID)); + EXPECT_FALSE(dps_cloud_is_registered(kDeviceID)); + EXPECT_FALSE(dps_cloud_is_logged_in(kDeviceID)); + + size_t invalidDeviceID = 42; + EXPECT_FALSE(dps_cloud_is_started(invalidDeviceID)); + EXPECT_FALSE(dps_cloud_is_registered(invalidDeviceID)); + EXPECT_FALSE(dps_cloud_is_logged_in(invalidDeviceID)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_apis.cpp b/api/plgd/unittest/plgd_dps_apis.cpp new file mode 100644 index 0000000000..6c2ab72b06 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_apis.cpp @@ -0,0 +1,226 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_helpers_internal.h" +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_apis_internal.h" +#include "plgd/plgd_dps.h" +#include "plgd_dps_test.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include + +static constexpr size_t kDeviceID = 0; + +class DPSApisTest : public testing::Test { +public: + static void SetUpTestCase() { oc_runtime_init(); } + + static void TearDownTestCase() { oc_runtime_shutdown(); } +}; + +TEST_F(DPSApisTest, IsEqualStringLen) +{ + EXPECT_TRUE(dps_is_equal_string_len({}, nullptr, 0)); + EXPECT_TRUE(dps_is_equal_string_len(OC_STRING_LOCAL(""), "", 0)); + EXPECT_TRUE(dps_is_equal_string_len(OC_STRING_LOCAL("test"), "test", 4)); + + EXPECT_FALSE(dps_is_equal_string_len(OC_STRING_LOCAL(""), nullptr, 0)); + EXPECT_FALSE(dps_is_equal_string_len({}, "", 0)); + EXPECT_FALSE(dps_is_equal_string_len(OC_STRING_LOCAL("test"), "test1", 5)); + EXPECT_FALSE(dps_is_equal_string_len(OC_STRING_LOCAL("testA"), "testB", 5)); +} + +TEST_F(DPSApisTest, IsEqualString) +{ + EXPECT_TRUE(dps_is_equal_string({}, {})); + EXPECT_TRUE(dps_is_equal_string(OC_STRING_LOCAL(""), OC_STRING_LOCAL(""))); + EXPECT_TRUE( + dps_is_equal_string(OC_STRING_LOCAL("test"), OC_STRING_LOCAL("test"))); + + EXPECT_FALSE(dps_is_equal_string(OC_STRING_LOCAL(""), {})); + EXPECT_FALSE(dps_is_equal_string({}, OC_STRING_LOCAL(""))); + EXPECT_FALSE( + dps_is_equal_string(OC_STRING_LOCAL("test"), OC_STRING_LOCAL("test1"))); + EXPECT_FALSE( + dps_is_equal_string(OC_STRING_LOCAL("testA"), OC_STRING_LOCAL("testB"))); +} + +TEST_F(DPSApisTest, IsTimeoutError) +{ + EXPECT_TRUE(dps_is_timeout_error_code(OC_REQUEST_TIMEOUT)); + EXPECT_TRUE(dps_is_timeout_error_code(OC_TRANSACTION_TIMEOUT)); + + std::vector nonTimeouts = { + OC_STATUS_OK, + OC_STATUS_CREATED, + OC_STATUS_CHANGED, + OC_STATUS_DELETED, + OC_STATUS_BAD_REQUEST, + OC_STATUS_UNAUTHORIZED, + OC_STATUS_FORBIDDEN, + OC_STATUS_NOT_FOUND, + OC_STATUS_METHOD_NOT_ALLOWED, + OC_STATUS_NOT_ACCEPTABLE, + OC_STATUS_REQUEST_ENTITY_TOO_LARGE, + OC_STATUS_UNSUPPORTED_MEDIA_TYPE, + OC_STATUS_INTERNAL_SERVER_ERROR, + OC_STATUS_NOT_IMPLEMENTED, + OC_STATUS_BAD_GATEWAY, + OC_STATUS_SERVICE_UNAVAILABLE, + OC_STATUS_GATEWAY_TIMEOUT, + OC_STATUS_PROXYING_NOT_SUPPORTED, + OC_IGNORE, + OC_PING_TIMEOUT, // should be returned only by oc_send_ping, which is not + // used in DPS + OC_CONNECTION_CLOSED, + OC_CANCELLED, + }; + for (auto status : nonTimeouts) { + EXPECT_FALSE(dps_is_timeout_error_code(status)); + } +} + +TEST_F(DPSApisTest, IsConnectionError) +{ + EXPECT_TRUE(dps_is_connection_error_code(OC_STATUS_SERVICE_UNAVAILABLE)); + EXPECT_TRUE(dps_is_connection_error_code(OC_STATUS_GATEWAY_TIMEOUT)); + + std::vector nonConnectionErrors = { + OC_STATUS_OK, + OC_STATUS_CREATED, + OC_STATUS_CHANGED, + OC_STATUS_DELETED, + OC_STATUS_BAD_REQUEST, + OC_STATUS_UNAUTHORIZED, + OC_STATUS_FORBIDDEN, + OC_STATUS_NOT_FOUND, + OC_STATUS_METHOD_NOT_ALLOWED, + OC_STATUS_NOT_ACCEPTABLE, + OC_STATUS_REQUEST_ENTITY_TOO_LARGE, + OC_STATUS_UNSUPPORTED_MEDIA_TYPE, + OC_STATUS_INTERNAL_SERVER_ERROR, + OC_STATUS_NOT_IMPLEMENTED, + OC_STATUS_BAD_GATEWAY, + OC_STATUS_PROXYING_NOT_SUPPORTED, + OC_IGNORE, + OC_PING_TIMEOUT, // should be returned only by oc_send_ping, which is not + // used in DPS + OC_REQUEST_TIMEOUT, + OC_CONNECTION_CLOSED, + OC_TRANSACTION_TIMEOUT, + OC_CANCELLED, + }; + for (auto status : nonConnectionErrors) { + EXPECT_FALSE(dps_is_connection_error_code(status)); + } +} + +TEST_F(DPSApisTest, RedirectResponse) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + std::string ep1_uri = "/uri/1"; + std::string ep1_name = "name1"; + auto *ep1 = + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), ep1_uri.length(), + ep1_name.c_str(), ep1_name.length()); + ASSERT_NE(nullptr, ep1); + + oc::RepPool pool{}; + auto handleRedirect = [&ctx, &pool](std::string_view redirecturi) { + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, redirecturi, redirecturi.data()); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + ASSERT_TRUE(dps_handle_redirect_response(ctx.get(), rep.get())); + }; + + // ctx contains an endpoint, but with a different URI than the redirect + std::string redirect{ "coap://mock.plgd.dev" }; + handleRedirect(redirect); + const oc_string_t *selected_uri = + oc_endpoint_addresses_selected_uri(&ctx->store.endpoints); + ASSERT_NE(nullptr, selected_uri); + EXPECT_STREQ(redirect.c_str(), oc_string(*selected_uri)); + const oc_string_t *selected_name = + oc_endpoint_addresses_selected_name(&ctx->store.endpoints); + ASSERT_NE(nullptr, selected_name); + // the redirected URI should take name from the previously selected endpoint + EXPECT_STREQ(ep1_name.c_str(), oc_string(*selected_name)); + EXPECT_EQ(1, oc_endpoint_addresses_size(&ctx->store.endpoints)); + + // redirect to the selected endpoint + handleRedirect(redirect); + EXPECT_EQ(1, oc_endpoint_addresses_size(&ctx->store.endpoints)); + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, + oc_string_view(redirect.c_str(), redirect.length()))); + + // ctx contains multiple endpoints, including the redirected one, which should + // be selected + ep1 = + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), ep1_uri.length(), + ep1_name.c_str(), ep1_name.length()); + ASSERT_NE(nullptr, ep1); + oc_endpoint_addresses_select(&ctx->store.endpoints, ep1); + ASSERT_EQ(2, oc_endpoint_addresses_size(&ctx->store.endpoints)); + ASSERT_TRUE(oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view(ep1_uri.c_str(), ep1_uri.length()))); + handleRedirect(redirect); + EXPECT_EQ(1, oc_endpoint_addresses_size(&ctx->store.endpoints)); + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, + oc_string_view(redirect.c_str(), redirect.length()))); +} + +TEST_F(DPSApisTest, RedirectResponse_Fail) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + oc::RepPool pool{}; + // missing redirecturi -> processing skipped + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, plgd, "dev"); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + EXPECT_TRUE(dps_handle_redirect_response(ctx.get(), rep.get())); + + rep.reset(); + pool.Clear(); + // invalid redirecturi + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, redirecturi, ""); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + EXPECT_FALSE(dps_handle_redirect_response(ctx.get(), rep.get())); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ \ No newline at end of file diff --git a/api/plgd/unittest/plgd_dps_cloud.cpp b/api/plgd/unittest/plgd_dps_cloud.cpp new file mode 100644 index 0000000000..3e4cb73bd4 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_cloud.cpp @@ -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. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_manager_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_cloud_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/cloud/oc_cloud_context_internal.h" +#include "oc_cloud.h" +#include "oc_uuid.h" +#include "port/oc_random.h" +#include "tests/gtest/Device.h" + +#include "gtest/gtest.h" + +#include + +static constexpr size_t kDeviceID = 0; + +class DPSCloudTest : public testing::Test { +public: + static void SetUpTestCase() { oc_random_init(); } + static void TearDownTestCase() { oc_random_destroy(); } +}; + +TEST_F(DPSCloudTest, CloudObserverOnServerChange) +{ + oc_uuid_t nilUUID{}; + plgd_cloud_status_observer_t obs{}; + obs.last_endpoint_uuid = nilUUID; + EXPECT_FALSE(dps_cloud_observer_copy_endpoint_uuid(&obs, nullptr)); + EXPECT_TRUE(oc_uuid_is_empty(obs.last_endpoint_uuid)); + EXPECT_FALSE(dps_cloud_observer_copy_endpoint_uuid(&obs, &nilUUID)); + EXPECT_TRUE(oc_uuid_is_empty(obs.last_endpoint_uuid)); + + oc_uuid_t uuid; + oc_gen_uuid(&uuid); + EXPECT_TRUE(dps_cloud_observer_copy_endpoint_uuid(&obs, &uuid)); + EXPECT_TRUE(oc_uuid_is_equal(obs.last_endpoint_uuid, uuid)); +} + +class DPSCloudWithServerTest : public testing::Test { +public: + static void SetUpTestCase() + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + plgd_dps_init(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } +}; + +TEST_F(DPSCloudWithServerTest, CloudObserverOnServerChange) +{ + plgd_dps_context_t ctx{}; + dps_cloud_observer_init(&ctx.cloud_observer); + + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + + // no remaining changes + ctx.device = cloud_ctx->device; + ASSERT_TRUE(dps_cloud_observer_load(&ctx.cloud_observer, cloud_ctx)); + ASSERT_EQ(0, ctx.cloud_observer.remaining_endpoint_changes); + dps_cloud_observer_on_server_change(&ctx); + + std::string_view uri{ "/uri/1" }; + oc_uuid_t uuid{ + { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }, + }; + auto *ep1 = + oc_cloud_add_server_address(cloud_ctx, uri.data(), uri.length(), uuid); + ASSERT_NE(nullptr, ep1); + ASSERT_TRUE(dps_cloud_observer_load(&ctx.cloud_observer, cloud_ctx)); + ASSERT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + // invalid device + ctx.device = 42; + dps_cloud_observer_on_server_change(&ctx); + EXPECT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + + // no selected endpoint + ctx.device = cloud_ctx->device; + oc_endpoint_addresses_clear(&cloud_ctx->store.ci_servers); + dps_cloud_observer_on_server_change(&ctx); + EXPECT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + oc_cloud_context_clear(cloud_ctx, false); + + // rotate back to initial endpoint + ep1 = oc_cloud_add_server_address(cloud_ctx, uri.data(), uri.length(), uuid); + ASSERT_NE(nullptr, ep1); + ASSERT_TRUE(dps_cloud_observer_load(&ctx.cloud_observer, cloud_ctx)); + ASSERT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + const auto *selected = oc_cloud_selected_server_address(cloud_ctx); + ASSERT_TRUE(oc_string_is_equal(&ctx.cloud_observer.initial_endpoint_uri, + oc_endpoint_address_uri(selected))); + dps_cloud_observer_on_server_change(&ctx); + EXPECT_EQ(0, ctx.cloud_observer.remaining_endpoint_changes); + + // select the second endpoint with different uuid + ASSERT_TRUE(dps_cloud_observer_load(&ctx.cloud_observer, cloud_ctx)); + ASSERT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + ASSERT_TRUE(oc_cloud_select_server_address(cloud_ctx, ep1)); + selected = oc_cloud_selected_server_address(cloud_ctx); + ASSERT_FALSE(oc_string_is_equal(&ctx.cloud_observer.initial_endpoint_uri, + oc_endpoint_address_uri(selected))); + ASSERT_FALSE(oc_uuid_is_equal(ctx.cloud_observer.last_endpoint_uuid, + *oc_endpoint_address_uuid(selected))); + dps_cloud_observer_on_server_change(&ctx); + EXPECT_EQ(0, ctx.cloud_observer.remaining_endpoint_changes); + oc_cloud_context_clear(cloud_ctx, false); + + // tselect the second endpoint with the same (empty) uuid + ep1 = oc_cloud_add_server_address(cloud_ctx, uri.data(), uri.length(), {}); + ASSERT_TRUE(dps_cloud_observer_load(&ctx.cloud_observer, cloud_ctx)); + ASSERT_EQ(1, ctx.cloud_observer.remaining_endpoint_changes); + ASSERT_TRUE(oc_cloud_select_server_address(cloud_ctx, ep1)); + selected = oc_cloud_selected_server_address(cloud_ctx); + ASSERT_FALSE(oc_string_is_equal(&ctx.cloud_observer.initial_endpoint_uri, + oc_endpoint_address_uri(selected))); + ASSERT_TRUE(oc_uuid_is_equal(ctx.cloud_observer.last_endpoint_uuid, + *oc_endpoint_address_uuid(selected))); + dps_cloud_observer_on_server_change(&ctx); + EXPECT_EQ(0, ctx.cloud_observer.remaining_endpoint_changes); + + dps_manager_stop(&ctx); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_context.cpp b/api/plgd/unittest/plgd_dps_context.cpp new file mode 100644 index 0000000000..4369ecacb3 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_context.cpp @@ -0,0 +1,289 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +class TestDPSWithContext : public testing::Test { +protected: + void SetUp() override + { + oc_runtime_init(); + memset(ctx_.get(), 0, sizeof(plgd_dps_context_t)); + dps_context_init(ctx_.get(), kDeviceID); + } + void TearDown() override + { + dps_context_deinit(ctx_.get()); + oc_runtime_shutdown(); + } + +public: + std::unique_ptr ctx_{ + std::make_unique() + }; +}; + +TEST_F(TestDPSWithContext, HasForcedReprovision) +{ + EXPECT_FALSE(plgd_dps_has_forced_reprovision(ctx_.get())); + + plgd_dps_force_reprovision(ctx_.get()); + EXPECT_TRUE(plgd_dps_has_forced_reprovision(ctx_.get())); +} + +TEST_F(TestDPSWithContext, HasBeenProvisionedSinceReset) +{ + EXPECT_FALSE(plgd_dps_has_been_provisioned_since_reset(ctx_.get())); + + dps_set_has_been_provisioned_since_reset(ctx_.get(), /*dump*/ false); + EXPECT_TRUE(plgd_dps_has_been_provisioned_since_reset(ctx_.get())); +} + +TEST_F(TestDPSWithContext, GetProvisionStatus) +{ + EXPECT_EQ(0, plgd_dps_get_provision_status(ctx_.get())); + + dps_set_ps_and_last_error(ctx_.get(), PLGD_DPS_INITIALIZED, 0, PLGD_DPS_OK); + EXPECT_EQ(PLGD_DPS_INITIALIZED, plgd_dps_get_provision_status(ctx_.get())); +} + +TEST_F(TestDPSWithContext, GetLastError) +{ + EXPECT_EQ(PLGD_DPS_OK, plgd_dps_get_last_error(ctx_.get())); + + dps_set_last_error(ctx_.get(), PLGD_DPS_ERROR_CONNECT); + EXPECT_EQ(PLGD_DPS_ERROR_CONNECT, plgd_dps_get_last_error(ctx_.get())); +} + +TEST_F(TestDPSWithContext, SetCloudObserver) +{ + plgd_cloud_status_observer_configuration_t cfg = + plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(30, cfg.max_count); + EXPECT_EQ(1, cfg.interval_s); + + EXPECT_FALSE(plgd_dps_set_cloud_observer_configuration( + ctx_.get(), /*max_retry_count*/ 0, /*retry_interval_s*/ 0)); + cfg = plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(30, cfg.max_count); + EXPECT_EQ(1, cfg.interval_s); + + EXPECT_TRUE(plgd_dps_set_cloud_observer_configuration( + ctx_.get(), /*max_retry_count*/ 13, /*retry_interval_s*/ 37)); + cfg = plgd_dps_get_cloud_observer_configuration(ctx_.get()); + EXPECT_EQ(13, cfg.max_count); + EXPECT_EQ(37, cfg.interval_s); +} + +TEST_F(TestDPSWithContext, SetExpiringLimit) +{ + const uint16_t expiresIn = 1337; + plgd_dps_pki_set_expiring_limit(ctx_.get(), expiresIn); + EXPECT_EQ(expiresIn, plgd_dps_pki_get_expiring_limit(ctx_.get())); +} + +TEST_F(TestDPSWithContext, SetEndpoint) +{ + std::array buffer{ '\0' }; + EXPECT_EQ(0, plgd_dps_get_endpoint(ctx_.get(), buffer.data(), buffer.size())); + + const char endpoint[] = "coaps+tcp://plgd.cloud:25684"; +#ifndef OC_DYNAMIC_ALLOCATION + ASSERT_GE(OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH, + std::string(endpoint).length()); +#endif /* OC_DYNAMIC_ALLOCATION */ + + plgd_dps_set_endpoint(ctx_.get(), endpoint); + std::array too_small{ '\0' }; + EXPECT_GT( + 0, plgd_dps_get_endpoint(ctx_.get(), too_small.data(), too_small.size())); + + size_t s = plgd_dps_get_endpoint(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(sizeof(endpoint), s); + EXPECT_STREQ(endpoint, buffer.data()); +} + +TEST_F(TestDPSWithContext, SetRetry) +{ + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), nullptr, 0)); + std::vector empty{}; + EXPECT_FALSE( + plgd_dps_set_retry_configuration(ctx_.get(), empty.data(), empty.size())); + std::array too_large; + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), too_large.data(), + too_large.size())); + std::array empty_values = { 0 }; + EXPECT_FALSE(plgd_dps_set_retry_configuration(ctx_.get(), empty_values.data(), + empty_values.size())); + + std::array arr{ 1, 2, 3, 4, 5 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), arr.data(), arr.size())); + std::array too_small; + size_t cfg_size = plgs_dps_get_retry_configuration( + ctx_.get(), too_small.data(), too_small.size()); + EXPECT_EQ(-1, cfg_size); + + std::array buffer{ 0 }; + auto EXPECT_ARR_EQ = [](const uint8_t *arr1, const uint8_t *arr2, + size_t size) { + for (size_t i = 0; i < size; ++i) { + EXPECT_EQ(arr1[i], arr2[i]) + << "Arrays differ at index " << i << " (" << (int)arr1[i] << " vs " + << (int)arr2[i] << ")"; + } + }; + std::array single{ 1 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), single.data(), single.size())); + cfg_size = + plgs_dps_get_retry_configuration(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(single.size(), cfg_size); + EXPECT_ARR_EQ(single.data(), buffer.data(), cfg_size); + + std::array full{ 1, 2, 3, 4, 5, 6, 7, 8 }; + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx_.get(), full.data(), full.size())); + cfg_size = + plgs_dps_get_retry_configuration(ctx_.get(), buffer.data(), buffer.size()); + EXPECT_EQ(full.size(), cfg_size); + EXPECT_ARR_EQ(full.data(), buffer.data(), cfg_size); +} + +TEST_F(TestDPSWithContext, SetScheduleAction) +{ + bool cbk_called = false; + auto cbk = [](plgd_dps_context_t *, plgd_dps_status_t, uint8_t, uint64_t *, + uint16_t *, void *user_data) { + auto called = static_cast(user_data); + *called = true; + return false; + }; + plgd_dps_set_schedule_action(ctx_.get(), cbk, &cbk_called); + dps_retry_increment(ctx_.get(), PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx_.get()->retry.count); + EXPECT_TRUE(cbk_called); + plgd_dps_set_schedule_action(ctx_.get(), nullptr, nullptr); +} + +TEST_F(TestDPSWithContext, SetCertificateFingerprint) +{ + // no fingerprint set + std::array fingerprint_ok{ '\0' }; + mbedtls_md_type_t md_type = MBEDTLS_MD_NONE; + EXPECT_EQ(0, plgd_dps_get_certificate_fingerprint(ctx_.get(), &md_type, + &fingerprint_ok[0], + fingerprint_ok.size())); + + EXPECT_EQ(true, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_NONE, nullptr, 0)); + + std::array fingerprint = { + (uint8_t)0xB8, (uint8_t)0xF5, (uint8_t)0xBA, (uint8_t)0x0D, (uint8_t)0x9F, + (uint8_t)0x6D, (uint8_t)0x4D, (uint8_t)0xEF, (uint8_t)0x3F, (uint8_t)0x82, + (uint8_t)0x28, (uint8_t)0xD2, (uint8_t)0x5F, (uint8_t)0x53, (uint8_t)0xD9, + (uint8_t)0x42, (uint8_t)0x8E, (uint8_t)0xAF, (uint8_t)0x0B, (uint8_t)0x36, + (uint8_t)0x71, (uint8_t)0xED, (uint8_t)0x80, (uint8_t)0xD7, (uint8_t)0x6C, + (uint8_t)0xE7, (uint8_t)0xDB, (uint8_t)0xAF, (uint8_t)0x44, (uint8_t)0xEC, + (uint8_t)0x28, (uint8_t)0xD3 + }; + + EXPECT_EQ(false, + plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_MD5, &fingerprint[0], fingerprint.size())); + EXPECT_EQ(false, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_SHA256, &fingerprint[0], + fingerprint.size() + 1)); + EXPECT_EQ(true, plgd_dps_set_certificate_fingerprint( + ctx_.get(), MBEDTLS_MD_SHA256, &fingerprint[0], + fingerprint.size())); + + std::array fingerprint_too_small{ '\0' }; + EXPECT_EQ(-1, plgd_dps_get_certificate_fingerprint( + ctx_.get(), &md_type, &fingerprint_too_small[0], + fingerprint_too_small.size())); + + EXPECT_EQ(fingerprint_ok.size(), + plgd_dps_get_certificate_fingerprint( + ctx_.get(), &md_type, &fingerprint_ok[0], fingerprint_ok.size())); + EXPECT_EQ(MBEDTLS_MD_SHA256, md_type); + + EXPECT_EQ(fingerprint, fingerprint_ok); +} + +TEST_F(TestDPSWithContext, SetValuesFromVendorEncapsulatedOptions) +{ + std::string data = "c8:1c:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:70:6c:67:64:2e:" + "63:6c:6f:75:64:3a:32:36:36:38:34:c9:20:" + "a1:e1:c3:4c:3e:3:17:8d:e4:77:79:f9:92:28:7d:fe:b4:b7:70:" + "2f:80:ee:d9:15:dd:ec:d6:" + "54:e4:c6:4f:e2:ca:6:53:48:41:32:35:36"; + ssize_t ret = + plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), nullptr, 0); + ASSERT_EQ(72, ret); + std::array buf; + ret = plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), &buf[0], ret); + ASSERT_EQ(72, ret); + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_ERROR, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret - 1)); + // last byte is '6' from SHA256 + buf[ret - 1] = 'A'; + // invalid SHA25A + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_ERROR, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + buf[ret - 1] = '6'; + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_NOT_CHANGED, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf[0], ret)); + std::string data1 = "c8:1c:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:70:6c:67:64:" + "2e:63:6c:6f:75:64:3a:32:36:36:38:34:c9:20:" + "a1:e1:c3:4c:3e:3:17:8d:e4:77:79:f9:92:28:7d:fe:b4:b7:70:" + "2f:81:ee:d9:15:dd:ec:d6:" + "54:e4:c6:4f:e2:ca:6:53:48:41:32:35:36"; + std::array buf1; + ret = plgd_dps_hex_string_to_bytes(data1.c_str(), data1.length(), &buf1[0], + buf1.size()); + ASSERT_EQ(72, ret); + ctx_.get()->status = PLGD_DPS_PROVISIONED_MASK; + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_UPDATED, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + ctx_.get(), &buf1[0], ret)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ \ No newline at end of file diff --git a/api/plgd/unittest/plgd_dps_dhcp.cpp b/api/plgd/unittest/plgd_dps_dhcp.cpp new file mode 100644 index 0000000000..9de6f49e63 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_dhcp.cpp @@ -0,0 +1,106 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_dhcp_internal.h" + +#include "gtest/gtest.h" + +#include + +TEST(DPSDhcpTest, DpsOptions) +{ + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + plgd_dps_dhcp_init(&ctx.dhcp); + + EXPECT_EQ( + 200, plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_endpoint(&ctx)); + EXPECT_EQ( + 201, + plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_certificate_fingerprint( + &ctx)); + + plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_endpoint(&ctx, 202); + EXPECT_EQ( + 202, plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_endpoint(&ctx)); + + plgd_dps_dhcp_set_vendor_encapsulated_option_code_dps_certificate_fingerprint( + &ctx, 203); + EXPECT_EQ( + 203, + plgd_dps_dhcp_get_vendor_encapsulated_option_code_dps_certificate_fingerprint( + &ctx)); +} + +TEST(DPSDhcpTest, ConvertISCDhcpOptionsToBytes) +{ + std::string data = "c8:2:a:b"; + ssize_t ret = + plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), nullptr, 0); + ASSERT_EQ(4, ret); + + std::array buf; + ret = plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), &buf[0], ret); + std::array expected_data = { (uint8_t)0xc8, (uint8_t)0x2, + (uint8_t)0xA, (uint8_t)0xB }; + EXPECT_EQ(expected_data.size(), ret); + EXPECT_EQ(0, memcmp(&expected_data[0], &buf[0], ret)); +} + +TEST(DPSDhcpTest, ParseISCDhcpOptions) +{ + std::string data = "c8:20:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:74:72:79:2e:70:" + "6c:67:64:2e:63:6c:6f:75:64:3a:31:35:36:38:34:c9:20:b8:f5:" + "ba:d:9f:6d:4d:ef:3f:82:28:d2:5f:53:d9:42:8e:af:b:36:71:" + "ed:80:d7:6c:e7:db:af:44:ec:28:d3"; + ssize_t ret = + plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), nullptr, 0); + ASSERT_EQ(68, ret); + std::array buf; + ret = plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), &buf[0], ret); + ASSERT_EQ(68, ret); + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + plgd_dps_dhcp_init(&ctx.dhcp); + dhcp_parse_data_t parse_data; + memset(&parse_data, 0, sizeof(parse_data)); + parse_data.dhcp = &ctx.dhcp; + EXPECT_TRUE( + dps_dhcp_parse_vendor_encapsulated_options(&parse_data, &buf[0], ret)); + EXPECT_EQ(0, memcmp("coaps+tcp://try.plgd.cloud:15684", parse_data.endpoint, + parse_data.endpoint_size)); + std::array fingerprint = { + (uint8_t)0xB8, (uint8_t)0xF5, (uint8_t)0xBA, (uint8_t)0x0D, (uint8_t)0x9F, + (uint8_t)0x6D, (uint8_t)0x4D, (uint8_t)0xEF, (uint8_t)0x3F, (uint8_t)0x82, + (uint8_t)0x28, (uint8_t)0xD2, (uint8_t)0x5F, (uint8_t)0x53, (uint8_t)0xD9, + (uint8_t)0x42, (uint8_t)0x8E, (uint8_t)0xAF, (uint8_t)0x0B, (uint8_t)0x36, + (uint8_t)0x71, (uint8_t)0xED, (uint8_t)0x80, (uint8_t)0xD7, (uint8_t)0x6C, + (uint8_t)0xE7, (uint8_t)0xDB, (uint8_t)0xAF, (uint8_t)0x44, (uint8_t)0xEC, + (uint8_t)0x28, (uint8_t)0xD3 + }; + EXPECT_EQ(fingerprint.size(), parse_data.certificate_fingerprint_size); + EXPECT_EQ(0, memcmp(&fingerprint[0], parse_data.certificate_fingerprint, + parse_data.certificate_fingerprint_size)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_endpoint.cpp b/api/plgd/unittest/plgd_dps_endpoint.cpp new file mode 100644 index 0000000000..df56847337 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_endpoint.cpp @@ -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. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h" +#include "oc_api.h" +#include "oc_core_res.h" +#include "oc_helpers.h" +#include "oc_uuid.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +TEST(DPSApiTest, EndpointIsEmpty) +{ + oc_endpoint_t endpoint; + memset(&endpoint, 0, sizeof(oc_endpoint_t)); + EXPECT_TRUE(dps_endpoint_is_empty(&endpoint)); + + std::string ep_str{ "coap://224.0.1.187:5683" }; + oc_string_t ep_ocstr; + oc_new_string(&ep_ocstr, ep_str.c_str(), ep_str.length()); + oc_string_to_endpoint(&ep_ocstr, &endpoint, nullptr); + oc_free_string(&ep_ocstr); + EXPECT_FALSE(dps_endpoint_is_empty(&endpoint)); +} + +TEST(DPSApiTest, EndpointToString) +{ + EXPECT_FALSE(dps_endpoint_log_string(nullptr, nullptr, 0)); + + std::vector out; + out.resize(5); + EXPECT_FALSE(dps_endpoint_log_string(nullptr, out.data(), out.size())); + + std::string ep_str = "coap://224.0.1.187:5683"; + oc_string_t ep_ocstr; + oc_new_string(&ep_ocstr, ep_str.c_str(), ep_str.length()); + oc_endpoint_t endpoint; + EXPECT_EQ(0, oc_string_to_endpoint(&ep_ocstr, &endpoint, nullptr)); + oc_free_string(&ep_ocstr); + + out.resize(ep_str.size() - 1); + EXPECT_FALSE(dps_endpoint_log_string(&endpoint, out.data(), out.size())); + +#if DPS_DBG_IS_ENABLED + std::string exp_str = "endpoint(addr=" + ep_str + ", session_id=-1)"; +#else // !DPS_DBG_IS_ENABLED + std::string exp_str = "endpoint(" + ep_str + ")"; +#endif // DPS_DBG_IS_ENABLED + out.resize(exp_str.size() + 1); + EXPECT_TRUE(dps_endpoint_log_string(&endpoint, out.data(), out.size())); + EXPECT_STREQ(exp_str.c_str(), out.data()); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_endpoints.cpp b/api/plgd/unittest/plgd_dps_endpoints.cpp new file mode 100644 index 0000000000..eb99ddad5c --- /dev/null +++ b/api/plgd/unittest/plgd_dps_endpoints.cpp @@ -0,0 +1,171 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h" +#include "plgd_dps_test.h" +#include "util/oc_endpoint_address.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +class DPSEndpointsTest : public testing::Test { +public: + static void SetUpTestCase() { oc_runtime_init(); } + + static void TearDownTestCase() { oc_runtime_shutdown(); } +}; + +TEST_F(DPSEndpointsTest, SetEndpoint) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + std::string endpoint = "coaps+tcp://plgd.cloud:25684"; + ASSERT_EQ(DPS_ENDPOINT_CHANGED, dps_set_endpoint(ctx.get(), endpoint.c_str(), + endpoint.length(), true)); + + std::array buffer{ '\0' }; + ASSERT_LT(0, plgd_dps_get_endpoint(ctx.get(), buffer.data(), buffer.size())); + EXPECT_STREQ(endpoint.c_str(), buffer.data()); + + // not changed + ASSERT_EQ( + DPS_ENDPOINT_NOT_CHANGED, + dps_set_endpoint(ctx.get(), endpoint.c_str(), endpoint.length(), true)); + ASSERT_LT(0, plgd_dps_get_endpoint(ctx.get(), buffer.data(), buffer.size())); + EXPECT_STREQ(endpoint.c_str(), buffer.data()); + + // invalid - string longer than OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH + auto invalid = std::string(OC_ENDPOINT_MAX_ENDPOINT_URI_LENGTH + 1, 'a'); + ASSERT_EQ( + -1, dps_set_endpoint(ctx.get(), invalid.c_str(), invalid.length(), true)); +} + +TEST_F(DPSEndpointsTest, IsEmpty) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + EXPECT_TRUE(plgd_dps_endpoint_is_empty(ctx.get())); + EXPECT_EQ(nullptr, plgd_dps_selected_endpoint_address(ctx.get())); +} + +TEST_F(DPSEndpointsTest, EndpointsAPI) +{ + auto ctx = dps::make_unique_context(kDeviceID); + // after init, no endpoint should be selected + EXPECT_TRUE(plgd_dps_endpoint_is_empty(ctx.get())); + EXPECT_EQ(nullptr, plgd_dps_selected_endpoint_address(ctx.get())); + + // add + std::string ep1_uri = "/uri/1"; + std::string ep1_name = "ep1"; + auto *ep1 = + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), ep1_uri.length(), + ep1_name.c_str(), ep1_name.length()); + std::string ep2_uri = "/uri2"; + auto *ep2 = plgd_dps_add_endpoint_address(ctx.get(), ep2_uri.c_str(), + ep2_uri.length(), nullptr, 0); + ASSERT_NE(nullptr, ep2); + std::string ep3_uri = "/uri3"; + std::string ep3_name = "ep3"; + auto *ep3 = + plgd_dps_add_endpoint_address(ctx.get(), ep3_uri.c_str(), ep3_uri.length(), + ep3_name.c_str(), ep3_name.length()); + + auto verify_selected_endpoint = [&ctx](oc_endpoint_address_t *ep, + const std::string &uri, + const std::string &name) { + auto *selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_EQ(ep, selected); + if (ep == nullptr) { + return; + } + auto *selected_uri = oc_endpoint_address_uri(selected); + ASSERT_NE(nullptr, selected_uri); + EXPECT_STREQ(uri.c_str(), oc_string(*selected_uri)); + auto *selected_name = oc_endpoint_address_name(selected); + ASSERT_NE(nullptr, selected_name); + if (name.empty()) { + EXPECT_EQ(nullptr, oc_string(*selected_name)); + } else { + EXPECT_STREQ(name.c_str(), oc_string(*selected_name)); + } + }; + + // first item added to empty list should be selected + verify_selected_endpoint(ep1, ep1_uri, ep1_name); + + // remove the first item + ASSERT_TRUE(plgd_dps_remove_endpoint_address(ctx.get(), ep1)); + + // next endpoint should be selected + verify_selected_endpoint(ep2, ep2_uri, {}); + + oc_endpoint_address_t notInList{}; + EXPECT_FALSE(plgd_dps_select_endpoint_address(ctx.get(), ¬InList)); + + std::set> uris{}; + // iterate + plgd_dps_iterate_server_addresses( + ctx.get(), + [](oc_endpoint_address_t *eaddr, void *data) { + auto uri = oc_endpoint_address_uri(eaddr); + static_cast *>(data)->insert( + std::string(oc_string(*uri), oc_string_len(*uri))); + return true; + }, + &uris); + + ASSERT_EQ(2, uris.size()); + EXPECT_NE(uris.end(), uris.find(ep3_uri)); + EXPECT_NE(uris.end(), uris.find(ep2_uri)); + EXPECT_EQ(uris.end(), uris.find(ep1_uri)); + + oc_endpoint_address_t *toSelect = nullptr; + // iterate to get the last endpoint + plgd_dps_iterate_server_addresses( + ctx.get(), + [](oc_endpoint_address_t *eaddr, void *data) { + *static_cast(data) = eaddr; + return true; + }, + &toSelect); + ASSERT_NE(nullptr, toSelect); + + ASSERT_TRUE(plgd_dps_select_endpoint_address(ctx.get(), ep3)); + verify_selected_endpoint(ep3, ep3_uri, ep3_name); + + std::string ep3_newname = "ep2"; + oc_endpoint_address_set_name(toSelect, ep3_newname.c_str(), + ep3_newname.length()); + verify_selected_endpoint(ep3, ep3_uri, ep3_newname); + + ASSERT_TRUE(plgd_dps_remove_endpoint_address(ctx.get(), toSelect)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_log.cpp b/api/plgd/unittest/plgd_dps_log.cpp new file mode 100644 index 0000000000..ec1e8dd8bb --- /dev/null +++ b/api/plgd/unittest/plgd_dps_log.cpp @@ -0,0 +1,118 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "plgd/plgd_dps.h" + +#include "gtest/gtest.h" + +#include + +class TestDPSLog : public testing::Test { +public: + void TearDown() override + { + plgd_dps_set_log_fn(nullptr); + plgd_dps_log_set_level(OC_LOG_LEVEL_INFO); + } +}; + +TEST_F(TestDPSLog, LogToStdout) +{ + DPS_ERR("error"); + DPS_WRN("warning"); + DPS_NOTE("notice"); + DPS_INFO("info"); + DPS_DBG("debug"); + DPS_TRACE("trace"); + + plgd_dps_log_set_level(OC_LOG_LEVEL_TRACE); + EXPECT_EQ(OC_LOG_LEVEL_TRACE, plgd_dps_log_get_level()); + DPS_DBG("debug"); + DPS_TRACE("trace"); +} + +static void printLog(oc_log_level_t log_level, const char *file, int line, + const char *func_name, const char *format, va_list args) + OC_PRINTF_FORMAT(5, 0); + +static void +printLog(oc_log_level_t log_level, const char *file, int line, + const char *func_name, const char *format, va_list args) +{ + printf("[%s:%d %s]<%s>: ", file, line, func_name, + oc_log_level_to_label(log_level)); + vprintf(format, args); + printf("\n"); + fflush(stdout); +} + +static void expectUpToNotice(oc_log_level_t log_level, + + const char *file, int line, const char *func_name, + const char *format, ...) OC_PRINTF_FORMAT(5, 6); +static void +expectUpToNotice(oc_log_level_t log_level, const char *file, int line, + const char *func_name, const char *format, ...) +{ + EXPECT_TRUE(log_level == OC_LOG_LEVEL_ERROR || + log_level == OC_LOG_LEVEL_WARNING || + log_level == OC_LOG_LEVEL_NOTICE); + va_list ap; + va_start(ap, format); + printLog(log_level, file, line, func_name, format, ap); + va_end(ap); +} + +TEST_F(TestDPSLog, LogToFunction) +{ + plgd_dps_log_set_level(OC_LOG_LEVEL_NOTICE); + plgd_dps_set_log_fn(expectUpToNotice); + + DPS_LOG(OC_LOG_LEVEL_ERROR, "error"); + DPS_LOG(OC_LOG_LEVEL_WARNING, "warning"); + DPS_LOG(OC_LOG_LEVEL_NOTICE, "notice"); + DPS_LOG(OC_LOG_LEVEL_INFO, "info"); + DPS_LOG(OC_LOG_LEVEL_DEBUG, "debug"); + DPS_LOG(OC_LOG_LEVEL_TRACE, "trace"); +} + +static void +expectNoLog(oc_log_level_t, const char *, int, const char *, const char *, ...) +{ + FAIL() << "unexpected log"; +} + +TEST_F(TestDPSLog, SkipLogByComponent) +{ + plgd_dps_log_set_level(OC_LOG_LEVEL_TRACE); + plgd_dps_set_log_fn(expectNoLog); + + DPS_ERR("error"); + DPS_WRN("warning"); + DPS_NOTE("notice"); + DPS_INFO("info"); + DPS_DBG("debug"); + DPS_TRACE("trace"); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_manager.cpp b/api/plgd/unittest/plgd_dps_manager.cpp new file mode 100644 index 0000000000..b6ec7fbbf8 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_manager.cpp @@ -0,0 +1,191 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/oc_helpers_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_manager_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_security_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_tag_internal.h" +#include "oc_acl.h" +#include "oc_cred.h" +#include "plgd_dps_test.h" +#include "plgd/plgd_time.h" +#include "tests/gtest/Device.h" + +#include "gtest/gtest.h" + +#include + +static constexpr size_t kDeviceID = 0; + +using namespace std::chrono_literals; + +class TestDPSManager : public testing::Test { +public: + static void SetUpTestCase() { ASSERT_TRUE(oc::TestDevice::StartServer()); } + + static void TearDownTestCase() { oc::TestDevice::StopServer(); } +}; + +TEST_F(TestDPSManager, ChangeEndpointOnRetry) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + // set retry loop -> single attempt + std::array arr{ 1 }; // 1s + EXPECT_TRUE( + plgd_dps_set_retry_configuration(ctx.get(), arr.data(), arr.size())); + + // set multiple DPS endpoints + std::string ep1_uri = "coap://127.0.0.1:12345"; + ASSERT_NE(nullptr, + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), + ep1_uri.length(), nullptr, 0)); + std::string ep2_uri = "coap+tcp://127.0.0.1:12345"; + ASSERT_NE(nullptr, + plgd_dps_add_endpoint_address(ctx.get(), ep2_uri.c_str(), + ep2_uri.length(), nullptr, 0)); + ASSERT_TRUE(oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view(ep1_uri.c_str(), ep1_uri.length()))); + + // after one retry loop finishes, the endpoint should be changed + // delay = [0s..0,5s] + timeout = 1s + random jitter ([0s..0,5s]) == around 2s + // should be enough + dps_provisioning_start(ctx.get()); + bool selected = false; + for (int i = 0; i < 16; ++i) { + oc::TestDevice::PoolEventsMsV1(200ms); + selected = oc_endpoint_addresses_is_selected( + &ctx->store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length())); + if (selected) { + break; + } + } + EXPECT_TRUE(selected); +} + +TEST_F(TestDPSManager, StartAlreadyStarted) +{ + oc::keypair_t rootKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + oc::keypair_t identKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + int mfg_credid = + dps::addIdentityCertificate(kDeviceID, identKey, rootKey, true); + ASSERT_LT(0, mfg_credid); + + plgd_dps_context_t ctx{}; + dps_context_init(&ctx, kDeviceID); + ctx.skip_verify = true; + dps_context_list_add(&ctx); + + std::string ep_uri = "coap://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + &ctx, ep_uri.c_str(), ep_uri.length(), nullptr, 0)); + + EXPECT_EQ(0, plgd_dps_manager_start(&ctx)); + EXPECT_EQ(0, plgd_dps_manager_start(&ctx)); + + plgd_dps_manager_stop(&ctx); + dps_context_list_remove(&ctx); + dps_context_deinit(&ctx); + ASSERT_TRUE(oc_sec_remove_cred_by_credid(mfg_credid, kDeviceID)); +} + +TEST_F(TestDPSManager, GetProvisionAndCloudObserverFlags) +{ + plgd_time_set_time(oc_clock_time()); + + plgd_dps_context_t ctx{}; + dps_context_init(&ctx, kDeviceID); + auto pof = dps_get_provision_and_cloud_observer_flags(&ctx); + uint32_t provision_flags = PLGD_DPS_HAS_TIME; + uint8_t cloud_observer_status = 0; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + oc_uuid_t owner; + oc_gen_uuid(&owner); + ASSERT_TRUE(dps_set_owner(&ctx, &owner)); + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + provision_flags |= PLGD_DPS_HAS_OWNER; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + auto *cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + std::string at{ "access_token" }; + oc_new_string(&cloud_ctx->store.access_token, at.c_str(), at.length()); + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + provision_flags |= PLGD_DPS_HAS_CLOUD; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + +#ifdef OC_DYNAMIC_ALLOCATION + oc::keypair_t rootKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + int root_credid = dps::addRootCertificate(kDeviceID, rootKey, false, true); + ASSERT_LT(0, root_credid); + oc::keypair_t identKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + int credid = + dps::addIdentityCertificate(kDeviceID, identKey, rootKey, false, true); + ASSERT_LT(0, credid); + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + provision_flags |= PLGD_DPS_HAS_CREDENTIALS; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + ASSERT_TRUE(oc_sec_acl_add_bootstrap_acl(kDeviceID)); + auto *ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + EXPECT_NE(nullptr, ace); + oc_set_string(&ace->tag, DPS_TAG, DPS_TAG_LEN); + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + provision_flags |= PLGD_DPS_HAS_ACLS; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + cloud_ctx->store.status = OC_CLOUD_REGISTERED; + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + cloud_observer_status |= OC_CLOUD_REGISTERED; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + cloud_ctx->cloud_manager = true; + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + provision_flags |= PLGD_DPS_CLOUD_STARTED; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + cloud_ctx->store.status |= OC_CLOUD_LOGGED_IN; + pof = dps_get_provision_and_cloud_observer_flags(&ctx); + cloud_observer_status |= OC_CLOUD_LOGGED_IN; + EXPECT_EQ(provision_flags, pof.provision_flags); + EXPECT_EQ(cloud_observer_status, pof.cloud_observer_status); + + ASSERT_TRUE(oc_sec_remove_cred_by_credid(credid, kDeviceID)); + ASSERT_TRUE(oc_sec_remove_cred_by_credid(root_credid, kDeviceID)); +#endif /* OC_DYNAMIC_ALLOCATION */ + + dps_context_deinit(&ctx); + plgd_time_set_time(0); + plgd_time_set_status(PLGD_TIME_STATUS_IN_SYNC); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_pki.cpp b/api/plgd/unittest/plgd_dps_pki.cpp new file mode 100644 index 0000000000..10ff9754a2 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_pki.cpp @@ -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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_runtime_internal.h" +#include "api/oc_server_api_internal.h" +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_pki_internal.h" +#include "oc_rep.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include +#include + +using namespace std::chrono_literals; + +static constexpr size_t kDeviceID = 0; + +class TestPKI : public testing::Test { +public: + void SetUp() override { oc_runtime_init(); } + void TearDown() override { oc_runtime_shutdown(); } +}; + +TEST_F(TestPKI, SendCSR_FailInvalidDeviceID) +{ + plgd_dps_context_t ctx{}; + ctx.device = 42; + + EXPECT_FALSE(dps_pki_send_csr(&ctx, [](oc_client_response_t *) { + // no-op + })); +} + +TEST_F(TestPKI, ReplaceCertificates_FailInvalidDeviceID) +{ + oc_rep_t emptyRep{}; + oc_endpoint_t emptyEp{}; + EXPECT_FALSE(dps_pki_replace_certificates(42, &emptyRep, &emptyEp)); +} + +TEST_F(TestPKI, CalculateRenewCertificatesInterval) +{ + dps_pki_configuration_t cfg{ + /*.expiring_limit =*/10, + }; + oc_clock_time_t valid_to = oc_clock_seconds_v1(); + EXPECT_EQ(0, dps_pki_calculate_renew_certificates_interval(cfg, valid_to)); + + // expiring within 1 minute + valid_to = oc_clock_seconds_v1() + 30; + EXPECT_EQ(std::chrono::duration_cast(10s).count(), + dps_pki_calculate_renew_certificates_interval(cfg, valid_to)); + + // expiring within 3 minutes + valid_to = oc_clock_seconds_v1() + 120; + EXPECT_EQ(std::chrono::duration_cast(1min).count(), + dps_pki_calculate_renew_certificates_interval(cfg, valid_to)); + + // expiring within 6 minutes + valid_to = oc_clock_seconds_v1() + 300; + EXPECT_EQ(std::chrono::duration_cast(2min).count(), + dps_pki_calculate_renew_certificates_interval(cfg, valid_to)); + + // longer than 6 minutes + valid_to = oc_clock_seconds_v1() + 600; + EXPECT_GT(std::chrono::duration_cast( + std::chrono::seconds(600)) + .count(), + dps_pki_calculate_renew_certificates_interval(cfg, valid_to)); +} + +class TestPKIWithDevice : public testing::Test { +public: + static void SetUpTestCase() { ASSERT_TRUE(oc::TestDevice::StartServer()); } + + static void TearDownTestCase() { oc::TestDevice::StopServer(); } + + void TearDown() override + { + oc::TestDevice::Reset(); + oc::TestDevice::CloseSessions(kDeviceID); + // wait for asynchronous closing of sessions to finish + oc::TestDevice::PoolEventsMsV1(10ms); + oc::TestDevice::ClearSystemTime(); + } +}; + +TEST_F(TestPKIWithDevice, ReplaceCertificates_FailInvalidRep) +{ + oc_endpoint_t emptyEp{}; + std::string uuid = "00000000-0000-0000-0000-000000000001"; + + oc::RepPool pool{}; + oc_rep_start_root_object(); + oc_rep_set_text_string_v1(root, rowneruuid, uuid.c_str(), uuid.length()); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + + auto rep = pool.ParsePayload(); + EXPECT_FALSE(dps_pki_replace_certificates(kDeviceID, rep.get(), &emptyEp)); +} + +TEST_F(TestPKIWithDevice, TryRenewCertificates) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.device = kDeviceID; + EXPECT_TRUE(dps_pki_try_renew_certificates(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + plgd_dps_manager_stop(&ctx); +} + +TEST_F(TestPKIWithDevice, RenewCertificates) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.device = kDeviceID; + ctx.status = PLGD_DPS_PROVISIONED_MASK | PLGD_DPS_CLOUD_STARTED; + + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + cloud_ctx->cloud_manager = true; + + oc_reset_delayed_callback(&ctx, dps_pki_renew_certificates_async, 0); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + plgd_dps_manager_stop(&ctx); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_provision.cpp b/api/plgd/unittest/plgd_dps_provision.cpp new file mode 100644 index 0000000000..38ee1645b5 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_provision.cpp @@ -0,0 +1,176 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h" +#include "oc_rep.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +struct cloudEndpoint +{ + std::string uri; + std::string id; +}; + +static struct cloudEndpoint +makeCloudEndpoint(const std::string &uri, const std::string &id) +{ + return cloudEndpoint{ uri, id }; +} + +static void +makeCloudConfigurationPayload(const std::string &at, const std::string &apn, + const std::string &cis, const std::string &sid, + const std::vector &endpoints = {}) +{ + oc_rep_start_root_object(); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + if (!at.empty()) { + oc_rep_set_text_string(root, at, at.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!apn.empty()) { + oc_rep_set_text_string(root, apn, apn.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!cis.empty()) { + oc_rep_set_text_string(root, cis, cis.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!sid.empty()) { + oc_rep_set_text_string(root, sid, sid.c_str()); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); + } + if (!endpoints.empty()) { + std::string epsKey = "x.org.iotivity.servers"; + g_err |= oc_rep_encode_text_string(oc_rep_object(root), epsKey.c_str(), + epsKey.length()); + oc_rep_begin_array(oc_rep_object(root), endpoints); + for (const auto &endpoint : endpoints) { + oc_rep_object_array_start_item(endpoints); + oc_rep_set_text_string(endpoints, uri, endpoint.uri.c_str()); + oc_rep_set_text_string(endpoints, id, endpoint.id.c_str()); + oc_rep_object_array_end_item(endpoints); + } + oc_rep_end_array(oc_rep_object(root), endpoints); + } + + oc_rep_end_root_object(); + EXPECT_EQ(CborNoError, oc_rep_get_cbor_errno()); +} + +TEST(DPSFillCloudTest, MissingAccessToken) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("", "auth_provider", "server", "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingAuthProvider) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "", "server", "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingServer) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "auth_provider", "", + "server_id"); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, MissingServerID) +{ + oc::RepPool pool{}; + + makeCloudConfigurationPayload("access_token", "auth_provider", "server", ""); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_FALSE(dps_register_cloud_fill_data(rep.get(), &cloud)); +} + +TEST(DPSFillCloudTest, FillSuccess) +{ + oc::RepPool pool{}; + + std::vector endpoints = { makeCloudEndpoint("uri/1", "id1"), + makeCloudEndpoint("uri/2", "id2") }; + makeCloudConfigurationPayload("access_token", "auth_provider", "server", + "server id", endpoints); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + cloud_conf_t cloud{}; + EXPECT_TRUE(dps_register_cloud_fill_data(rep.get(), &cloud)); + + EXPECT_STREQ("access_token", oc_string(*cloud.access_token)); + EXPECT_STREQ("auth_provider", oc_string(*cloud.auth_provider)); + EXPECT_STREQ("server", oc_string(*cloud.ci_server)); + EXPECT_STREQ("server id", oc_string(*cloud.sid)); + EXPECT_NE(nullptr, cloud.ci_servers); + size_t count = 0; + for (const oc_rep_t *server = cloud.ci_servers; server != nullptr; + server = server->next) { + std::string_view uriKey = "uri"; + const oc_rep_t *prop = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, uriKey.data(), uriKey.length()); + ASSERT_NE(nullptr, prop); + ASSERT_NE(nullptr, oc_string(prop->value.string)); + std::string uri = oc_string(prop->value.string); + + std::string_view idKey = "id"; + prop = oc_rep_get_by_type_and_key(server->value.object, OC_REP_STRING, + idKey.data(), idKey.length()); + ASSERT_NE(nullptr, prop); + ASSERT_NE(nullptr, oc_string(prop->value.string)); + std::string id = oc_string(prop->value.string); + + EXPECT_NE(endpoints.end(), std::find_if(endpoints.begin(), endpoints.end(), + [&](const cloudEndpoint &endpoint) { + return endpoint.uri == uri && + endpoint.id == id; + })); + ++count; + } + EXPECT_EQ(endpoints.size(), count); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_provision_cloud.cpp b/api/plgd/unittest/plgd_dps_provision_cloud.cpp new file mode 100644 index 0000000000..42f597dc34 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_provision_cloud.cpp @@ -0,0 +1,380 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/cloud/oc_cloud_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_manager_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_cloud_internal.h" +#include "oc_api.h" +#include "oc_cloud.h" +#include "oc_core_res.h" +#include "oc_helpers.h" +#include "oc_rep.h" +#include "oc_uuid.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/Endpoint.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +class DPSProvisionCloudWithServerTest : public testing::Test { +public: + static void SetUpTestCase() + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + plgd_dps_init(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFOTM; + oc::TestDevice::StopServer(); + } + + void SetUp() override + { + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFNOP; + } + + void TearDown() override + { + oc::TestDevice::Reset(); + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + oc_cloud_context_clear(cloud_ctx, false); + } + + static void clearCloudServers(size_t device) + { + auto cloud_ctx = oc_cloud_get_context(device); + ASSERT_NE(nullptr, cloud_ctx); + do { + const oc_endpoint_address_t *ea = + oc_cloud_selected_server_address(cloud_ctx); + if (ea == nullptr) { + break; + } + oc_cloud_remove_server_address(cloud_ctx, ea); + } while (true); + } + + static int encodeConfResourcePayload(const std::string &cis, + const std::string &sid, + const std::string &at, + const std::string &apn) + { + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, cis, cis.c_str()); + oc_rep_set_text_string(root, sid, sid.c_str()); + oc_rep_set_text_string(root, at, at.c_str()); + oc_rep_set_text_string(root, apn, apn.c_str()); + oc_rep_end_root_object(); + return oc_rep_get_cbor_errno(); + } +}; + +TEST_F(DPSProvisionCloudWithServerTest, HandleSetCloudResponseFail) +{ + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + // invalid payload + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, dps_handle_set_cloud_response(&data)); + + oc::RepPool pool{}; + + // invalid sid + ctx->device = kDeviceID; + ASSERT_EQ(CborNoError, + encodeConfResourcePayload("cis", "not-an-UUID", "at", "apn")); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + ASSERT_EQ(CborNoError, + encodeConfResourcePayload( + "cis", "00000000-0000-0000-0000-000000000001", "at", "apn")); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + // invalid device + ctx->device = 42; + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); + + ctx->device = kDeviceID; + // logged in and no cloud server set + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + clearCloudServers(kDeviceID); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, dps_handle_set_cloud_response(&data)); +} + +TEST_F(DPSProvisionCloudWithServerTest, HandleSetCloudResponse) +{ + oc::RepPool pool{}; + + std::string cis = "cis"; + std::string sid = "00000000-0000-0000-0000-000000000001"; + std::string at = "at"; + std::string apn = "apn"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + + // update signed in cloud with same data + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(ctx->device); + ASSERT_NE(nullptr, cloud_ctx); + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different cis + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + cis = "nextcis"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different apn + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + apn = "nextapn"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different sid, but not connected to a cloud + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + sid = "00000000-0000-0000-0000-000000000002"; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + rep.reset(); + pool.Clear(); + + // update signed in cloud with different sid and connected to a cloud + cloud_ctx->store.status = OC_CLOUD_REGISTERED | OC_CLOUD_LOGGED_IN; + std::string uid = "501"; + oc_set_string(&cloud_ctx->store.uid, uid.c_str(), uid.length()); + sid = "00000000-0000-0000-0000-000000000003"; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + memcpy(cloud_ctx->cloud_ep, &ep, sizeof(oc_endpoint_t)); + cloud_ctx->cloud_ep_state = OC_SESSION_CONNECTED; + ASSERT_EQ(CborNoError, encodeConfResourcePayload(cis, sid, at, apn)); + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + EXPECT_EQ(PLGD_DPS_OK, dps_handle_set_cloud_response(&data)); + EXPECT_EQ(OC_SESSION_DISCONNECTED, cloud_ctx->cloud_ep_state); +} + +TEST_F(DPSProvisionCloudWithServerTest, HasCloudConfiguration) +{ + // invalid device + EXPECT_FALSE(dps_has_cloud_configuration(42)); + + auto cloud_ctx = oc_cloud_get_context(kDeviceID); + ASSERT_NE(nullptr, cloud_ctx); + + // missing access token + oc_cloud_context_clear(cloud_ctx, false); + ASSERT_EQ(nullptr, oc_string(*oc_cloud_get_access_token(cloud_ctx))); + EXPECT_FALSE(dps_has_cloud_configuration(kDeviceID)); + + // no selected gateway + clearCloudServers(kDeviceID); + EXPECT_FALSE(dps_has_cloud_configuration(kDeviceID)); + + // ok + ASSERT_EQ(0, oc_cloud_provision_conf_resource(cloud_ctx, "coap://[ff02::158]", + "access_token", "", "")); + EXPECT_TRUE(dps_has_cloud_configuration(kDeviceID)); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudHandler) +{ + auto ctx = std::make_unique(); + oc_client_response_t data{}; + data.user_data = ctx.get(); + data.code = OC_STATUS_OK; + + // if PLGD_DPS_HAS_CLOUD is not set then PLGD_DPS_GET_CLOUD must be set + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_SET_CLOUD, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // returned error code + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_GET_CLOUD; + data.code = OC_STATUS_BAD_REQUEST; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // invalid payload + data.code = OC_STATUS_OK; + data.payload = nullptr; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx->last_error); + EXPECT_NE(0, ctx->status & PLGD_DPS_FAILURE); + + // ok + oc::RepPool pool{}; + ASSERT_EQ(CborNoError, + encodeConfResourcePayload( + "cis", "00000000-0000-0000-0000-000000000001", "at", "apn")); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + data.payload = rep.get(); + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_OK, ctx->last_error); + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_HAS_CLOUD, + ctx->status); + + // if PLGD_DPS_HAS_CLOUD flag is already set then nothing should be done + ctx->status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_HAS_OWNER | + PLGD_DPS_HAS_CLOUD; + data.payload = nullptr; + dps_set_cloud_handler(&data); + EXPECT_EQ(PLGD_DPS_OK, ctx->last_error); + + dps_manager_stop(ctx.get()); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodeSelectedGatewayFail) +{ + auto ctx = std::make_unique(); + ctx->device = 42; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + + oc::RepPool pool{ 1 }; + ctx->device = kDeviceID; + oc_rep_begin_root_object(); + EXPECT_FALSE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodeSelectedGateway) +{ + oc::RepPool pool{}; + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + + // with selected gateway + oc_rep_begin_root_object(); + EXPECT_TRUE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + oc_rep_end_root_object(); + auto rep = pool.ParsePayload(); + DPS_DBG("%s", oc::RepPool::GetJson(rep.get(), true).data()); + + rep.reset(); + pool.Clear(); + + // without selected gateway and selected gateway id + clearCloudServers(kDeviceID); + + oc_rep_begin_root_object(); + EXPECT_TRUE(dps_provisioning_set_cloud_encode_selected_gateway(ctx.get())); + oc_rep_end_root_object(); + rep = pool.ParsePayload(); + DPS_DBG("%s", oc::RepPool::GetJson(rep.get(), true).data()); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodePayloadFail) +{ + auto ctx = std::make_unique(); + ctx->device = 42; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_payload(ctx.get())); + + oc::RepPool pool{ 1 }; + ctx->device = kDeviceID; + EXPECT_FALSE(dps_provisioning_set_cloud_encode_payload(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudEncodePayload) +{ + oc::RepPool pool{}; + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + EXPECT_TRUE(dps_provisioning_set_cloud_encode_payload(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloudFail) +{ + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + ctx->endpoint = &ep; + + // must be in RFNOP state + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFOTM; + EXPECT_FALSE(dps_provisioning_set_cloud(ctx.get())); +} + +TEST_F(DPSProvisionCloudWithServerTest, SetCloud) +{ + auto ctx = std::make_unique(); + ctx->device = kDeviceID; + oc_endpoint_t ep = oc::endpoint::FromString("coap://[ff02::158]"); + ctx->endpoint = &ep; + + EXPECT_TRUE(dps_provisioning_set_cloud(ctx.get())); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_provision_owner.cpp b/api/plgd/unittest/plgd_dps_provision_owner.cpp new file mode 100644 index 0000000000..b872b3f057 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_provision_owner.cpp @@ -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. + * + ****************************************************************************/ + +#include "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_owner_internal.h" +#include "oc_api.h" +#include "oc_rep.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" + +#include "gtest/gtest.h" + +using namespace std::chrono_literals; + +static constexpr size_t kDeviceID = 0; + +class TestProvisionOwnerWithDevice : public testing::Test { +public: + static void SetUpTestCase() { ASSERT_TRUE(oc::TestDevice::StartServer()); } + + static void TearDownTestCase() { oc::TestDevice::StopServer(); } + + void SetUp() override + { + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + ASSERT_NE(nullptr, pstat); + pstat->s = OC_DOS_RFNOP; + } + + void TearDown() override + { + oc::TestDevice::Reset(); + oc::TestDevice::CloseSessions(kDeviceID); + // wait for asynchronous closing of sessions to finish + oc::TestDevice::PoolEventsMsV1(10ms); + oc::TestDevice::ClearSystemTime(); + } +}; + +TEST_F(TestProvisionOwnerWithDevice, GetOwner_FailInvalidDOSState) +{ + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + ASSERT_NE(nullptr, pstat); + pstat->s = OC_DOS_RFOTM; + + plgd_dps_context_t ctx{}; + ctx.device = kDeviceID; + EXPECT_FALSE(dps_get_owner(&ctx)); + + pstat->s = OC_DOS_RFNOP; +} + +TEST_F(TestProvisionOwnerWithDevice, GetOwner_OwnerAlreadySet) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_HAS_OWNER; + EXPECT_TRUE(dps_get_owner(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + oc_has_delayed_callback(nullptr, dps_provision_next_step_async, true); +} + +TEST_F(TestProvisionOwnerWithDevice, GetOwner_InvalidStatus) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_INITIALIZED; + EXPECT_TRUE(dps_get_owner(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_GET_OWNER | PLGD_DPS_FAILURE, + ctx.status); + EXPECT_EQ(PLGD_DPS_ERROR_GET_OWNER, ctx.last_error); +} + +TEST_F(TestProvisionOwnerWithDevice, GetOwner_InvalidResponse) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME; + EXPECT_TRUE(dps_get_owner(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_HAS_TIME | PLGD_DPS_GET_OWNER | + PLGD_DPS_FAILURE, + ctx.status); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx.last_error); +} + +TEST_F(TestProvisionOwnerWithDevice, HandleGetOwnerResponse_Fail) +{ + oc_client_response_t response{}; + // owner property not set + EXPECT_EQ(-1, dps_handle_get_owner_response(&response)); + + // unexpected property + std::string uuid = "00000000-0000-0000-0000-000000000001"; + + oc::RepPool pool{}; + oc_rep_start_root_object(); + oc_rep_set_text_string_v1(root, rowneruuid, uuid.c_str(), uuid.length()); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + + auto rep = pool.ParsePayload(); + response.payload = rep.get(); + EXPECT_EQ(-1, dps_handle_get_owner_response(&response)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_resource.cpp b/api/plgd/unittest/plgd_dps_resource.cpp new file mode 100644 index 0000000000..f8c940f0a7 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_resource.cpp @@ -0,0 +1,573 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#if defined(OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING) && \ + defined(OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM) + +#include "api/oc_rep_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_endpoints_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_resource_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" +#include "tests/gtest/Resource.h" +#include "util/oc_macros_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +static constexpr size_t kDeviceID = 0; + +using namespace std::chrono_literals; + +class DPSResourceTest : public testing::Test {}; + +struct DPSEndpointData +{ + std::string uri; + std::string name; +}; + +struct DPSData +{ + std::optional lastErrorCode; + std::optional provisionStatus; + DPSEndpointData endpoint; + std::vector endpoints; + bool forceReprovision; +}; + +static std::vector +parseDPSEndpoints(const oc_rep_t *servers) +{ + std::vector endpoints{}; + for (const oc_rep_t *server = servers; server != nullptr; + server = server->next) { + DPSEndpointData data{}; + const oc_rep_t *rep = oc_rep_get_by_type_and_key( + server->value.object, OC_REP_STRING, "uri", OC_CHAR_ARRAY_LEN("uri")); + if (rep == nullptr) { + return {}; + } + data.uri = oc_string(rep->value.string); + + rep = oc_rep_get_by_type_and_key(server->value.object, OC_REP_STRING, + "name", OC_CHAR_ARRAY_LEN("name")); + if (rep == nullptr) { + return {}; + } + data.name = oc_string(rep->value.string); + endpoints.push_back(data); + } + return endpoints; +} + +static bool +parseDPSDataProperty(const oc_rep_t *rep, DPSData &dpsData) +{ + if (rep->type == OC_REP_BOOL) { + if (std::string(oc_string(rep->name)) == "forceReprovision") { + dpsData.forceReprovision = rep->value.boolean; + return true; + } + return false; + } + + if (rep->type == OC_REP_INT) { + if (std::string(oc_string(rep->name)) == "lastErrorCode") { + dpsData.lastErrorCode = static_cast(rep->value.integer); + return true; + } + return false; + } + + if (rep->type == OC_REP_STRING) { + if (std::string(oc_string(rep->name)) == "endpoint") { + dpsData.endpoint.uri = oc_string(rep->value.string); + return true; + } + if (std::string(oc_string(rep->name)) == "endpointName") { + dpsData.endpoint.name = oc_string(rep->value.string); + return true; + } + if (std::string(oc_string(rep->name)) == "provisionStatus") { + dpsData.provisionStatus = oc_string(rep->value.string); + return true; + } + return false; + } + + if (rep->type == OC_REP_OBJECT_ARRAY) { + if (std::string(oc_string(rep->name)) == "endpoints") { + dpsData.endpoints = parseDPSEndpoints(rep->value.object_array); + return true; + } + return false; + } + +#ifdef PLGD_DPS_RESOURCE_TEST_PROPERTIES + if (rep->type == OC_REP_OBJECT) { + if (std::string(oc_string(rep->name)) == "test") { + return true; + } + return false; + } +#endif /* PLGD_DPS_RESOURCE_TEST_PROPERTIES */ + + return false; +} + +static bool +parseDPSData(const oc_rep_t *rep, DPSData &dpsData) +{ + for (; rep != nullptr; rep = rep->next) { + if (oc_rep_is_baseline_interface_property(rep)) { + continue; + } + + if (parseDPSDataProperty(rep, dpsData)) { + continue; + } + DPS_DBG("Unexpected property: %s\n", oc_string(rep->name)); + } + return true; +} + +TEST_F(DPSResourceTest, EncodeRead) +{ + oc::RepPool pool{}; + + dps_resource_data_t data{}; + data.last_error = PLGD_DPS_ERROR_RESPONSE; + std::string status = kPlgdDpsStatusProvisioned; + data.provision_status = status.c_str(); + data.provision_status_length = status.length(); + oc_endpoint_addresses_t ea{}; + ASSERT_TRUE(dps_endpoints_init(&ea, nullptr, nullptr)); + data.endpoints = &ea; + std::string ep1_uri = "coap://[::1]:42"; + std::string ep1_name = "name"; + auto *ep1 = oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(ep1_uri.c_str(), ep1_uri.length()), + oc_string_view(ep1_name.c_str(), ep1_name.length()))); + ASSERT_NE(nullptr, ep1); + data.forceReprovision = true; + dps_resource_encode(OC_IF_R, nullptr, &data); + + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + DPSData parsed{}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_TRUE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(data.provision_status, parsed.provisionStatus->c_str()); + EXPECT_STREQ(ep1_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep1_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(0, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + // no status + rep.reset(); + pool.Clear(); + data.provision_status = nullptr; + data.provision_status_length = 0; + dps_resource_encode(OC_IF_R, nullptr, &data); + + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + parsed = {}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(ep1_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep1_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(0, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + // multiple endpoints + rep.reset(); + pool.Clear(); + std::string ep2_uri = "coap://[::1]:43"; + std::string ep2_name = "name2"; + auto *ep2 = oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(ep2_uri.c_str(), ep2_uri.length()), + oc_string_view(ep2_name.c_str(), ep2_name.length()))); + ASSERT_NE(nullptr, ep2); + oc_endpoint_addresses_select(&ea, ep2); + dps_resource_encode(OC_IF_R, nullptr, &data); + + rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + parsed = {}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_TRUE(parsed.lastErrorCode.has_value()); + EXPECT_EQ(data.last_error, *parsed.lastErrorCode); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(ep2_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(ep2_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(2, parsed.endpoints.size()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + oc_endpoint_addresses_deinit(&ea); +} + +TEST_F(DPSResourceTest, EncodeReadWrite) +{ + oc::RepPool pool{}; + + dps_resource_data_t data{}; + data.last_error = PLGD_DPS_ERROR_RESPONSE; + std::string status = kPlgdDpsStatusProvisioned; + data.provision_status = status.c_str(); + data.provision_status_length = status.length(); + oc_endpoint_addresses_t ea{}; + ASSERT_TRUE(dps_endpoints_init(&ea, nullptr, nullptr)); + data.endpoints = &ea; + std::string endpoint_uri = "coap://[::1]:42"; + std::string endpoint_name = "name"; + ASSERT_NE( + nullptr, + oc_endpoint_addresses_add( + &ea, oc_endpoint_address_make_view_with_name( + oc_string_view(endpoint_uri.c_str(), endpoint_uri.length()), + oc_string_view(endpoint_name.c_str(), endpoint_name.length())))); + data.forceReprovision = true; + dps_resource_encode(OC_IF_RW, nullptr, &data); + + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + DPSData parsed{}; + ASSERT_TRUE(parseDPSData(rep.get(), parsed)); + ASSERT_FALSE(parsed.lastErrorCode.has_value()); + ASSERT_FALSE(parsed.provisionStatus.has_value()); + EXPECT_STREQ(endpoint_uri.c_str(), parsed.endpoint.uri.c_str()); + EXPECT_STREQ(endpoint_name.c_str(), parsed.endpoint.name.c_str()); + EXPECT_EQ(data.forceReprovision, parsed.forceReprovision); + + oc_endpoint_addresses_deinit(&ea); +} + +TEST_F(DPSResourceTest, StatusToString) +{ + EXPECT_STREQ(dps_status_to_str(0).data, kPlgdDpsStatusUninitialized); + EXPECT_STREQ(dps_status_to_str(PLGD_DPS_INITIALIZED).data, + kPlgdDpsStatusInitialized); + EXPECT_STREQ(dps_status_to_str(PLGD_DPS_INITIALIZED | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + + uint32_t status = PLGD_DPS_INITIALIZED; + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_TIME).data, + kPlgdDpsStatusGetTime); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_TIME | PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_TIME).data, + kPlgdDpsStatusHasTime); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_TIME | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_TIME; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_OWNER).data, + kPlgdDpsStatusGetOwner); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_OWNER | PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_OWNER).data, + kPlgdDpsStatusHasOwner); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_OWNER | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_OWNER; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CLOUD).data, + kPlgdDpsStatusGetCloud); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_CLOUD | PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_CLOUD).data, + kPlgdDpsStatusHasCloud); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_CLOUD | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_CLOUD; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CREDENTIALS).data, + kPlgdDpsStatusGetCredentials); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_CREDENTIALS | + PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_CREDENTIALS).data, + kPlgdDpsStatusHasCredentials); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_CREDENTIALS | PLGD_DPS_FAILURE) + .data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_CREDENTIALS; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_GET_ACLS).data, + kPlgdDpsStatusGetAcls); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_GET_ACLS | PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_HAS_ACLS).data, + kPlgdDpsStatusHasAcls); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_HAS_ACLS | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_HAS_ACLS; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED).data, + kPlgdDpsStatusProvisioned); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED | + PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_CLOUD_STARTED | PLGD_DPS_FAILURE).data, + kPlgdDpsStatusFailure); + status |= PLGD_DPS_CLOUD_STARTED; + + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS).data, + kPlgdDpsStatusRenewCredentials); + EXPECT_STREQ(dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS | + PLGD_DPS_TRANSIENT_FAILURE) + .data, + kPlgdDpsStatusTransientFailure); + EXPECT_STREQ( + dps_status_to_str(status | PLGD_DPS_RENEW_CREDENTIALS | PLGD_DPS_FAILURE) + .data, + kPlgdDpsStatusFailure); + + for (uint8_t i = 0; i < 32; ++i) { + uint32_t flag = (1 << i); + if ((PLGD_DPS_PROVISIONED_ALL_FLAGS & flag) == 0) { + EXPECT_EQ(nullptr, dps_status_to_str(flag).data); + } + } +} + +class DPSResourceTestWithServer : public testing::Test { +public: + static void SetUpDPS() + { + plgd_dps_init(); + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + plgd_dps_set_configuration_resource(dps_ctx, true); + + ASSERT_TRUE(oc::SetAccessInRFOTM(dps_ctx->conf, true, + OC_PERM_RETRIEVE | OC_PERM_UPDATE)); + } + + static void SetUpTestCase() + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + SetUpDPS(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } +}; + +template +static void +getRequestWithQuery(const std::string &query = "") +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + auto getHandler = [](oc_client_response_t *data) { + oc::TestDevice::Terminate(); + *static_cast(data->user_data) = true; + EXPECT_EQ(CODE, data->code); + DPS_DBG("GET payload: %s", + oc::RepPool::GetJson(data->payload, true).data()); + if (data->code != OC_STATUS_OK) { + return; + } + DPSData dpsData{}; + EXPECT_TRUE(parseDPSData(data->payload, dpsData)); + ASSERT_TRUE(dpsData.lastErrorCode.has_value()); + EXPECT_EQ(0, *dpsData.lastErrorCode); + ASSERT_TRUE(dpsData.provisionStatus.has_value()); + EXPECT_STREQ(kPlgdDpsStatusUninitialized, dpsData.provisionStatus->c_str()); + EXPECT_FALSE(dpsData.forceReprovision); + }; + + auto timeout = 1s; + bool invoked = false; + ASSERT_TRUE(oc_do_get_with_timeout( + PLGD_DPS_URI, &ep, query.empty() ? nullptr : query.c_str(), timeout.count(), + getHandler, HIGH_QOS, &invoked)); + oc::TestDevice::PoolEventsMsV1(timeout, true); + EXPECT_TRUE(invoked); +} + +TEST_F(DPSResourceTestWithServer, GetRequest_FailNoDPS) +{ + plgd_dps_shutdown(); + oc::TestDevice::PoolEventsMsV1(10ms); + + oc_resource_t *dps = dps_create_dpsconf_resource(kDeviceID); + ASSERT_NE(nullptr, dps); + ASSERT_TRUE(oc::SetAccessInRFOTM(dps, true, OC_PERM_RETRIEVE)); + + getRequestWithQuery(); + + dps_delete_dpsconf_resource(dps); + SetUpDPS(); +} + +TEST_F(DPSResourceTestWithServer, GetRequest_FailNoDPSResource) +{ + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + plgd_dps_set_configuration_resource(dps_ctx, false); + + getRequestWithQuery(); + + plgd_dps_set_configuration_resource(dps_ctx, true); + ASSERT_TRUE(oc::SetAccessInRFOTM(dps_ctx->conf, true, + OC_PERM_RETRIEVE | OC_PERM_UPDATE)); +} + +TEST_F(DPSResourceTestWithServer, GetRequest) +{ + getRequestWithQuery("if=oic.if.r"); +} + +TEST_F(DPSResourceTestWithServer, GetRequestBaseline) +{ + getRequestWithQuery("if=oic.if.baseline"); +} + +template +static void +postRequest(Fn encode) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + auto postHandler = [](oc_client_response_t *data) { + oc::TestDevice::Terminate(); + *static_cast(data->user_data) = true; + DPS_DBG("POST payload: %s", + oc::RepPool::GetJson(data->payload, true).data()); + EXPECT_EQ(OC_STATUS_CHANGED, data->code); + }; + + bool invoked = false; + ASSERT_TRUE( + oc_init_post(PLGD_DPS_URI, &ep, nullptr, postHandler, HIGH_QOS, &invoked)); + + encode(); + + auto timeout = 1s; + ASSERT_TRUE(oc_do_post_with_timeout(timeout.count())); + oc::TestDevice::PoolEventsMsV1(timeout, true); + EXPECT_TRUE(invoked); +} + +TEST_F(DPSResourceTestWithServer, PostRequest) +{ + std::string ep1URI = "coap://dps.plgd.dev"; + std::string ep1Name = "plgd.dev"; + std::string ep2URI = "coaps://dps.plgd.dev"; + std::string ep2Name = "plgd.dev2"; + std::string ep3URI = "coap+tcp://dps.plgd.dev"; + + postRequest([&ep1URI, &ep1Name, &ep2URI, &ep2Name, &ep3URI] { + oc_rep_begin_root_object(); + oc_rep_set_text_string(root, endpoint, ep1URI.c_str()); + oc_rep_set_text_string(root, endpointName, ep1Name.c_str()); + std::string_view key{ "endpoints" }; + g_err |= + oc_rep_encode_text_string(oc_rep_object(root), key.data(), key.length()); + oc_rep_begin_array(oc_rep_object(root), servers); + oc_rep_object_array_begin_item(servers); + oc_rep_set_text_string(servers, uri, ep2URI.c_str()); + oc_rep_set_text_string(servers, name, ep2Name.c_str()); + oc_rep_object_array_end_item(servers); + oc_rep_object_array_begin_item(servers); + oc_rep_set_text_string(servers, uri, ep3URI.c_str()); + oc_rep_object_array_end_item(servers); + oc_rep_end_array(oc_rep_object(root), servers); + oc_rep_end_root_object(); + }); + + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, dps_ctx); + const auto *selected = plgd_dps_selected_endpoint_address(dps_ctx); + ASSERT_NE(nullptr, selected); + EXPECT_STREQ(ep1URI.c_str(), oc_string(*oc_endpoint_address_uri(selected))); + EXPECT_STREQ(ep1Name.c_str(), oc_string(*oc_endpoint_address_name(selected))); + + std::map> endpoints{}; + plgd_dps_iterate_server_addresses( + dps_ctx, + [](oc_endpoint_address_t *ea, void *data) { + auto &eps = + *static_cast *>( + data); + const oc_string_t *uri = oc_endpoint_address_uri(ea); + eps[oc_string(*uri)] = ea; + return true; + }, + &endpoints); + + ASSERT_EQ(3, endpoints.size()); + auto it = endpoints.find(ep1URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_STREQ(ep1Name.c_str(), + oc_string(*oc_endpoint_address_name(it->second))); + it = endpoints.find(ep2URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_STREQ(ep2Name.c_str(), + oc_string(*oc_endpoint_address_name(it->second))); + it = endpoints.find(ep3URI); + ASSERT_NE(endpoints.end(), it); + EXPECT_EQ(nullptr, oc_string(*oc_endpoint_address_name(it->second))); + + oc_endpoint_addresses_deinit(&dps_ctx->store.endpoints); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING && \ + OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM */ diff --git a/api/plgd/unittest/plgd_dps_retry.cpp b/api/plgd/unittest/plgd_dps_retry.cpp new file mode 100644 index 0000000000..563f41e9db --- /dev/null +++ b/api/plgd/unittest/plgd_dps_retry.cpp @@ -0,0 +1,92 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_retry_internal.h" + +#include "gtest/gtest.h" + +#include "oc_api.h" + +class TestDPSRetry : public testing::Test { +private: + static void SignalEventLoop() + { + // no-op for tests + } + + static int AppInit() + { + // no-op for tests + return 0; + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + EXPECT_EQ(0, oc_main_init(&handler)); + } + + void TearDown() override { oc_main_shutdown(); } +}; + +TEST_F(TestDPSRetry, IncrementRetry) +{ + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + dps_retry_init(&ctx.retry); + + size_t size = dps_retry_size(&ctx.retry); + EXPECT_LE(2, size); + for (size_t i = 0; i < size - 1; i++) { + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_LT(0, ctx.retry.count); + } + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); +} + +TEST_F(TestDPSRetry, ResetRetry) +{ + plgd_dps_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + dps_retry_init(&ctx.retry); + EXPECT_EQ(0, ctx.retry.count); + + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_LT(0, ctx.retry.count); + + dps_retry_reset(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); + + // change default_cfg size 0 to invoke reset internally + ctx.retry.default_cfg[0] = 0; + dps_retry_increment(&ctx, PLGD_DPS_GET_CREDENTIALS); + EXPECT_EQ(0, ctx.retry.count); + EXPECT_EQ(DEFAULT_RESET_TIMEOUT, ctx.retry.schedule_action.timeout); + EXPECT_LT(0, ctx.retry.schedule_action.delay); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_security.cpp b/api/plgd/unittest/plgd_dps_security.cpp new file mode 100644 index 0000000000..a221ee32c2 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_security.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_pki_internal.h" + +#include "gtest/gtest.h" + +#include + +TEST(DPSSecurityTest, CalculateCertificateCheckInterval) +{ + time_t now = time(nullptr); + EXPECT_NE(-1, now); + + const uint16_t expiresIn = 60; + dps_pki_configuration_t cfg = { /*.expiring_limit =*/expiresIn }; + uint64_t expired{ static_cast(now) - 1 }; + uint64_t expiring{ static_cast(now) + (expiresIn / 2) }; + // expiring and expired certificates should be immediately renewed + EXPECT_EQ(0, dps_pki_calculate_renew_certificates_interval(cfg, expired)); + EXPECT_EQ(0, dps_pki_calculate_renew_certificates_interval(cfg, expiring)); + + // non-expiring certificates should have some non-zero time for renewal + uint64_t valid{ static_cast(now) + (expiresIn * 2) }; + EXPECT_LT(0, dps_pki_calculate_renew_certificates_interval(cfg, valid)); +} + +TEST(DPSSecurityTest, CertificateStateToStr) +{ + EXPECT_STREQ("valid", + dps_pki_certificate_state_to_str(DPS_CERTIFICATE_VALID)); + EXPECT_STREQ("expired", + dps_pki_certificate_state_to_str(DPS_CERTIFICATE_EXPIRED)); +} + +TEST(DPSSecurityTest, CertificateValidityToState) +{ + time_t now = time(nullptr); + EXPECT_NE(-1, now); + + dps_pki_configuration_t cfg = { + /*.expiring_limit = */ 60, + }; + + time_t future = now + 60; + EXPECT_EQ(DPS_CERTIFICATE_NOT_YET_VALID, + dps_pki_validate_certificate(cfg, future, 0)); + + time_t expiring = now + cfg.expiring_limit - 1; + EXPECT_EQ(DPS_CERTIFICATE_EXPIRING, + dps_pki_validate_certificate(cfg, now, expiring)); + + time_t past = now - 1; + EXPECT_EQ(DPS_CERTIFICATE_EXPIRED, + dps_pki_validate_certificate(cfg, past, past)); + + time_t valid = now + cfg.expiring_limit + 60; + EXPECT_EQ(DPS_CERTIFICATE_VALID, + dps_pki_validate_certificate(cfg, now, valid)); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_store.cpp b/api/plgd/unittest/plgd_dps_store.cpp new file mode 100644 index 0000000000..ddb9bf1145 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_store.cpp @@ -0,0 +1,312 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/oc_helpers_internal.h" +#include "api/oc_runtime_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_log_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_store_internal.h" +#include "oc_helpers.h" +#include "plgd_dps_test.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" +#include "tests/gtest/RepPool.h" +#include "util/oc_endpoint_address_internal.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +static constexpr size_t kDeviceID = 0; +static constexpr std::string_view kStoragePath{ "dps_test_storage" }; + +using namespace std::chrono_literals; + +class DPSStoreTest : public testing::Test { +public: + static void SetUpTestCase() { oc_runtime_init(); } + + static void TearDownTestCase() { oc_runtime_shutdown(); } + + static bool is_directory(const char *path) + { + struct stat statbuf; + if (stat(path, &statbuf) != 0) { + return false; + } + return S_ISDIR(statbuf.st_mode) != 0; + } + + static bool make_storage(std::string_view storage) + { + if (mkdir(storage.data(), S_IRWXU | S_IRWXG) == 0) { + return true; + } + if ((EEXIST != errno) || !is_directory(storage.data())) { + DPS_ERR("failed to create storage at path %s", storage.data()); + return false; + } + return true; + } + + static void clean_storage() + { + for (const auto &entry : + std::filesystem::directory_iterator(kStoragePath.data())) { + std::filesystem::remove_all(entry.path()); + } + } +}; + +static void +compareStores(const plgd_dps_store_t &store1, const plgd_dps_store_t &store2) +{ + ASSERT_EQ(oc_endpoint_addresses_size(&store1.endpoints), + oc_endpoint_addresses_size(&store2.endpoints)); + const char *owner1 = oc_string(store1.owner); + const char *owner2 = oc_string(store2.owner); + if (owner1 == nullptr) { + ASSERT_EQ(nullptr, owner2); + } else { + ASSERT_NE(nullptr, owner2); + EXPECT_STREQ(owner1, owner2); + } + EXPECT_EQ(store1.has_been_provisioned_since_reset, + store2.has_been_provisioned_since_reset); +} + +TEST_F(DPSStoreTest, EncodeAndDecode) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + std::string ep1_uri = "/uri/1"; + std::string ep1_name = "name1"; + auto *ep1 = + plgd_dps_add_endpoint_address(ctx.get(), ep1_uri.c_str(), ep1_uri.length(), + ep1_name.c_str(), ep1_name.length()); + ASSERT_NE(nullptr, ep1); + std::string ep2_uri = "/uri/2"; + std::string ep2_name = "name2"; + auto *ep2 = + plgd_dps_add_endpoint_address(ctx.get(), ep2_uri.c_str(), ep2_uri.length(), + ep2_name.c_str(), ep2_name.length()); + ASSERT_NE(nullptr, ep2); + + oc::RepPool pool{}; + ASSERT_TRUE(dps_store_encode(&ctx->store)); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + auto rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + dps_store_decode(rep.get(), &store); + + compareStores(ctx->store, store); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, Decode_SelectedEndpointMissingURI) +{ + oc::RepPool pool{}; + + oc_rep_begin_root_object(); + oc_rep_set_text_string_v1(root, ep, "", 0); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + dps_store_decode(rep.get(), &store); + EXPECT_EQ(0, oc_endpoint_addresses_size(&store.endpoints)); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, Decode_EndpointMissingURI) +{ + oc::RepPool pool{}; + + oc_rep_begin_root_object(); + oc_rep_open_array(root, eps); + oc_rep_object_array_begin_item(eps); + oc_rep_set_text_string_v1(eps, ep, "", 0); + oc_rep_object_array_end_item(eps); + oc_rep_close_array(root, eps); + oc_rep_end_root_object(); + ASSERT_EQ(CborNoError, oc_rep_get_cbor_errno()); + oc::oc_rep_unique_ptr rep = pool.ParsePayload(); + ASSERT_NE(nullptr, rep.get()); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + dps_store_decode(rep.get(), &store); + EXPECT_EQ(0, oc_endpoint_addresses_size(&store.endpoints)); + + dps_store_deinit(&store); +} + +TEST_F(DPSStoreTest, SaveAndLoad) +{ + auto ctx = dps::make_unique_context(kDeviceID); + + EXPECT_GT(0, dps_store_load(&ctx->store, kDeviceID)); + EXPECT_GT(0, dps_store_dump(&ctx->store, kDeviceID)); + + EXPECT_FALSE(ctx->store.has_been_provisioned_since_reset); + + ASSERT_TRUE(make_storage(kStoragePath)); + ASSERT_EQ(0, oc_storage_config(kStoragePath.data())); + std::string endpoint_addr = "coaps+tcp://127.0.0.1:12345"; + std::string endpoint_name = "dps test endpoint"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx.get(), endpoint_addr.c_str(), endpoint_addr.length(), + endpoint_name.c_str(), endpoint_name.length())); + ctx->store.has_been_provisioned_since_reset = true; + auto *selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_NE(nullptr, selected); + auto *selected_addr = oc_endpoint_address_uri(selected); + ASSERT_NE(nullptr, selected_addr); + EXPECT_STREQ(endpoint_addr.c_str(), oc_string(*selected_addr)); + auto *selected_name = oc_endpoint_address_name(selected); + ASSERT_NE(nullptr, selected_name); + EXPECT_STREQ(endpoint_name.c_str(), oc_string(*selected_name)); + ASSERT_EQ(0, dps_store_dump(&ctx->store, kDeviceID)); + + oc_endpoint_addresses_deinit(&ctx->store.endpoints); + selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_EQ(nullptr, selected); + ctx->store.has_been_provisioned_since_reset = false; + + EXPECT_EQ(0, dps_store_load(&ctx->store, kDeviceID)); + selected = plgd_dps_selected_endpoint_address(ctx.get()); + ASSERT_NE(nullptr, selected); + selected_addr = oc_endpoint_address_uri(selected); + ASSERT_NE(nullptr, selected_addr); + EXPECT_STREQ(endpoint_addr.c_str(), oc_string(*selected_addr)); + selected_name = oc_endpoint_address_name(selected); + ASSERT_NE(nullptr, selected_name); + EXPECT_STREQ(endpoint_name.c_str(), oc_string(*selected_name)); + EXPECT_TRUE(ctx->store.has_been_provisioned_since_reset); + + clean_storage(); +} + +class DPSStoreWithDeviceTest : public testing::Test { +public: + static void SetUpTestCase() + { + ASSERT_TRUE(oc::TestDevice::StartServer()); + + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFNOP; + plgd_dps_init(); + } + + static void TearDownTestCase() + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } + + void SetUp() override + { + ASSERT_TRUE(DPSStoreTest::make_storage(kStoragePath)); + ASSERT_EQ(0, oc_storage_config(kStoragePath.data())); + } + + void TearDown() override { DPSStoreTest::clean_storage(); } +}; + +TEST_F(DPSStoreWithDeviceTest, DumpOnEndpointChange) +{ + auto *ctx = plgd_dps_get_context(kDeviceID); + ASSERT_NE(nullptr, ctx); + + plgd_dps_store_t store{}; + dps_store_init(&store, nullptr, nullptr); + ASSERT_GT(0, dps_store_load(&store, kDeviceID)); + + std::string ep1_uri = "coap+tcp://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep1_uri.c_str(), ep1_uri.length(), nullptr, 0)); + std::string ep2_uri = "coap://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep2_uri.c_str(), ep2_uri.length(), nullptr, 0)); +#ifdef OC_DYNAMIC_ALLOCATION + std::string ep3_uri = "coaps+tcp://127.0.0.1:12345"; + ASSERT_NE(nullptr, plgd_dps_add_endpoint_address( + ctx, ep3_uri.c_str(), ep3_uri.length(), nullptr, 0)); +#endif /* OC_DYNAMIC_ALLOCATION */ + oc::TestDevice::PoolEventsMsV1(10ms); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(3, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &store.endpoints, oc_string_view(ep1_uri.c_str(), ep1_uri.length()))); + + // change selected endpoint + oc_endpoint_addresses_select_by_uri( + &ctx->store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length())); + oc::TestDevice::PoolEventsMsV1(10ms); + dps_store_init(&store, nullptr, nullptr); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(3, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected( + &store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length()))); + + // remove selected endpoint + ASSERT_TRUE(oc_endpoint_addresses_remove_by_uri( + &ctx->store.endpoints, oc_string_view(ep2_uri.c_str(), ep2_uri.length()))); +#ifdef OC_DYNAMIC_ALLOCATION + oc_string_view_t selected = oc_string_view(ep3_uri.c_str(), ep3_uri.length()); +#else /* !OC_DYNAMIC_ALLOCATION */ + oc_string_view_t selected = oc_string_view(ep1_uri.c_str(), ep1_uri.length()); +#endif /* OC_DYNAMIC_ALLOCATION */ + ASSERT_TRUE( + oc_endpoint_addresses_is_selected(&ctx->store.endpoints, selected)); + oc::TestDevice::PoolEventsMsV1(10ms); + dps_store_init(&store, nullptr, nullptr); + ASSERT_EQ(0, dps_store_load(&store, kDeviceID)); +#ifdef OC_DYNAMIC_ALLOCATION + EXPECT_EQ(2, oc_endpoint_addresses_size(&store.endpoints)); +#else /* !OC_DYNAMIC_ALLOCATION */ + EXPECT_EQ(1, oc_endpoint_addresses_size(&store.endpoints)); +#endif /* OC_DYNAMIC_ALLOCATION */ + EXPECT_TRUE(oc_endpoint_addresses_is_selected(&store.endpoints, selected)); + + dps_store_deinit(&store); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_tag.cpp b/api/plgd/unittest/plgd_dps_tag.cpp new file mode 100644 index 0000000000..d0157c46fb --- /dev/null +++ b/api/plgd/unittest/plgd_dps_tag.cpp @@ -0,0 +1,153 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_tag_internal.h" +#include "oc_acl.h" +#include "oc_api.h" +#include "oc_cred.h" +#include "oc_pki.h" +#include "plgd_dps_test.h" + +#include "gtest/gtest.h" + +#include +#include + +class TestDPSTag : public testing::Test { +private: + static void SignalEventLoop() + { + // no-op for tests + } + + static int AppInit() + { + if (oc_init_platform("Samsung", nullptr, nullptr) != 0) { + return -1; + } + if (oc_add_device("/oic/d", "oic.d.light", "Lamp", "ocf.1.0.0", + "ocf.res.1.0.0", nullptr, nullptr) != 0) { + return -1; + } + return 0; + } + +public: + void SetUp() override + { + static oc_handler_t handler{}; + handler.init = AppInit; + handler.signal_event_loop = SignalEventLoop; + EXPECT_EQ(0, oc_main_init(&handler)); + } + + void TearDown() override { oc_main_shutdown(); } +}; + +#ifdef OC_DYNAMIC_ALLOCATION + +TEST_F(TestDPSTag, TagCredentials) +{ + oc::keypair_t rootKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + oc::keypair_t identKey{ oc::GetECPKeyPair(MBEDTLS_ECP_DP_SECP256R1) }; + int mfg_credid = + dps::addIdentityCertificate(0, identKey, rootKey, true, true); + ASSERT_LT(0, mfg_credid); + + dps_credentials_set_stale_tag(0); + oc_sec_cred_t *cred = oc_sec_get_cred_by_credid(mfg_credid, 0); + ASSERT_NE(nullptr, cred); + EXPECT_STREQ(DPS_STALE_TAG, oc_string(cred->tag)); + + dps_credentials_remove_stale_tag(0); + cred = oc_sec_get_cred_by_credid(mfg_credid, 0); + ASSERT_NE(nullptr, cred); + EXPECT_STREQ(DPS_TAG, oc_string(cred->tag)); + + ASSERT_TRUE(oc_sec_remove_cred_by_credid(mfg_credid, 0)); +} + +#endif /* OC_DYNAMIC_ALLOCATION */ + +TEST_F(TestDPSTag, TagNoCredentials) +{ + oc_sec_creds_t *creds = oc_sec_get_creds(0); + ASSERT_NE(nullptr, creds); + + for (const auto *cred = + static_cast(oc_list_head(creds->creds)); + cred != nullptr; cred = cred->next) { + ASSERT_EQ(nullptr, oc_string(cred->tag)); + } + dps_credentials_set_stale_tag(0); + + for (const auto *cred = + static_cast(oc_list_head(creds->creds)); + cred != nullptr; cred = cred->next) { + EXPECT_EQ(nullptr, oc_string(cred->tag)); + } + + dps_credentials_remove_stale_tag(0); +} + +TEST_F(TestDPSTag, TagACLs) +{ + EXPECT_TRUE(oc_sec_acl_add_bootstrap_acl(0)); + auto *ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + EXPECT_NE(nullptr, ace); + oc_set_string(&ace->tag, DPS_TAG, DPS_TAG_LEN); + + dps_acls_set_stale_tag(0); + ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + ASSERT_NE(nullptr, ace); + EXPECT_STREQ(DPS_STALE_TAG, oc_string(ace->tag)); + + dps_acls_remove_stale_tag(0); + ace = (oc_sec_ace_t *)oc_list_head(oc_sec_get_acl(0)->subjects); + ASSERT_NE(nullptr, ace); + EXPECT_STREQ(DPS_TAG, oc_string(ace->tag)); + + oc_set_string(&ace->tag, nullptr, 0); +} + +TEST_F(TestDPSTag, TagNoACLs) +{ + oc_sec_acl_t *acl = oc_sec_get_acl(0); + ASSERT_NE(nullptr, acl); + + for (const auto *ace = + static_cast(oc_list_head(acl->subjects)); + ace != nullptr; ace = ace->next) { + ASSERT_EQ(nullptr, oc_string(ace->tag)); + } + + dps_acls_set_stale_tag(0); + for (const auto *ace = + static_cast(oc_list_head(acl->subjects)); + ace != nullptr; ace = ace->next) { + EXPECT_EQ(nullptr, oc_string(ace->tag)); + } + + dps_acls_remove_stale_tag(0); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_test.cpp b/api/plgd/unittest/plgd_dps_test.cpp new file mode 100644 index 0000000000..2ff3fbc408 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_test.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_tag_internal.h" +#include "oc_cred.h" +#include "oc_pki.h" +#include "plgd_dps_test.h" +#include "tests/gtest/PKI.h" + +#include + +namespace dps { + +context_unique_ptr +make_unique_context(size_t device) +{ + auto ctxUPtr = + context_unique_ptr{ new plgd_dps_context_t, [](plgd_dps_context_t *ctx) { + dps_context_deinit(ctx); + delete ctx; + } }; + memset(ctxUPtr.get(), 0, sizeof(plgd_dps_context_t)); + dps_context_init(ctxUPtr.get(), device); + return ctxUPtr; +} + +#ifdef OC_DYNAMIC_ALLOCATION + +static bool +addTag(size_t device, int credid) +{ + oc_sec_cred_t *cred = oc_sec_get_cred_by_credid(credid, device); + if (cred == nullptr) { + return false; + } + oc_set_string(&cred->tag, DPS_TAG, DPS_TAG_LEN); + return true; +} + +int +addRootCertificate(size_t device, const oc::keypair_t &kp, bool is_mfg, + bool add_tag) +{ + auto pem = oc::pki::GenerateRootCertificate(kp); + int credid = is_mfg + ? oc_pki_add_mfg_trust_anchor(device, pem.data(), pem.size()) + : oc_pki_add_trust_anchor(device, pem.data(), pem.size()); + if ((credid < 0) || (add_tag && !addTag(device, credid))) { + goto error; + } + return credid; + +error: + if (credid != -1) { + oc_sec_remove_cred_by_credid(credid, device); + } + return -1; +} + +int +addIdentityCertificate(size_t device, const oc::keypair_t &kp, + const oc::keypair_t &issuer_kp, bool is_mfg, + bool add_tag) +{ + auto pem = oc::pki::GeneratIdentityCertificate(kp, issuer_kp); + if (pem.empty()) { + return -1; + } + oc::pki::KeyParser parser{}; + auto keyPem = + parser.GetPrivateKey(kp.private_key.data(), kp.private_key_size); + if (keyPem.empty()) { + return -1; + } + + int credid = is_mfg ? oc_pki_add_mfg_cert(device, pem.data(), pem.size(), + keyPem.data(), keyPem.size()) + : oc_pki_add_identity_cert(device, pem.data(), pem.size(), + keyPem.data(), keyPem.size()); + if ((credid < 0) || (add_tag && !addTag(device, credid))) { + goto error; + } + return credid; +error: + if (credid != -1) { + oc_sec_remove_cred_by_credid(credid, device); + } + return -1; +} + +#endif /* OC_DYNAMIC_ALLOCATION */ + +} // namespace dps + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_test.h b/api/plgd/unittest/plgd_dps_test.h new file mode 100644 index 0000000000..b6f65cee03 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_test.h @@ -0,0 +1,48 @@ +/**************************************************************************** + * + * 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. + * + ****************************************************************************/ + +#pragma once + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "tests/gtest/KeyPair.h" +#include "util/oc_features.h" + +#include +#include +#include +#include + +namespace dps { + +using context_unique_ptr = + std::unique_ptr; + +context_unique_ptr make_unique_context(size_t device); + +#ifdef OC_DYNAMIC_ALLOCATION + +int addRootCertificate(size_t device, const oc::keypair_t &kp, + bool is_mfg = false, bool add_tag = false); + +int addIdentityCertificate(size_t device, const oc::keypair_t &kp, + const oc::keypair_t &issuer_kp, bool is_mfg = false, + bool add_tag = false); + +#endif /* OC_DYNAMIC_ALLOCATION */ + +} // namespace dps diff --git a/api/plgd/unittest/plgd_dps_time.cpp b/api/plgd/unittest/plgd_dps_time.cpp new file mode 100644 index 0000000000..a738954e93 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_time.cpp @@ -0,0 +1,127 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_context_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_provision_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_time_internal.h" +#include "security/oc_pstat_internal.h" +#include "tests/gtest/Device.h" + +#include "gtest/gtest.h" + +using namespace std::chrono_literals; + +static constexpr size_t kDeviceID = 0; + +class TestDPSTimeWithDevice : public testing::Test { +public: + static void SetUpTestCase() { ASSERT_TRUE(oc::TestDevice::StartServer()); } + + static void TearDownTestCase() { oc::TestDevice::StopServer(); } + + void SetUp() override + { + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + ASSERT_NE(nullptr, pstat); + pstat->s = OC_DOS_RFNOP; + } + + void TearDown() override + { + oc::TestDevice::Reset(); + oc::TestDevice::CloseSessions(kDeviceID); + // wait for asynchronous closing of sessions to finish + oc::TestDevice::PoolEventsMsV1(10ms); + oc::TestDevice::ClearSystemTime(); + } +}; + +TEST_F(TestDPSTimeWithDevice, GetTime_FailInvalidDOSState) +{ + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + ASSERT_NE(nullptr, pstat); + pstat->s = OC_DOS_RFOTM; + + plgd_dps_context_t ctx{}; + ctx.device = kDeviceID; + EXPECT_FALSE(dps_get_plgd_time(&ctx)); + + pstat->s = OC_DOS_RFNOP; +} + +TEST_F(TestDPSTimeWithDevice, GetTime_OwnerAlreadySet) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_HAS_TIME; + EXPECT_TRUE(dps_get_plgd_time(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + oc_has_delayed_callback(nullptr, dps_provision_next_step_async, true); +} + +TEST_F(TestDPSTimeWithDevice, GetTime_InvalidStatus) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_INITIALIZED | PLGD_DPS_GET_OWNER; + EXPECT_TRUE(dps_get_plgd_time(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME | PLGD_DPS_GET_OWNER | + PLGD_DPS_FAILURE, + ctx.status); + EXPECT_EQ(PLGD_DPS_ERROR_GET_TIME, ctx.last_error); +} + +TEST_F(TestDPSTimeWithDevice, GetTime_InvalidResponse) +{ + auto epOpt = oc::TestDevice::GetEndpoint(kDeviceID); + ASSERT_TRUE(epOpt.has_value()); + auto ep = std::move(*epOpt); + + plgd_dps_context_t ctx{}; + ctx.endpoint = &ep; + ctx.status = PLGD_DPS_INITIALIZED; + EXPECT_TRUE(dps_get_plgd_time(&ctx)); + + auto timeout = 10ms; + oc::TestDevice::PoolEventsMsV1(timeout, true); + + EXPECT_EQ(PLGD_DPS_INITIALIZED | PLGD_DPS_GET_TIME | PLGD_DPS_FAILURE, + ctx.status); + EXPECT_EQ(PLGD_DPS_ERROR_RESPONSE, ctx.last_error); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/plgd/unittest/plgd_dps_verify_certificate.cpp b/api/plgd/unittest/plgd_dps_verify_certificate.cpp new file mode 100644 index 0000000000..65cfcd0c01 --- /dev/null +++ b/api/plgd/unittest/plgd_dps_verify_certificate.cpp @@ -0,0 +1,210 @@ +/**************************************************************************** + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + +#include "api/plgd/device-provisioning-client/plgd_dps_endpoint_internal.h" +#include "api/plgd/device-provisioning-client/plgd_dps_verify_certificate_internal.h" +#include "plgd_dps_test.h" +#include "security/oc_pstat_internal.h" +#include "security/oc_tls_internal.h" +#include "tests/gtest/Device.h" + +#include "gtest/gtest.h" +#include "mbedtls/x509_crt.h" + +#include +#include + +static constexpr size_t kDeviceID = 0; + +class TestVerifyCertificate : public testing::Test {}; + +TEST_F(TestVerifyCertificate, AllocateVerifyCertificateData) +{ + dps_verify_certificate_data_free(nullptr); + + oc_pki_user_data_t pud{}; + pud.data = malloc(42); + pud.free = free; + + dps_verify_certificate_data_t *dvcd = + dps_verify_certificate_data_new({ pud, nullptr }); + + dps_verify_certificate_data_free(dvcd); +} + +TEST_F(TestVerifyCertificate, VerifyCertificate_FailInvalidDevice) +{ + oc_tls_peer_t peer{}; + peer.endpoint.device = 42; + mbedtls_x509_crt crt{}; + uint32_t flags{}; + EXPECT_EQ(-1, dps_verify_certificate(&peer, &crt, 0, &flags)); +} + +TEST_F(TestVerifyCertificate, VerifyCertificate_FailMissingPeerContext) +{ + auto ctx = dps::make_unique_context(kDeviceID); + dps_context_list_add(ctx.get()); + + oc_tls_peer_t peer{}; + peer.endpoint.device = kDeviceID; + mbedtls_x509_crt crt{}; + uint32_t flags{}; + EXPECT_EQ(-1, dps_verify_certificate(&peer, &crt, 0, &flags)); + + dps_context_list_remove(ctx.get()); +} + +TEST_F(TestVerifyCertificate, VerifyCertificate_FailInvalidFingerprint) +{ + auto ctx = dps::make_unique_context(kDeviceID); + dps_context_list_add(ctx.get()); + + oc_tls_peer_t peer{}; + peer.endpoint.device = kDeviceID; + dps_verify_certificate_data_t vcd{}; + peer.user_data.data = &vcd; + mbedtls_x509_crt crt{}; + uint32_t flags{}; + + // unsupported algorithm + ctx->certificate_fingerprint.md_type = MBEDTLS_MD_MD5; + EXPECT_EQ(-1, dps_verify_certificate(&peer, &crt, 0, &flags)); + dps_context_list_remove(ctx.get()); +} + +class TestVerifyCertificateWithDevice : public testing::Test { +protected: + void SetUp() override + { + EXPECT_TRUE(oc::TestDevice::StartServer()); + + oc_sec_pstat_t *pstat = oc_sec_get_pstat(kDeviceID); + pstat->s = OC_DOS_RFNOP; + plgd_dps_init(); + } + + void TearDown() override + { + plgd_dps_shutdown(); + oc::TestDevice::StopServer(); + } + +public: + static oc_endpoint_t getEndpoint(const std::string &ep) + { + oc_string_t ep_str; + oc_new_string(&ep_str, ep.c_str(), ep.length()); + oc_endpoint_t endpoint; + EXPECT_EQ(0, oc_string_to_endpoint(&ep_str, &endpoint, nullptr)); + oc_free_string(&ep_str); + return endpoint; + } +}; + +TEST_F(TestVerifyCertificateWithDevice, VerifyCertificate) +{ + oc_endpoint_t ep = getEndpoint("coaps://[ff02::43]:1338"); + + std::string data = "c8:21:63:6f:61:70:73:2b:74:63:70:3a:2f:2f:6d:6f:63:6b:2e:" + "70:6c:67:64:2e:63:6c:6f:75:64:3a:32:36:" + "36:38:34:c9:20:a1:e1:c3:4c:3e:3:17:8d:e4:77:79:f9:92:28:" + "7d:fe:b4:b7:70:2f:80:ee:d9:15:dd:ec:d6:" + "54:e4:c6:4f:e2:ca:6:53:48:41:32:35:36"; + ssize_t ret = + plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), nullptr, 0); + ASSERT_EQ(77, ret); + std::array buf; + ret = plgd_dps_hex_string_to_bytes(data.c_str(), data.length(), &buf[0], ret); + ASSERT_EQ(77, ret); + EXPECT_EQ(PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION, + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + plgd_dps_get_context(kDeviceID), &buf[0], ret)); + + /* + * Convert a certificate from PEM to hex for embedding into C-code + * $ openssl x509 -outform der -in certificate.pem -out certificate.der + * $ xxd -i certificate.der + */ + std::array certificate = { + 0x30, 0x82, 0x01, 0x8d, 0x30, 0x82, 0x01, 0x32, 0xa0, 0x03, 0x02, 0x01, + 0x02, 0x02, 0x11, 0x00, 0xa7, 0x5b, 0x88, 0x93, 0x82, 0x6a, 0xba, 0xf8, + 0x60, 0xfd, 0xf6, 0x38, 0x8a, 0xca, 0xc9, 0xa1, 0x30, 0x0a, 0x06, 0x08, + 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x12, 0x31, 0x10, + 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x07, 0x70, 0x6c, 0x67, + 0x64, 0x2d, 0x63, 0x61, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x32, 0x31, 0x32, + 0x30, 0x31, 0x31, 0x31, 0x33, 0x38, 0x33, 0x37, 0x5a, 0x17, 0x0d, 0x32, + 0x33, 0x31, 0x32, 0x30, 0x31, 0x31, 0x31, 0x33, 0x38, 0x33, 0x37, 0x5a, + 0x30, 0x1a, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, + 0x0f, 0x6d, 0x6f, 0x63, 0x6b, 0x2e, 0x70, 0x6c, 0x67, 0x64, 0x2e, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, + 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, + 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x31, 0xe7, 0xc9, 0x43, 0xbf, + 0xd7, 0xfb, 0x88, 0x91, 0x78, 0xad, 0xcc, 0x0b, 0xd1, 0x54, 0xe4, 0x50, + 0xac, 0xb2, 0xbb, 0x6b, 0xa0, 0xec, 0x56, 0x6e, 0x96, 0x6d, 0x34, 0x6b, + 0xde, 0x03, 0x2f, 0x6a, 0x9c, 0x8e, 0x15, 0x2c, 0x1b, 0x37, 0x8e, 0x78, + 0x30, 0xe8, 0x7d, 0xba, 0xbe, 0x43, 0x33, 0x87, 0xab, 0x5e, 0x33, 0xe9, + 0x87, 0xe3, 0x32, 0x4a, 0xa5, 0x7e, 0xe5, 0x8e, 0xd6, 0x47, 0x78, 0xa3, + 0x61, 0x30, 0x5f, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x25, 0x04, 0x16, + 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x30, 0x0c, + 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, + 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, + 0x14, 0xb3, 0x75, 0xa7, 0xac, 0x25, 0x64, 0x51, 0x5d, 0xe6, 0x15, 0x5d, + 0x15, 0x16, 0xe2, 0xe8, 0x5f, 0xff, 0xc9, 0x3d, 0x91, 0x30, 0x0f, 0x06, + 0x03, 0x55, 0x1d, 0x11, 0x04, 0x08, 0x30, 0x06, 0x87, 0x04, 0x7f, 0x00, + 0x00, 0x01, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, 0x21, 0x00, 0xc0, 0x7d, + 0xe9, 0x7b, 0x47, 0x40, 0x8f, 0x0e, 0xfe, 0x86, 0x17, 0xf8, 0xbd, 0xde, + 0x4a, 0x60, 0x34, 0x9b, 0xca, 0x97, 0xcc, 0x8e, 0xed, 0x55, 0xd3, 0xbe, + 0xcb, 0xdc, 0x37, 0xa5, 0x48, 0x75, 0x02, 0x21, 0x00, 0x93, 0x88, 0x3e, + 0x53, 0x7c, 0xa6, 0x1e, 0xc9, 0x04, 0x9a, 0x6d, 0xf8, 0x4f, 0x72, 0xd3, + 0x7a, 0x84, 0x11, 0xad, 0xef, 0x4e, 0x76, 0xb5, 0x87, 0xe1, 0x1e, 0x97, + 0x0e, 0x21, 0xfb, 0x94, 0xd0 + }; + + oc_tls_peer_t *peer = dps_endpoint_add_peer(&ep); + ASSERT_NE(nullptr, peer); + ASSERT_NE(nullptr, peer->verify_certificate); + + mbedtls_x509_crt crt; + memset(&crt, 0, sizeof(mbedtls_x509_crt)); + + // validate intermediate certificate + uint32_t flags = MBEDTLS_X509_BADCERT_NOT_TRUSTED; + ASSERT_EQ(0, peer->verify_certificate(peer, &crt, 1, &flags)); + ASSERT_EQ(0, flags); + + // validate leaf certificate + flags = MBEDTLS_X509_BADCERT_NOT_TRUSTED; + ASSERT_EQ(-1, peer->verify_certificate(peer, &crt, 0, &flags)); + ASSERT_EQ(MBEDTLS_X509_BADCERT_NOT_TRUSTED, flags); + + // validate leaf with good certificate + crt.raw.p = &certificate[0]; + crt.raw.len = certificate.size(); + flags = MBEDTLS_X509_BADCERT_NOT_TRUSTED; + ASSERT_EQ(0, peer->verify_certificate(peer, &crt, 0, &flags)); + ASSERT_EQ(0, flags); +} + +#endif /* OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING */ diff --git a/api/unittest/plgdtimetest.cpp b/api/plgd/unittest/plgdtimetest.cpp similarity index 100% rename from api/unittest/plgdtimetest.cpp rename to api/plgd/unittest/plgdtimetest.cpp diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index f7a1d4fa16..ea0c470107 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -226,6 +226,20 @@ if(UNIX) DEPENDENCIES client-server-static ) endif() + + if(PLGD_DEV_DEVICE_PROVISIONING_ENABLED) + oc_add_app_executable( + TARGET dps_cloud_server + SOURCES ${PROJECT_SOURCE_DIR}/dps_cloud_server.c + DEPENDENCIES client-server-static + ) + if(PLGD_DPS_CLOUD_SERVER_DBG_ENABLED) + target_compile_definitions(dps_cloud_server PRIVATE PLGD_DPS_CLOUD_SERVER_DBG) + endif() + if(PLGD_DPS_FAKETIME_ENABLED) + target_compile_definitions(dps_cloud_server PRIVATE "PLGD_DPS_FAKETIME" "PLGD_DPS_FAKETIME_SET_SYSTEM_TIME_ON_RESET") + endif() + endif() elseif(WIN32) oc_add_app_executable( TARGET simpleclient diff --git a/apps/dps_cloud_server.c b/apps/dps_cloud_server.c new file mode 100644 index 0000000000..c57699c299 --- /dev/null +++ b/apps/dps_cloud_server.c @@ -0,0 +1,1956 @@ +/**************************************************************************** + * + * 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. + * + ****************************************************************************/ + +// NOLINTNEXTLINE(bugprone-reserved-identifier) +#define _GNU_SOURCE // required to get strptime from time.h + +#include "oc_api.h" +#include "oc_certs.h" +#include "oc_clock_util.h" +#include "oc_core_res.h" +#include "oc_cred.h" +#include "oc_log.h" +#include "oc_pki.h" +#include "plgd/plgd_dps.h" +#include "plgd/plgd_time.h" +#include "util/oc_atomic.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef PLGD_DPS_CLOUD_SERVER_DBG +#define DPSCS_DBG(...) printf(__VA_ARGS__) +#else /* !PLGD_DPS_CLOUD_SERVER_DBG */ +#define DPSCS_DBG(...) +#endif /* PLGD_DPS_CLOUD_SERVER_DBG */ + +#ifdef __linux__ +#include +#include +#include +#include +#include +#else +#error "Unsupported OS" +#endif + +#ifdef PLGD_DPS_FAKETIME +#include +#endif + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +/* Signal variables */ +static OC_ATOMIC_INT8_T g_quit = 0; +static OC_ATOMIC_INT8_T g_reset = 0; + +/* Application state */ +static bool g_initialized = false; +static const size_t g_device_id = 0; +static int g_create_configuration_resource = 0; +static int g_expiration_limit = -1; +static int16_t g_observer_max_retry = -1; +static int g_skip_ca_verification = 0; +static int g_wait_for_reset = 0; +static bool g_dhcp_enabled = false; +static bool g_set_system_time = false; + +#ifdef PLGD_DPS_FAKETIME +static struct +{ + char value[128]; // NOLINT(readability-magic-numbers) + char format[64]; // NOLINT(readability-magic-numbers) + bool enabled; +} g_faketime = { 0 }; + +#endif /* PLGD_DPS_FAKETIME */ + +/* Application configuration */ +static const char *g_dps_device_name = "dps"; +static char g_dps_cert_dir[PATH_MAX] = "pki_certs"; +#ifdef OC_DYNAMIC_ALLOCATION +static char **g_dps_endpoint = NULL; +#else /* !OC_DYNAMIC_ALLOCATION */ +static char g_dps_endpoint[2][512] = { + { 0 }, + { 0 } +}; // NOLINT(readability-magic-numbers) +#endif /* OC_DYNAMIC_ALLOCATION */ +static int g_dps_endpoint_count = 0; +static const char *g_dhcp_leases_file = "/var/lib/dhcp/dhclient.leases"; +static const char *g_dhcp_option_vendor_encapsulated_options = + "vendor-encapsulated-options"; + +/* Run-loop synchronization */ +static int g_eventfd = -1; + +static const char * +strnstr(const char *str1, const char *str2, size_t n) +{ + // simplistic algorithm with O(n2) worst case + if (str2 == NULL || str1 == NULL || *str2 == '\0') { + return NULL; + } + for (size_t len = strlen(str2); len <= n; n--, str1++) { + if (memcmp(str1, str2, len) == 0) { + return str1; + } + } + return NULL; +} + +static const char * +find_start_value(const char *value, size_t len) +{ + const char *ignored_characters = " \t\""; + for (size_t i = 0; i < len; i++) { + if (strchr(ignored_characters, value[i]) != NULL) { + continue; + } + return value + i; + } + return NULL; +} + +static const char * +find_end_value(const char *value, size_t len) +{ + const char *end_separator = " \t\"\n,;"; + for (size_t i = 0; i < len; i++) { + if (strchr(end_separator, value[i]) != NULL) { + return value + i; + } + } + return NULL; +} + +/** + * @brief Callback function which is called when value of dhcp option has been + * parsed. + * + * @param value value is temporary buffer, so user must copy it to other buffer. + * @param value_len length of value. + * @param user_data user data. + * @return true the parsing will continue. + * @return false for end parsing. + */ +typedef bool (*plgd_dps_dhcp_get_option_value_cb_t)(const char *value, + size_t value_len, + void *user_data); + +/** + * @brief Parse dhcp options from dhcp leases file. + * + * @param file_path path to dhcp lease file. + * @param dhcp_option_name name of dhcp option. + * @param value_cb callback function to get dps uri. + * @param user_data user data. + * @return 0 on success. + * @return -1 on failure. + */ +static int +plgd_dps_dhcp_get_option_leases_file( + const char *file_path, const char *dhcp_option_name, + plgd_dps_dhcp_get_option_value_cb_t value_cb, void *user_data) +{ + if (file_path == NULL || dhcp_option_name == NULL) { + printf("ERROR: invalid arguments"); + return -1; + } + FILE *file = fopen(file_path, "r"); + if (file == NULL) { + printf("ERROR: cannot open file %s", file_path); + return -1; + } + + char *line = NULL; + size_t len = 0; + bool found = false; + while ((getline(&line, &len, file)) != -1) { + const char *opt = strnstr(line, dhcp_option_name, len); + if (opt == NULL) { + continue; + } + const char *start_value = + find_start_value(opt + strlen(dhcp_option_name), + len - (opt - line) - strlen(dhcp_option_name)); + if (start_value == NULL) { + DPSCS_DBG("cannot find start for option %s in line %s", dhcp_option_name, + line); + continue; + } + const char *end_value = + find_end_value(start_value, len - (start_value - line)); + if (end_value == NULL) { + DPSCS_DBG("cannot find end for option %s in line %s", dhcp_option_name, + line); + continue; + } + size_t value_len = end_value - start_value; + if (value_len == 0) { + DPSCS_DBG("value is empty for option %s in line %s", dhcp_option_name, + line); + continue; + } + found = true; + if (value_cb != NULL) { + char buf[value_len + 1]; + memcpy(buf, start_value, value_len); + buf[value_len] = '\0'; + bool con = value_cb(start_value, value_len, user_data); + if (!con) { + break; + } + } + } + fclose(file); + if (line) { + free(line); + } + if (!found) { + printf("ERROR: cannot find option %s in file %s", dhcp_option_name, + file_path); + } + return found ? 0 : -1; +} + +static void +signal_event_loop(void) +{ + ssize_t len = 0; + do { + len = eventfd_write(g_eventfd, 1); + } while (len < 0 && errno == EINTR); +#ifdef PLGD_DPS_CLOUD_SERVER_DBG + if (len < 0) { + DPSCS_DBG("failed to signal loop event, error (%d)", errno); + } +#endif /* PLGD_DPS_CLOUD_SERVER_DBG */ +} + +static void +handle_signal(int signal) +{ + if (signal == SIGPIPE) { + return; + } + signal_event_loop(); + if (signal == SIGHUP) { + OC_ATOMIC_STORE8(g_reset, 1); + } else { + OC_ATOMIC_STORE8(g_quit, 1); + } +} + +static bool +init(void) +{ + struct sigaction sig; + sigfillset(&sig.sa_mask); + sig.sa_flags = 0; + sig.sa_handler = handle_signal; + sigaction(SIGHUP, &sig, NULL); + sigaction(SIGINT, &sig, NULL); + sigaction(SIGPIPE, &sig, NULL); + sigaction(SIGTERM, &sig, NULL); + + int evtfd = + eventfd(/*initval*/ 0, EFD_SEMAPHORE | EFD_NONBLOCK | EFD_CLOEXEC); + if (evtfd < 0) { + printf("ERROR: failed to create eventfd, error (%d)\n", errno); + return false; + } + g_eventfd = evtfd; + return true; +} + +static void +deinit(void) +{ + close(g_eventfd); + g_eventfd = -1; +#ifdef OC_DYNAMIC_ALLOCATION + for (int i = 0; i < g_dps_endpoint_count; i++) { + free(g_dps_endpoint[i]); + } + free(g_dps_endpoint); + g_dps_endpoint = NULL; +#else /* !OC_DYNAMIC_ALLOCATION */ + memset(g_dps_endpoint, 0, sizeof(g_dps_endpoint)); +#endif /* OC_DYNAMIC_ALLOCATION */ + g_dps_endpoint_count = 0; +} + +static void +reset(void) +{ + DPSCS_DBG("reset (device(%zu) %s)\n", g_device_id, + g_initialized ? "initialized" : "not initialized"); + if (!g_initialized) { + return; + } + const plgd_dps_context_t *ctx = plgd_dps_get_context(g_device_id); + if (ctx == NULL) { + printf("ERROR: cannot reset: device(%zu) context not found\n", g_device_id); + return; + } + oc_reset_device(plgd_dps_get_device(ctx)); +} + +static void +run_loop_read_eventfd(int evtfd) +{ + ssize_t len; + eventfd_t dummy_value; + do { + len = eventfd_read(evtfd, &dummy_value); + } while (len < 0 && errno == EINTR); +} + +static void +run_loop_ppoll(int evtfd, const struct timespec *timeout) +{ + struct pollfd fds = { + .fd = evtfd, + .events = POLLIN, + }; + int ret = ppoll(&fds, 1, timeout, NULL); + if (ret < 0) { + printf("ERROR: failed to poll descriptor(%d), error(%d)\n", evtfd, errno); + return; + } + if (ret == 0) { + return; + } + + if ((fds.revents & POLLIN) == 0) { + return; + } + run_loop_read_eventfd(evtfd); +} + +static void +run(void) +{ + while (OC_ATOMIC_LOAD8(g_quit) != 1) { + if (OC_ATOMIC_LOAD8(g_reset) != 0) { + OC_ATOMIC_STORE8(g_reset, 0); + g_wait_for_reset = 0; + reset(); + } + + oc_clock_time_t next_event_mt = oc_main_poll_v1(); + if (next_event_mt == 0) { + run_loop_ppoll(g_eventfd, NULL); + continue; + } + + oc_clock_time_t now_mt = oc_clock_time_monotonic(); + if (now_mt >= next_event_mt) { + continue; + } + struct timespec timeout = oc_clock_time_to_timespec(next_event_mt - now_mt); + run_loop_ppoll(g_eventfd, &timeout); + } +} + +static void +cloud_status_handler(oc_cloud_context_t *cloud_ctx, oc_cloud_status_t status, + void *data) +{ + (void)data; + printf("\nCloud Manager Status:\n"); + if (status & OC_CLOUD_REGISTERED) { + printf("\t\t-Registered\n"); + } + if (status & OC_CLOUD_TOKEN_EXPIRY) { + printf("\t\t-Token Expiry: "); + if (cloud_ctx != NULL) { + printf("%d\n", oc_cloud_get_token_expiry(cloud_ctx)); + } else { + printf("\n"); + } + } + if (status & OC_CLOUD_FAILURE) { + printf("\t\t-Failure\n"); + } + if (status & OC_CLOUD_LOGGED_IN) { + printf("\t\t-Logged In\n"); + } + if (status & OC_CLOUD_LOGGED_OUT) { + printf("\t\t-Logged Out\n"); + } + if (status & OC_CLOUD_DEREGISTERED) { + printf("\t\t-DeRegistered\n"); + } + if (status & OC_CLOUD_REFRESHED_TOKEN) { + printf("\t\t-Refreshed Token\n"); + } +} + +static void +dps_status_handler(plgd_dps_context_t *ctx, plgd_dps_status_t status, + void *data) +{ + (void)data; + (void)ctx; + printf("\nDPS Manager Status:\n"); + if (status == 0) { + printf("\t\t-Uninitialized\n"); + } + if ((status & PLGD_DPS_INITIALIZED) != 0) { + printf("\t\t-Initialized\n"); + } + if ((status & PLGD_DPS_GET_TIME) != 0) { + printf("\t\t-Get time\n"); + } + if ((status & PLGD_DPS_HAS_TIME) != 0) { + printf("\t\t-Has time\n"); + } + if ((status & PLGD_DPS_GET_OWNER) != 0) { + printf("\t\t-Get owner\n"); + } + if ((status & PLGD_DPS_HAS_OWNER) != 0) { + printf("\t\t-Has owner\n"); + } + if ((status & PLGD_DPS_GET_CLOUD) != 0) { + printf("\t\t-Get cloud configuration\n"); + } + if ((status & PLGD_DPS_HAS_CLOUD) != 0) { + printf("\t\t-Has cloud configuration\n"); + } + if ((status & PLGD_DPS_GET_CREDENTIALS) != 0) { + printf("\t\t-Get credentials\n"); + } + if ((status & PLGD_DPS_HAS_CREDENTIALS) != 0) { + printf("\t\t-Has credentials\n"); + } + if ((status & PLGD_DPS_GET_ACLS) != 0) { + printf("\t\t-Get acls\n"); + } + if ((status & PLGD_DPS_HAS_ACLS) != 0) { + printf("\t\t-Has set acls\n"); + } + if ((status & PLGD_DPS_CLOUD_STARTED) != 0) { + printf("\t\t-Started cloud\n"); + } + if ((status & PLGD_DPS_RENEW_CREDENTIALS) != 0) { + printf("\t\t-Renew credentials\n"); + } + if ((status & PLGD_DPS_TRANSIENT_FAILURE) != 0) { + printf("\t\t-Transient failure\n"); + } + if ((status & PLGD_DPS_FAILURE) != 0) { + printf("\t\t-Failure\n"); + } +} + +static int +set_system_time(oc_clock_time_t time, void *data) +{ + (void)data; + struct timeval now; + now.tv_sec = (long)(time / OC_CLOCK_SECOND); + oc_clock_time_t rem_ticks = time % OC_CLOCK_SECOND; +#define USECS_IN_SEC 1000000 + now.tv_usec = + (__suseconds_t)(((double)rem_ticks * USECS_IN_SEC) / OC_CLOCK_SECOND); + return settimeofday(&now, NULL); +} + +static int +print_time(oc_clock_time_t time, void *data) +{ + (void)data; +#define RFC3339_BUFFER_SIZE (64) + char ts_str[RFC3339_BUFFER_SIZE] = { 0 }; + oc_clock_encode_time_rfc3339(time, ts_str, sizeof(ts_str)); + DPSCS_DBG("plgd time: %s\n", ts_str); + return 0; +} + +/** + * @brief Configure the plgd-time feature. + * + * @param system_time if true then settimeofday is used in by plgd_time_set_time + * to set time on the whole system otherwise mbedTLS function to get current + * time is overriden to get time derived from plgd_time + */ +static void +time_configure(bool system_time) +{ + if (system_time) { + DPSCS_DBG("using settimeofday to set system time\n"); + plgd_time_configure(/*use_in_mbedtls*/ false, set_system_time, NULL); + return; + } + DPSCS_DBG("using plgd time in mbedTLS\n"); + plgd_time_configure(/*use_in_mbedtls*/ true, print_time, NULL); +} + +static int +app_init(void) +{ + // define application specific values. + const char *spec_version = "ocf.2.0.5"; + const char *data_model_version = "ocf.res.1.3.0"; + const char *device_rt = "oic.d.cloudDevice"; + const char *manufacturer = "ocfcloud.com"; + + oc_set_con_res_announced(true); + if (oc_init_platform(manufacturer, NULL, NULL) != 0) { + printf("ERROR: failed to init platform\n"); + return -1; + } + if (oc_add_device("/oic/d", device_rt, g_dps_device_name, spec_version, + data_model_version, NULL, NULL) != 0) { + printf("ERROR: failed to add device resource\n"); + return -1; + } + if (plgd_dps_init() != 0) { + return -1; + } + time_configure(g_set_system_time); + return 0; +} + +struct light_t +{ + bool state; + int64_t power; +}; + +static struct light_t light1 = { 0 }; + +static void +get_handler(oc_request_t *request, oc_interface_mask_t iface, void *user_data) +{ + DPSCS_DBG("get_handler:\n"); + const struct light_t *light = (const struct light_t *)user_data; + + oc_rep_start_root_object(); + switch (iface) { + case OC_IF_BASELINE: + oc_process_baseline_interface(request->resource); + __attribute__((fallthrough)); + case OC_IF_RW: + oc_rep_set_boolean(root, state, light->state); + oc_rep_set_int(root, power, light->power); + oc_rep_set_text_string(root, name, "Light"); + break; + default: + break; + } + oc_rep_end_root_object(); + oc_send_response(request, OC_STATUS_OK); +} + +static void +post_handler(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + DPSCS_DBG("post_handler:\n"); + struct light_t *light = (struct light_t *)user_data; + (void)iface_mask; + for (oc_rep_t *rep = request->request_payload; rep != NULL; rep = rep->next) { + const char *key = oc_string(rep->name); + if (key == NULL) { + continue; + } + DPSCS_DBG("key: %s ", key); + if (strcmp(key, "state") == 0) { + if (rep->type != OC_REP_BOOL) { + oc_send_response(request, OC_STATUS_BAD_REQUEST); + return; + } + light->state = rep->value.boolean; + DPSCS_DBG("value: %d", light->state); + continue; + } + if (strcmp(key, "power") == 0) { + if (rep->type != OC_REP_INT) { + oc_send_response(request, OC_STATUS_BAD_REQUEST); + return; + } + light->power = rep->value.integer; + DPSCS_DBG("value: %" PRId64, light->power); + continue; + } + } + DPSCS_DBG("\n"); + oc_send_response(request, OC_STATUS_CHANGED); +} + +static bool +register_lights(size_t device) +{ + oc_resource_t *res = oc_new_resource(NULL, "/light/1", 1, device); + oc_resource_bind_resource_type(res, "core.light"); + oc_resource_bind_resource_interface(res, OC_IF_RW); + oc_resource_set_default_interface(res, OC_IF_RW); + oc_resource_set_discoverable(res, true); + oc_resource_set_observable(res, true); + oc_resource_set_request_handler(res, OC_GET, get_handler, &light1); + oc_resource_set_request_handler(res, OC_POST, post_handler, &light1); + if (!oc_add_resource(res)) { + printf("ERROR: Could not add %s resource to device\n", oc_string(res->uri)); + return false; + } + if (oc_cloud_add_resource(res) < 0) { + printf("ERROR: Could not add %s resource to cloud\n", oc_string(res->uri)); + return false; + } + return true; +} + +#ifdef OC_COLLECTIONS + +/* Setting custom Collection-level properties */ +static int64_t g_battery_level = 94; // NOLINT(readability-magic-numbers) + +static bool +set_switches_properties(const oc_resource_t *resource, const oc_rep_t *rep, + void *data) +{ + (void)resource; + (void)data; + for (; rep != NULL; rep = rep->next) { + if (rep->type != OC_REP_INT) { + continue; + } + const char battery_level[] = "bl"; + if (oc_string_len(rep->name) == sizeof(battery_level) - 1 && + memcmp(oc_string(rep->name), battery_level, + sizeof(battery_level) - 1) == 0) { + g_battery_level = rep->value.integer; + } + } + return true; +} + +static void +get_switches_properties(const oc_resource_t *resource, + oc_interface_mask_t iface_mask, void *data) +{ + (void)resource; + (void)data; + if (iface_mask == OC_IF_BASELINE) { + oc_rep_set_int(root, x.org.openconnectivity.bl, g_battery_level); + } +} + +/* Resource creation and request handlers for oic.r.switch.binary instances */ +typedef struct oc_switch_t +{ + struct oc_switch_t *next; + oc_resource_t *resource; + uint16_t id; + bool state; +} oc_switch_t; + +#ifdef OC_COLLECTIONS_IF_CREATE + +OC_MEMB(switch_s, oc_switch_t, 1); +OC_LIST(switches); // list of switch instances ordered by id + +static bool +set_switch_properties(const oc_resource_t *resource, const oc_rep_t *rep, + void *data) +{ + (void)resource; + oc_switch_t *cswitch = (oc_switch_t *)data; + for (; rep != NULL; rep = rep->next) { + if (rep->type != OC_REP_BOOL) { + continue; + } + cswitch->state = rep->value.boolean; + } + return true; +} + +static void +get_switch_properties(const oc_resource_t *resource, + oc_interface_mask_t iface_mask, void *data) +{ + const oc_switch_t *cswitch = (const oc_switch_t *)data; + switch (iface_mask) { + case OC_IF_BASELINE: + oc_process_baseline_interface(resource); + __attribute__((fallthrough)); + case OC_IF_A: + oc_rep_set_boolean(root, value, cswitch->state); + break; + default: + break; + } +} + +static bool +validate_cswitch_payload(oc_rep_t *rep) +{ + for (; rep != NULL; rep = rep->next) { + if (rep->type == OC_REP_BOOL) { + const char rep_name[] = "value"; + const size_t rep_name_len = sizeof(rep_name) - 1; + if (oc_string_len(rep->name) != rep_name_len || + memcmp(oc_string(rep->name), rep_name, rep_name_len) != 0) { + return false; + } + continue; + } + + if ((oc_string_len(rep->name) > 2) && + (strncmp(oc_string(rep->name), "x.", 2) == 0)) { + continue; + } + return false; + } + return true; +} + +static void +post_cswitch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + (void)iface_mask; + oc_switch_t *cswitch = (oc_switch_t *)user_data; + + bool bad_request = !validate_cswitch_payload(request->request_payload); + if (!bad_request) { + set_switch_properties(request->resource, request->request_payload, cswitch); + } + + oc_rep_start_root_object(); + oc_rep_set_boolean(root, value, cswitch->state); + oc_rep_end_root_object(); + + oc_status_t code = OC_STATUS_CHANGED; + if (bad_request) { + code = OC_STATUS_BAD_REQUEST; + } + oc_send_response(request, code); +} + +static void +get_cswitch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + oc_rep_start_root_object(); + get_switch_properties(request->resource, iface_mask, user_data); + oc_rep_end_root_object(); + oc_send_response(request, OC_STATUS_OK); +} + +static void +delete_cswitch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + DPSCS_DBG("%s\n", __func__); + (void)request; + (void)iface_mask; + oc_switch_t *cswitch = (oc_switch_t *)user_data; + + oc_delayed_delete_resource(cswitch->resource); + oc_send_response(request, OC_STATUS_DELETED); +} + +static oc_event_callback_retval_t +register_to_cloud(void *resource) +{ + oc_resource_t *res = (oc_resource_t *)resource; + oc_cloud_add_resource(res); + return OC_EVENT_DONE; +} + +static oc_resource_t * +get_switch_instance( + const char *href, const oc_string_array_t *types, + oc_resource_properties_t prop, oc_interface_mask_t iface_mask, + size_t device) // NOLINT(bugprone-easily-swappable-parameters) +{ + oc_switch_t *cswitch = (oc_switch_t *)oc_memb_alloc(&switch_s); + if (cswitch == NULL) { + return NULL; + } + cswitch->resource = oc_new_resource( + NULL, href, (uint8_t)oc_string_array_get_allocated_size(*types), device); + if (cswitch->resource == NULL) { + oc_memb_free(&switch_s, cswitch); + return NULL; + } + for (size_t i = 0; i < oc_string_array_get_allocated_size(*types); i++) { + const char *type = oc_string_array_get_item(*types, i); + oc_resource_bind_resource_type(cswitch->resource, type); + } + oc_resource_bind_resource_interface(cswitch->resource, iface_mask); + cswitch->resource->properties = prop; + oc_resource_set_default_interface(cswitch->resource, OC_IF_A); + oc_resource_set_request_handler(cswitch->resource, OC_GET, get_cswitch, + cswitch); + oc_resource_set_request_handler(cswitch->resource, OC_DELETE, delete_cswitch, + cswitch); + oc_resource_set_request_handler(cswitch->resource, OC_POST, post_cswitch, + cswitch); + oc_resource_set_properties_cbs(cswitch->resource, get_switch_properties, + cswitch, set_switch_properties, cswitch); + oc_add_resource(cswitch->resource); + oc_set_delayed_callback(cswitch->resource, register_to_cloud, 0); + oc_list_add(switches, cswitch); + return cswitch->resource; +} + +static void +free_switch_instance(oc_resource_t *resource) +{ + DPSCS_DBG("%s\n", __func__); + oc_switch_t *cswitch = (oc_switch_t *)oc_list_head(switches); + while (cswitch) { + if (cswitch->resource == resource) { + oc_cloud_delete_resource(resource); + oc_delete_resource(resource); + oc_list_remove(switches, cswitch); + oc_memb_free(&switch_s, cswitch); + return; + } + cswitch = cswitch->next; + } +} + +#endif /* OC_COLLECTIONS_IF_CREATE */ + +static bool +register_collection(size_t device) +{ + oc_resource_t *col = oc_new_collection(NULL, "/switches", 1, device); + oc_resource_bind_resource_type(col, "oic.wk.col"); + oc_resource_set_discoverable(col, true); + oc_resource_set_observable(col, true); + + oc_collection_add_supported_rt(col, "oic.r.switch.binary"); + oc_collection_add_mandatory_rt(col, "oic.r.switch.binary"); +#ifdef OC_COLLECTIONS_IF_CREATE + oc_resource_bind_resource_interface(col, OC_IF_CREATE); + oc_collections_add_rt_factory("oic.r.switch.binary", get_switch_instance, + free_switch_instance); +#endif /* OC_COLLECTIONS_IF_CREATE */ + /* The following enables baseline RETRIEVEs/UPDATEs to Collection properties + */ + oc_resource_set_properties_cbs(col, get_switches_properties, NULL, + set_switches_properties, NULL); + if (!oc_add_collection_v1(col)) { + printf("ERROR: could not register /switches collection\n"); + return false; + } + DPSCS_DBG("\tResources added to collection.\n"); + + if (oc_cloud_add_resource(col) < 0) { + printf("ERROR: could not publish /switches collection\n"); + return false; + } + DPSCS_DBG("\tCollection resource published.\n"); + return true; +} +#endif /* OC_COLLECTIONS */ + +static bool +register_con(size_t device) +{ + oc_resource_t *con_res = oc_core_get_resource_by_index(OCF_CON, device); + return oc_cloud_add_resource(con_res) == 0; +} + +static void +register_resources(void) +{ + if (!register_lights(g_device_id)) { + oc_abort("ERROR: could not register light\n"); + } +#ifdef OC_COLLECTIONS + if (!register_collection(g_device_id)) { + oc_abort("ERROR: could not register collection\n"); + } +#endif /* OC_COLLECTIONS */ + if (!register_con(g_device_id)) { + oc_abort("ERROR: could not register configuration resource\n"); + } + + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(g_device_id); + if (dps_ctx == NULL) { + return; + } + plgd_dps_set_configuration_resource(dps_ctx, + g_create_configuration_resource != 0); +} + +static void +display_device_uuid(size_t device_id) +{ + char buffer[OC_UUID_LEN]; + oc_uuid_to_str(oc_core_get_device_id(device_id), buffer, sizeof(buffer)); + printf("Started device with ID: %s\n", buffer); +} + +/************************************************************************** + * DPS + **************************************************************************/ + +static int +dps_read_pem(const char *file_path, char *buffer, size_t *buffer_size) +{ + FILE *file = fopen(file_path, "r"); + if (file == NULL) { + printf("ERROR: unable to open %s\n", file_path); + return -1; + } + if (fseek(file, 0, SEEK_END) != 0) { + goto error; + } + long pem_len = ftell(file); + if (pem_len < 0) { + goto error; + } + if ((size_t)pem_len >= *buffer_size) { + printf("ERROR: buffer provided too small\n"); + goto error; + } + if (fseek(file, 0, SEEK_SET) != 0) { + goto error; + } + if (fread(buffer, 1, pem_len, file) < (size_t)pem_len) { + goto error; + } + fclose(file); + buffer[pem_len] = '\0'; + *buffer_size = (size_t)pem_len; + return 0; + +error: + printf("ERROR: unable to read PEM\n"); + fclose(file); + return -1; +} + +static void +dps_concat_paths(char *buffer, size_t buffer_size, const char *cert_dir, + const char *file) +{ + memset(buffer, 0, buffer_size); + size_t cert_dir_len = strlen(cert_dir); + if (cert_dir_len >= buffer_size) { + abort(); + } + memcpy(buffer, cert_dir, cert_dir_len); + buffer[cert_dir_len] = '\0'; + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.strcpy) + strcat(buffer, file); +} + +/** + * @brief Add manufacturer's trusted root certificate authority and + * manufacturer's certificate to the device. + * + * @param dps_ctx device context (cannot be NULL) + * @param cert_dir path to directory with certificates (cannot be NULL) + * @return int 0 on success + * @return int -1 on failure + */ +static int +dps_add_certificates(const plgd_dps_context_t *dps_ctx, const char *cert_dir) +{ + assert(dps_ctx != NULL); + assert(cert_dir != NULL); +#define CERT_BUFFER_SIZE 4096 + + char path[PATH_MAX]; + int dpsca_credid = -1; + int mfg_credid = -1; + if (plgd_dps_get_skip_verify(dps_ctx) || g_dhcp_enabled) { + DPSCS_DBG("adding of manufacturer trusted root ca skipped\n"); + } else { + unsigned char dps_ca[CERT_BUFFER_SIZE]; + size_t dps_ca_size = sizeof(dps_ca) / sizeof(unsigned char); + dps_concat_paths(path, sizeof(path), cert_dir, "/dpsca.pem"); + if (dps_read_pem(path, (char *)dps_ca, &dps_ca_size) < 0) { + printf("ERROR: unable to read %s\n", path); + goto error; + } + dpsca_credid = oc_pki_add_mfg_trust_anchor(plgd_dps_get_device(dps_ctx), + dps_ca, dps_ca_size); + if (dpsca_credid < 0) { + printf("ERROR: installing manufacturer trusted root ca\n"); + goto error; + } + DPSCS_DBG("manufacturer trusted root ca credid=%d\n", dpsca_credid); + } + + unsigned char mfg_crt[CERT_BUFFER_SIZE]; + size_t mfg_crt_size = sizeof(mfg_crt) / sizeof(unsigned char); + dps_concat_paths(path, sizeof(path), cert_dir, "/mfgcrt.pem"); + if (dps_read_pem(path, (char *)mfg_crt, &mfg_crt_size) < 0) { + printf("ERROR: unable to read %s\n", path); + goto error; + } + unsigned char mfg_key[CERT_BUFFER_SIZE]; + size_t mfg_key_size = sizeof(mfg_key) / sizeof(unsigned char); + dps_concat_paths(path, sizeof(path), cert_dir, "/mfgkey.pem"); + if (dps_read_pem(path, (char *)mfg_key, &mfg_key_size) < 0) { + printf("ERROR: unable to read %s\n", path); + goto error; + } + mfg_credid = oc_pki_add_mfg_cert(plgd_dps_get_device(dps_ctx), mfg_crt, + mfg_crt_size, mfg_key, mfg_key_size); + if (mfg_credid < 0) { + printf("ERROR: installing manufacturer certificate\n"); + goto error; + } + DPSCS_DBG("manufacturer certificate credid=%d\n", mfg_credid); + oc_pki_set_security_profile(plgd_dps_get_device(dps_ctx), OC_SP_BLACK, + OC_SP_BLACK, mfg_credid); + return 0; + +error: + if (dpsca_credid != -1) { + if (oc_sec_remove_cred_by_credid(dpsca_credid, + plgd_dps_get_device(dps_ctx))) { + DPSCS_DBG("certificate(%d) removed\n", dpsca_credid); + } else { + printf("WARNING: failed to remove manufacturer trusted root ca(%d)\n", + dpsca_credid); + } + } + if (mfg_credid != -1) { + if (oc_sec_remove_cred_by_credid(mfg_credid, + plgd_dps_get_device(dps_ctx))) { + DPSCS_DBG("certificate(%d) removed\n", mfg_credid); + } else { + printf("WARNING: failed to remove manufacturer certificate(%d)\n", + mfg_credid); + } + } + return -1; +} + +/** + * @brief Setup the device to manufacturer's configuration. + * + * @param dps_ctx device context + * @return int 0 on success + * @return int -1 on failure + */ +static int +manufacturer_setup(plgd_dps_context_t *dps_ctx) +{ + // preserve name after factory reset + oc_device_info_t *dev = oc_core_get_device_info(plgd_dps_get_device(dps_ctx)); + if (dev != NULL) { + oc_free_string(&dev->name); + oc_new_string(&dev->name, g_dps_device_name, strlen(g_dps_device_name)); + } + plgd_dps_manager_callbacks_t callbacks = { + .on_status_change = dps_status_handler, + .on_status_change_data = NULL, + .on_cloud_status_change = cloud_status_handler, + .on_cloud_status_change_data = NULL, + }; + plgd_dps_set_manager_callbacks(dps_ctx, callbacks); + if (g_expiration_limit != -1) { + plgd_dps_pki_set_expiring_limit(dps_ctx, (uint16_t)g_expiration_limit); + } + if (g_observer_max_retry != -1) { + plgd_dps_set_cloud_observer_configuration(dps_ctx, + (uint8_t)g_observer_max_retry, 1); + } + plgd_dps_set_skip_verify(dps_ctx, g_skip_ca_verification != 0); + for (int i = 0; i < g_dps_endpoint_count; i++) { + size_t dps_endpoint_len = strlen(g_dps_endpoint[i]); + if (dps_endpoint_len > 0 && + !plgd_dps_add_endpoint_address(dps_ctx, g_dps_endpoint[i], + dps_endpoint_len, NULL, 0)) { + printf("ERROR: failed to add endpoint address\n"); + return -1; + } + } + if (dps_add_certificates(dps_ctx, g_dps_cert_dir) != 0) { + printf("ERROR: failed to add initial certificates on factory reset\n"); + return -1; + } + plgd_dps_force_reprovision(dps_ctx); + return 0; +} + +static int +try_start_dps(plgd_dps_context_t *ctx, plgd_dps_manager_callbacks_t callbacks) +{ + if (g_expiration_limit != -1) { + plgd_dps_pki_set_expiring_limit(ctx, (uint16_t)g_expiration_limit); + } + if (g_observer_max_retry != -1) { + plgd_dps_set_cloud_observer_configuration(ctx, + (uint8_t)g_observer_max_retry, 1); + } + plgd_dps_set_skip_verify(ctx, g_skip_ca_verification != 0); + plgd_dps_set_manager_callbacks(ctx, callbacks); + return plgd_dps_manager_start(ctx); +} + +static bool +dps_string_property_is_not_null(const oc_string_t *prop) +{ + return prop != NULL && oc_string(*prop) != NULL; +} + +static void +try_start_cloud(const plgd_dps_context_t *ctx) +{ + const oc_cloud_context_t *cloud_ctx = + oc_cloud_get_context(plgd_dps_get_device(ctx)); + if (cloud_ctx == NULL) { + return; + } + + if (oc_cloud_manager_is_started(cloud_ctx)) { + // already running + return; + } + + // check if cloud is configured to start + bool has_server = oc_cloud_get_server_uri(cloud_ctx) != NULL; + bool has_auth = dps_string_property_is_not_null( + oc_cloud_get_authorization_provider_name(cloud_ctx)); + bool has_access_token = + dps_string_property_is_not_null(oc_cloud_get_access_token(cloud_ctx)); + bool has_refresh_token = + dps_string_property_is_not_null(oc_cloud_get_refresh_token(cloud_ctx)); + bool has_token = has_access_token && has_refresh_token; + if (!has_server || (!has_auth && !has_token)) { + return; + } + if (!plgd_cloud_manager_start(ctx)) { + printf("ERROR: Failed to start cloud manager\n"); + return; + } +} + +#ifdef PLGD_DPS_FAKETIME + +static void +init_faketime(void) +{ + const char *faketime = getenv("FAKETIME"); + if (faketime == NULL) { + DPSCS_DBG("faketime disabled\n"); + g_faketime.enabled = false; + g_faketime.value[0] = '\0'; + g_faketime.format[0] = '\0'; + return; + } + g_faketime.enabled = true; + strncpy(g_faketime.value, faketime, sizeof(g_faketime.value) - 1); + DPSCS_DBG("faketime enabled time=%s\n", g_faketime.value); + + const char *fmt_env = getenv("FAKETIME_FMT"); + if (fmt_env == NULL) { + fmt_env = "%Y-%m-%d %T"; + } + strncpy(g_faketime.format, fmt_env, sizeof(g_faketime.format) - 1); + + const char *fake_monotonic_env = getenv("FAKETIME_DONT_FAKE_MONOTONIC"); + if (fake_monotonic_env == NULL || 0 != strcmp(fake_monotonic_env, "1")) { + printf( + "WARNING: monotonic time calculation calculated by faketime library\n"); + } +} + +/** @brief Parsing of env("FAKETIME") string understandable to libfaketime */ +static bool +parse_faketime(const char *faketime, const char *faketime_fmt, + struct timeval *tval) +{ + const char *start = faketime; + switch (faketime[0]) { + case '%': + case 'i': + case 'x': + // ignored options + DPSCS_DBG("ignoring faketime=%s\n", faketime); + return false; + case '@': + start = &faketime[1]; + break; + default: + break; + } + struct tm faketime_tm = { 0 }; + char *nstime_str = strptime(start, faketime_fmt, &faketime_tm); + if (nstime_str == NULL) { + return false; + } + + time_t tv_sec = mktime(&faketime_tm); + if (tv_sec == (time_t)-1) { + printf("ERROR: failed to parse faketime: %s\n", strerror(errno)); + return false; + } + tval->tv_sec = tv_sec; + tval->tv_usec = 0; + + if (nstime_str[0] == '.') { + double nstime = atof(--nstime_str); +#define USECS_IN_SEC 1000000 + tval->tv_usec = (long)((nstime - floor(nstime)) * USECS_IN_SEC); + } + return true; +} + +#ifdef PLGD_DPS_FAKETIME_SET_SYSTEM_TIME_ON_RESET +static void +set_faketime(void) +{ + if (!g_faketime.enabled) { + return; + } + + DPSCS_DBG("using faketime(%s) to set system time\n", g_faketime.value); + struct timeval tval = { 0 }; + if (!parse_faketime(g_faketime.value, g_faketime.format, &tval)) { + DPSCS_DBG("failed to parse faketime\n"); + return; + } + + DPSCS_DBG("set_faketime: sec=%ld usec=%ld\n", (long)tval.tv_sec, + (long)tval.tv_usec); + if (settimeofday(&tval, NULL) != 0) { + DPSCS_DBG("failed to set faketime\n"); + } +} +#endif /* PLGD_DPS_FAKETIME_SET_SYSTEM_TIME_ON_RESET */ + +#else /* !PLGD_DPS_FAKETIME */ + +static bool +is_root(void) +{ + return geteuid() == 0; +} + +#endif /* PLGD_DPS_FAKETIME */ + +static void +factory_presets_cb(size_t device_id, void *data) +{ + (void)data; + if (g_wait_for_reset != 0) { + DPSCS_DBG("skip factory reset handling: waiting for reset signal\n"); + return; + } + +#if defined(PLGD_DPS_FAKETIME) && \ + defined(PLGD_DPS_FAKETIME_SET_SYSTEM_TIME_ON_RESET) + set_faketime(); +#endif /* PLGD_DPS_FAKETIME && PLGD_DPS_FAKETIME_SET_SYSTEM_TIME_ON_RESET */ + + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(device_id); + if (dps_ctx == NULL) { + DPSCS_DBG("skip factory reset handling: empty context\n"); + return; + } + + if (plgd_dps_on_factory_reset(dps_ctx) != 0) { + printf("ERROR: cannot handle factory reset\n"); + return; + } + if (manufacturer_setup(dps_ctx) != 0) { + printf("ERROR: failed to configure device\n"); + return; + } + if (plgd_dps_manager_start(dps_ctx) != 0) { + printf("ERROR: failed to start dps manager\n"); + return; + } +} + +static char * +dirname(const char *exec_path) +{ + char *path = realpath(exec_path, NULL); + if (path == NULL) { + return NULL; + } + char *dir = strrchr(path, '/'); + if (dir == NULL) { + return NULL; + } + dir[0] = '\0'; + return path; +} + +static bool +is_directory(const char *path) +{ + struct stat statbuf; + if (stat(path, &statbuf) != 0) { + return false; + } + return S_ISDIR(statbuf.st_mode) != 0; +} + +typedef struct +{ + bool help; + uint8_t retry_configuration[PLGD_DPS_MAX_RETRY_VALUES_SIZE]; + size_t retry_configuration_size; + oc_log_level_t log_level; + oc_log_level_t oc_log_level; +} parse_options_result_t; + +static int +parse_string_to_int_array(char *str, uint8_t data[], size_t data_size) +{ + const char *token; + const char *delim = ","; + int size = 0; + while ((token = strtok_r(str, delim, &str)) != NULL) { + if ((size_t)size >= data_size || size > UINT8_MAX) { + return -1; + } + + char *eptr = NULL; + errno = 0; + long val = strtol(token, &eptr, 10); // NOLINT(readability-magic-numbers) + if (errno != 0 || eptr == token || (val <= 0 || val > UINT8_MAX)) { + return -1; + } + + data[size] = (uint8_t)val; + ++size; + } + return size; +} + +static int +parse_retry_configuration(const char *cfg, + parse_options_result_t *parsed_options) +{ + if (cfg == NULL || parsed_options == NULL) { + return -1; + } + +#ifdef OC_DYNAMIC_ALLOCATION + char *str = strdup(cfg); + if (str == NULL) { + return -1; + } +#else /* !OC_DYNAMIC_ALLOCATION */ + char str[64] = { 0 }; // NOLINT(readability-magic-numbers) + size_t cfg_len = strlen(cfg); + if (cfg_len >= sizeof(str)) { + return -1; + } + memcpy(str, cfg, cfg_len); + str[cfg_len] = 0; +#endif /* OC_DYNAMIC_ALLOCATION */ + + uint8_t data[sizeof(parsed_options->retry_configuration) / + sizeof(parsed_options->retry_configuration[0])] = { 0 }; + int size = + parse_string_to_int_array(str, data, sizeof(data) / sizeof(data[0])); + if (size == -1) { +#ifdef OC_DYNAMIC_ALLOCATION + free(str); +#endif /* OC_DYNAMIC_ALLOCATION */ + return -1; + } + + if (size > 0) { + memcpy(parsed_options->retry_configuration, data, size * sizeof(data[0])); + } +#ifdef OC_DYNAMIC_ALLOCATION + free(str); +#endif /* OC_DYNAMIC_ALLOCATION */ + return size; +} + +#define OPT_CREATE_CONF_RESOURCE "create-conf-resource" +#define OPT_EXPIRATION_LIMIT "expiration-limit" +#define OPT_HELP "help" +#define OPT_NO_VERIFY_CA "no-verify-ca" +#define OPT_CLOUD_OBSERVER_MAX_RETRY "cloud-observer-max-retry" +#define OPT_RETRY_CFG "retry-configuration" +#define OPT_WAIT_FOR_RESET "wait-for-reset" +#define OPT_DHCP_LEASE_FILE "dhcp-leases-file" +#define OPT_DHCP_ENABLED "dhcp-enabled" +#define OPT_SET_SYSTEM_TIME "set-system-time" +#define OPT_LOG_LEVEL "log-level" +#define OPT_OC_LOG_LEVEL "oc-log-level" +#define OPT_ENDPOINT "endpoint" + +#define OPT_ARG_DEVICE_NAME "device-name" +#define OPT_ARG_ENDPOINT "endpoint" + +#define OPT_OC_LOG_LEVEL_FLAG (256) + +static void +printhelp(const char *exec_path) +{ + const char *binary_name = strrchr(exec_path, '/'); + binary_name = binary_name != NULL ? binary_name + 1 : exec_path; + printf("./%s [%s] [%s]\n\n", binary_name, OPT_ARG_DEVICE_NAME, + OPT_ARG_ENDPOINT); + printf("OPTIONS:\n"); + printf(" -h | --%-26s print help\n", OPT_HELP); + printf(" -c | --%-26s create DPS configuration resource\n", + OPT_CREATE_CONF_RESOURCE); + printf(" -e | --%-26s set certificate expiration limit (in seconds)\n", + OPT_EXPIRATION_LIMIT); + printf(" -l | --%-26s set runtime log-level of the DPS library (supported " + "values: disabled, trace, debug, info, " + "notice, warning, error)\n", + OPT_LOG_LEVEL); + printf(" --%-26s set runtime log-level of the IoTivity library " + "(supported values: disabled, trace, debug, info, " + "notice, warning, error)\n", + OPT_OC_LOG_LEVEL); + printf(" -n | --%-26s skip loading of the DPS certificate authority\n", + OPT_NO_VERIFY_CA); + printf(" -f | --%-26s path to the dhcp leases file (default: " + "/var/lib/dhcp/dhclient.leases)\n", + OPT_DHCP_LEASE_FILE); + printf(" -x | --%-26s pull dhcp leases file every 5sec\n", OPT_DHCP_ENABLED); + printf(" -o | --%-26s maximal number of retries by cloud observer before " + "forcing reprovisioning\n", + OPT_CLOUD_OBSERVER_MAX_RETRY); + printf(" -r | --%-26s retry timeout configuration (array of non-zero values " + "delimited by ',', " + "maximum of %d values is accepted; example: 1,2,4,8,16)\n", + OPT_RETRY_CFG " [cfg]", PLGD_DPS_MAX_RETRY_VALUES_SIZE); + printf(" -s | --%-26s use plgd time to set system time (root required)\n", + OPT_SET_SYSTEM_TIME); + printf( + " -w | --%-26s don't start right away, but wait for SIGHUP signal\n\n", + OPT_WAIT_FOR_RESET); + printf(" -t | --%-26s additional endpoints for DPD (add multiple times for " + "multiple endpoints)\n", + OPT_ENDPOINT); + printf("ARGUMENTS:\n"); + printf(" %-33s name of the device (optional, default: dps)\n", + OPT_ARG_DEVICE_NAME); + printf(" %-33s address of the endpoint (optional, default: " + "coaps+tcp://127.0.0.1:20030)\n", + OPT_ARG_ENDPOINT); +} + +static int +make_storage(const char *storage_dir) +{ + if (oc_storage_config(storage_dir) != 0) { + return false; + } + errno = 0; + int ret = mkdir(storage_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + if (ret != 0 && ((EEXIST != errno) || !is_directory(storage_dir))) { + return false; + } + return true; +} + +static void +shutdown(size_t device_id) +{ + plgd_dps_shutdown(); + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(device_id); + if (cloud_ctx != NULL && (oc_cloud_manager_stop(cloud_ctx) != 0)) { + printf("ERROR: failed to stop cloud manager\n"); + } + oc_main_shutdown(); +} + +#define PLGD_VENDOR_ENCAPSULATED_OPTION_MAX_SIZE (512) + +typedef struct +{ + uint8_t value[PLGD_VENDOR_ENCAPSULATED_OPTION_MAX_SIZE]; + size_t size; +} dps_vendor_encapsulated_options_t; + +static bool +dps_dhcp_parse_vendor_encapsulated_options(const char *value, size_t size, + void *user_data) +{ + dps_vendor_encapsulated_options_t *veo = + (dps_vendor_encapsulated_options_t *)user_data; + ssize_t len = plgd_dps_hex_string_to_bytes(value, size, NULL, 0); + if (len < 0) { + printf("ERROR: invalid character in vendor encapsulated options\n"); + return true; + } + if (len > (ssize_t)(sizeof(veo->value))) { + printf("ERROR: vendor encapsulated options too long\n"); + return true; + } + len = + plgd_dps_hex_string_to_bytes(value, size, veo->value, sizeof(veo->value)); + if (len < (ssize_t)(sizeof(veo->value))) { + veo->value[len] = '\0'; + } + veo->size = (size_t)len; + return true; +} + +static oc_event_callback_retval_t +pull_vendor_encapsulated_options(void *data) +{ + DPSCS_DBG("pull vendor_encapsulated_options from dhcp leases file %s\n", + g_dhcp_leases_file); + if (g_dhcp_option_vendor_encapsulated_options == NULL) { + return OC_EVENT_DONE; + } + if (g_dhcp_leases_file == NULL) { + return OC_EVENT_DONE; + } + plgd_dps_context_t *dps_ctx = (plgd_dps_context_t *)data; + dps_vendor_encapsulated_options_t veo = { 0 }; + if (plgd_dps_dhcp_get_option_leases_file( + g_dhcp_leases_file, g_dhcp_option_vendor_encapsulated_options, + dps_dhcp_parse_vendor_encapsulated_options, &veo)) { + printf("ERROR: pull vendor_encapsulated_options: error during parsing\n"); + return OC_EVENT_CONTINUE; + } + plgd_dps_dhcp_set_values_t ret = + plgd_dps_dhcp_set_values_from_vendor_encapsulated_options( + dps_ctx, veo.value, veo.size); + switch (ret) { + case PLGD_DPS_DHCP_SET_VALUES_ERROR: + printf("ERROR: pull vendor_encapsulated_options: error during update\n"); + break; + case PLGD_DPS_DHCP_SET_VALUES_UPDATED: + DPSCS_DBG("pull vendor_encapsulated_options: updated but force " + "re-provision is not needed\n"); + break; + case PLGD_DPS_DHCP_SET_VALUES_NEED_REPROVISION: { + DPSCS_DBG("pull vendor_encapsulated_options: updated but needed force " + "re-provision and restart dps manager\n"); + plgd_dps_force_reprovision(dps_ctx); + if (plgd_dps_manager_restart(dps_ctx)) { + printf("ERROR: pull vendor_encapsulated_options: failed to restart dps " + "manager\n"); + } + } break; + case PLGD_DPS_DHCP_SET_VALUES_NOT_CHANGED: + DPSCS_DBG("pull vendor_encapsulated_options: no change\n"); + break; + default: + printf("ERROR: pull vendor_encapsulated_options: unknown return value %d\n", + ret); + break; + } + return OC_EVENT_CONTINUE; +} + +#define PLGD_VENDOR_ENCAPSULATED_OPTIONS_PULLING_INTERVAL (5) + +static oc_event_callback_retval_t +init_pull_vendor_encapsulated_options(void *data) +{ + pull_vendor_encapsulated_options(data); + plgd_dps_context_t *dps_ctx = (plgd_dps_context_t *)data; + oc_set_delayed_callback(dps_ctx, pull_vendor_encapsulated_options, + PLGD_VENDOR_ENCAPSULATED_OPTIONS_PULLING_INTERVAL); + return OC_EVENT_DONE; +} + +static bool +parse_positive_integer_value(const char *str, long *value) +{ + char *eptr = NULL; + errno = 0; + long val = strtol(str, &eptr, 10); // NOLINT(readability-magic-numbers) + if (errno != 0 || eptr == str || (*eptr) != '\0' || val < 0) { + return false; + } + *value = val; + return true; +} + +static bool +parse_oc_log_level(const char *log_level, oc_log_level_t *level) +{ + const char *levels_str[] = { + "trace", "debug", "info", "notice", "warning", "error", "disabled", + }; + oc_log_level_t levels[] = { + OC_LOG_LEVEL_TRACE, OC_LOG_LEVEL_DEBUG, OC_LOG_LEVEL_INFO, + OC_LOG_LEVEL_NOTICE, OC_LOG_LEVEL_WARNING, OC_LOG_LEVEL_ERROR, + OC_LOG_LEVEL_DISABLED, + }; + + for (size_t i = 0; i < sizeof(levels_str) / sizeof(levels_str[0]); ++i) { + if (strcmp(log_level, levels_str[i]) == 0) { + *level = levels[i]; + return true; + } + } + return false; +} + +static bool +add_endpoint(const char *endpoint) +{ +#if OC_DYNAMIC_ALLOCATION + char **new_dps_endpoint_buffer = (char **)realloc( + g_dps_endpoint, (g_dps_endpoint_count + 1) * sizeof(char *)); + if (new_dps_endpoint_buffer == NULL) { + printf("ERROR: failed to allocate memory for list of endpoints\n"); + return false; + } + g_dps_endpoint = new_dps_endpoint_buffer; + g_dps_endpoint[g_dps_endpoint_count] = strdup(endpoint); + if (g_dps_endpoint[g_dps_endpoint_count] == NULL) { + printf("ERROR: failed to allocate memory for endpoint\n"); + return false; + } +#else /* !OC_DYNAMIC_ALLOCATION */ + if (g_dps_endpoint_count == 2) { + printf("ERROR: cannot add more than 2 endpoints static allocation\n"); + return false; + } + size_t endpoint_len = strlen(endpoint); + if (sizeof(g_dps_endpoint[0]) <= endpoint_len) { + printf("ERROR: endpoint address too long\n"); + return false; + } + memcpy(g_dps_endpoint, endpoint, endpoint_len); + g_dps_endpoint[g_dps_endpoint_count][endpoint_len] = '\0'; +#endif /* OC_DYNAMIC_ALLOCATION */ + ++g_dps_endpoint_count; + return true; +} + +static bool +parse_option(int opt, char *argv[], parse_options_result_t *parsed_options) +{ + switch (opt) { + case 'c': + g_create_configuration_resource = 1; + return true; + case 'e': { + long expiration_limit = 0; + if (!parse_positive_integer_value(optarg, &expiration_limit) || + expiration_limit > UINT16_MAX) { + printf("invalid expiration limit argument value(%s)\n", optarg); + return false; + } + g_expiration_limit = (int)expiration_limit; + return true; + } + case 'l': + if (!parse_oc_log_level(optarg, &parsed_options->log_level)) { + printf("invalid log-level (%s)\n", optarg); + return false; + } + return true; + case 'n': + g_skip_ca_verification = 1; + return true; + case 'o': { + long observer_max_retry = 0; + if (!parse_positive_integer_value(optarg, &observer_max_retry) || + observer_max_retry > UINT8_MAX) { + printf("invalid observer max retry count argument value(%s)\n", optarg); + return false; + } + g_observer_max_retry = (int16_t)observer_max_retry; + return true; + } + case 'r': { + int size = parse_retry_configuration(optarg, parsed_options); + if (size < 1) { + printf("invalid retry configuration(%s)\n", optarg); + return false; + } + parsed_options->retry_configuration_size = (size_t)size; + return true; + } + case 's': +#ifndef PLGD_DPS_FAKETIME // intercepted settimeofday by faketime doesn't need + // root + if (!is_root()) { + printf("root required for settimeofday: see man settimeofday\n"); + return false; + } +#endif /* !PLGD_DPS_FAKETIME */ + g_set_system_time = true; + return true; + case 'w': + g_wait_for_reset = 1; + return true; + case 'f': + g_dhcp_leases_file = optarg; + return true; + case 'x': + g_dhcp_enabled = true; + return true; + case OPT_OC_LOG_LEVEL_FLAG: + if (!parse_oc_log_level(optarg, &parsed_options->oc_log_level)) { + printf("invalid oc-log-level (%s)\n", optarg); + return false; + } + return true; + case 't': + return add_endpoint(optarg); + default: + break; + } + + printf("invalid option(%s)\n", argv[optind]); + return false; +} + +static bool +parse_options(int argc, char *argv[], parse_options_result_t *parsed_options) +{ + static struct option long_options[] = { + { OPT_CREATE_CONF_RESOURCE, no_argument, &g_create_configuration_resource, + 'c' }, + { OPT_EXPIRATION_LIMIT, required_argument, NULL, 'e' }, + { OPT_HELP, no_argument, NULL, 'h' }, + { OPT_LOG_LEVEL, required_argument, NULL, 'l' }, + { OPT_OC_LOG_LEVEL, required_argument, NULL, OPT_OC_LOG_LEVEL_FLAG }, + { OPT_NO_VERIFY_CA, no_argument, &g_skip_ca_verification, 'n' }, + { OPT_CLOUD_OBSERVER_MAX_RETRY, required_argument, NULL, 'o' }, + { OPT_RETRY_CFG, required_argument, NULL, 'r' }, + { OPT_SET_SYSTEM_TIME, no_argument, NULL, 's' }, + { OPT_WAIT_FOR_RESET, no_argument, &g_wait_for_reset, 'w' }, + { OPT_DHCP_LEASE_FILE, required_argument, NULL, 'f' }, + { OPT_DHCP_ENABLED, no_argument, NULL, 'x' }, + { OPT_ENDPOINT, required_argument, NULL, 't' }, + { NULL, 0, NULL, 0 }, + }; + while (true) { + int option_index = 0; + int opt = getopt_long(argc, argv, "ce:f:hl:nr:o:sxwt:", long_options, + &option_index); + if (opt == -1) { + break; + } + switch (opt) { + case 0: + if (long_options[option_index].flag != 0) { + break; + } + printf("invalid option(%s)\n", argv[optind]); + return false; + case 'h': + printhelp(argv[0]); + parsed_options->help = true; + return true; + default: + if (!parse_option(opt, argv, parsed_options)) { + return false; + } + break; + } + } + argc -= (optind - 1); + for (int i = 1; i < argc; ++i, ++optind) { + argv[i] = argv[optind]; + } + + if (argc == 1) { + printf("./dps_cloud_server \n"); + printf("Default parameters:\n" + "\tdevice_name: %s\n", + g_dps_device_name); + } + if (argc > 1) { + g_dps_device_name = argv[1]; + printf("device_name: %s\n", argv[1]); + } + if (argc > 2) { +#ifdef OC_DYNAMIC_ALLOCATION + free(g_dps_endpoint[0]); + g_dps_endpoint[0] = strdup(argv[2]); + if (g_dps_endpoint[0] == NULL) { + printf("ERROR: failed to allocate memory for endpoint\n"); + return false; + } +#else /* !OC_DYNAMIC_ALLOCATION */ + size_t endpoint_len = strlen(argv[2]); + if (sizeof(g_dps_endpoint[0]) <= endpoint_len) { + printf("ERROR: endpoint address too long\n"); + return false; + } + memcpy(g_dps_endpoint[0], argv[2], endpoint_len); + g_dps_endpoint[0][endpoint_len] = '\0'; +#endif /* OC_DYNAMIC_ALLOCATION */ + } + printf("Endpoints:\n"); + for (int i = 0; i < g_dps_endpoint_count; ++i) { + printf("\t%s\n", g_dps_endpoint[i]); + } + + char *dir = dirname(argv[0]); + if (dir == NULL) { + printf("ERROR: failed to resolve parent directory\n"); + return false; + } + dps_concat_paths(g_dps_cert_dir, sizeof(g_dps_cert_dir), dir, "/pki_certs"); + free(dir); + +#ifdef PLGD_DPS_FAKETIME + init_faketime(); +#endif /* PLGD_DPS_FAKETIME */ + + return true; +} + +int +main(int argc, char *argv[]) +{ + parse_options_result_t parsed_options = { + .help = false, + .retry_configuration = { 0 }, + .retry_configuration_size = 0, + .log_level = OC_LOG_LEVEL_INFO, + .oc_log_level = OC_LOG_LEVEL_INFO, + }; + if (!add_endpoint( + "coaps+tcp://127.0.0.1:20030")) { // NOLINT(readability-magic-numbers) + return -1; + } + if (!parse_options(argc, argv, &parsed_options)) { + return -1; + } + if (parsed_options.help) { + return 0; + } + + oc_log_set_level(parsed_options.oc_log_level); + plgd_dps_log_set_level(parsed_options.log_level); + + if (!init()) { + return -1; + } + +#define DPS_STORAGE "./dps_cloud_server_creds/" + if (!make_storage(DPS_STORAGE)) { + printf("ERROR: failed to create storage at path %s", DPS_STORAGE); + deinit(); + return -1; + } + oc_set_factory_presets_cb(factory_presets_cb, NULL); + +#ifdef OC_DYNAMIC_ALLOCATION + const size_t max_app_data_size = (size_t)(8 * 1024); + oc_set_max_app_data_size(max_app_data_size); + const size_t min_app_data_size = 512; + oc_set_min_app_data_size(min_app_data_size); +#endif /* OC_DYNAMIC_ALLOCATION */ +#if defined(OC_SECURITY) && defined(OC_PKI) + oc_sec_certs_md_set_algorithms_allowed( + MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA256) | + MBEDTLS_X509_ID_FLAG(MBEDTLS_MD_SHA384)); + oc_sec_certs_ecp_set_group_ids_allowed( + MBEDTLS_X509_ID_FLAG(MBEDTLS_ECP_DP_SECP256R1) | + MBEDTLS_X509_ID_FLAG(MBEDTLS_ECP_DP_SECP384R1)); +#endif /* OC_SECURITY && OC_PKI */ + static const oc_handler_t handler = { + .init = app_init, + .signal_event_loop = signal_event_loop, + .register_resources = register_resources, + }; + if (oc_main_init(&handler) < 0) { + deinit(); + return -1; + } + g_initialized = true; + display_device_uuid(g_device_id); + plgd_dps_context_t *dps_ctx = plgd_dps_get_context(g_device_id); + if (dps_ctx == NULL) { + printf("ERROR: cannot start dps manager: empty context\n"); + shutdown(g_device_id); + deinit(); + return -1; + } + + if (g_dhcp_enabled) { + oc_set_delayed_callback(dps_ctx, init_pull_vendor_encapsulated_options, 0); + } + + if (parsed_options.retry_configuration_size > 0 && + !plgd_dps_set_retry_configuration( + dps_ctx, parsed_options.retry_configuration, + parsed_options.retry_configuration_size)) { + printf("ERROR: cannot start dps manager: invalid retry configuration\n"); + shutdown(g_device_id); + deinit(); + return -1; + } + + if (g_wait_for_reset != 0) { + run(); + shutdown(g_device_id); + deinit(); + return 0; + } + + if (!plgd_dps_manager_is_started(dps_ctx)) { + plgd_dps_manager_callbacks_t callbacks = { + .on_status_change = dps_status_handler, + .on_status_change_data = NULL, + .on_cloud_status_change = cloud_status_handler, + .on_cloud_status_change_data = NULL, + }; + if (try_start_dps(dps_ctx, callbacks) != 0) { + printf("ERROR: failed to start dps manager\n"); + shutdown(g_device_id); + deinit(); + return -1; + } + + oc_cloud_context_t *cloud_ctx = oc_cloud_get_context(g_device_id); + if (cloud_ctx != NULL) { + // setup callbacks in case cloud gets configured later + oc_cloud_set_on_status_change(cloud_ctx, + (oc_cloud_on_status_change_t){ + callbacks.on_cloud_status_change, + callbacks.on_cloud_status_change_data, + }); + } + if (!plgd_dps_manager_is_started(dps_ctx) && + plgd_dps_endpoint_is_empty(dps_ctx)) { + try_start_cloud(dps_ctx); + } + } + + run(); + shutdown(g_device_id); + deinit(); + return 0; +} diff --git a/docker/apps/Dockerfile.cloud-server b/docker/apps/Dockerfile.cloud-server index 43c6f713a3..9f33c9677a 100644 --- a/docker/apps/Dockerfile.cloud-server +++ b/docker/apps/Dockerfile.cloud-server @@ -1,4 +1,4 @@ -FROM alpine:3.19 AS build +FROM alpine:3.20 AS build ARG BUILD_TYPE=Release ARG BUILD_ARGS RUN apk add --no-cache cmake curl git build-base gcc linux-headers patch perl python3 @@ -17,7 +17,7 @@ RUN git clone https://github.com/wolfcw/libfaketime.git && \ cd /libfaketime/src && \ make install FAKETIME_COMPILE_CFLAGS="-DFAKE_SETTIME -DFAKE_STATELESS -D__USE_LARGEFILE64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE" -FROM alpine:3.19 AS service +FROM alpine:3.20 AS service RUN apk add --no-cache bash COPY --from=build /iotivity-lite/build/apps/cloud_server /iotivity-lite/port/linux/service COPY --from=build /usr/local/lib/faketime/libfaketimeMT.so.1 /usr/local/lib/faketime/libfaketimeMT.so.1 diff --git a/docker/apps/Dockerfile.dps-cloud-server b/docker/apps/Dockerfile.dps-cloud-server new file mode 100644 index 0000000000..cc72b54880 --- /dev/null +++ b/docker/apps/Dockerfile.dps-cloud-server @@ -0,0 +1,33 @@ +FROM ubuntu:22.04 AS build +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential \ + ca-certificates clang-15 cmake g++ gcc git python3 && \ + apt-get clean && \ + update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 10 && \ + update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-15 10 +ARG BUILD_TYPE=Release +ARG BUILD_ARGS= +RUN mkdir /device-provisioning-client +COPY . /device-provisioning-client +WORKDIR /device-provisioning-client +RUN ls -l . && \ + mkdir build && \ + cd build && \ + cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DOC_CLOUD_ENABLED=ON \ + -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DPLGD_DEV_TIME_ENABLED=ON \ + -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON \ + ${BUILD_ARGS} .. && \ + cmake --build . --target mbedtls mbedx509 mbedcrypto && \ + cmake --build . -j$(nproc) --target dps_cloud_server + +FROM ubuntu:22.04 AS service +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gcc adduser \ + && apt-get clean +COPY --from=build /device-provisioning-client/build/apps/dps_cloud_server /dps/dps_cloud_server +COPY --from=build /device-provisioning-client/build/libiotivity-lite-client-server.so* /dps/ +RUN adduser nonroot && \ + chown -R nonroot:nonroot /dps +USER nonroot +WORKDIR /dps +ENTRYPOINT [ "./dps_cloud_server" ] diff --git a/docker/apps/Dockerfile.dps-cloud-server-debug b/docker/apps/Dockerfile.dps-cloud-server-debug new file mode 100644 index 0000000000..26aaef24b7 --- /dev/null +++ b/docker/apps/Dockerfile.dps-cloud-server-debug @@ -0,0 +1,40 @@ +FROM ubuntu:22.04 AS service +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends adduser build-essential \ + ca-certificates cmake g++ gcc gcovr git python3 && \ + apt-get clean +ARG BUILD_TYPE=Debug +ARG BUILD_ARGS= +# install libfaketime +RUN git clone https://github.com/wolfcw/libfaketime.git && \ + cd /libfaketime/src && \ + make install FAKETIME_COMPILE_CFLAGS="-DFAKE_SETTIME" +WORKDIR / +RUN mkdir /device-provisioning-client +COPY . /device-provisioning-client +RUN adduser nonroot && \ + chown -R nonroot:nonroot /device-provisioning-client && \ + mkdir /dps && \ + chown nonroot:nonroot /dps +USER nonroot +WORKDIR /device-provisioning-client +RUN mkdir build && \ + cd build && \ + cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DOC_CLOUD_ENABLED=ON \ + -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON \ + -DOC_DEBUG_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DPLGD_DEV_DEVICE_PROVISIONING_ENABLED=ON \ + -DPLGD_DEV_DEVICE_PROVISIONING_TEST_PROPERTIES_ENABLED=ON -DBUILD_TESTING=ON ${BUILD_ARGS} .. && \ + cmake --build . --target mbedtls mbedx509 mbedcrypto && \ + cmake --build . -j$(nproc) --target dps_cloud_server && \ + cp /device-provisioning-client/build/apps/dps_cloud_server /dps/ + +WORKDIR /dps +ENV LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 +ENV FAKETIME="@2011-01-01 12:00:00" +ENV FAKETIME_DONT_FAKE_MONOTONIC=1 +ENV FAKETIME_TIMESTAMP_FILE= +ENV FAKETIME_UPDATE_TIMESTAMP_FILE= +ENV FAKETIME_DONT_RESET= +ENV FAKETIME_NO_CACHE= +ENV FAKETIME_CACHE_DURATION= +ENTRYPOINT [ "./dps_cloud_server" ] diff --git a/include/oc_log.h b/include/oc_log.h index 7a38134a49..ee0d92a2bf 100644 --- a/include/oc_log.h +++ b/include/oc_log.h @@ -36,6 +36,7 @@ extern "C" { #include "oc_config.h" #include "oc_export.h" #include "util/oc_compiler.h" +#include "util/oc_features.h" #include #include @@ -166,6 +167,9 @@ typedef enum { OC_LOG_COMPONENT_CLOUD = 1 << 1, ///< cloud #endif OC_LOG_COMPONENT_COAP = 1 << 2, ///< coap +#ifdef OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING + OC_LOG_COMPONENT_DEVICE_PROVISIONING = 1 << 3, ///< device provisioning +#endif } oc_log_component_t; /** diff --git a/include/plgd/plgd_dps.h b/include/plgd/plgd_dps.h new file mode 100644 index 0000000000..d53fcd9677 --- /dev/null +++ b/include/plgd/plgd_dps.h @@ -0,0 +1,859 @@ +/**************************************************************************** + * + * 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 + +/** + * @brief Custom logging function + * + * @param level log level 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, const char *file, + int line, const char *func_name, + const char *format, ...) + OC_PRINTF_FORMAT(5, 6) 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); + +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_pstat.c b/security/oc_pstat.c index 988345c463..7403d902e4 100644 --- a/security/oc_pstat.c +++ b/security/oc_pstat.c @@ -444,7 +444,7 @@ oc_sec_get_pstat(size_t device) bool oc_sec_is_operational(size_t device) { - return g_pstat[device].isop; + return oc_sec_get_pstat(device)->isop; } bool @@ -456,7 +456,7 @@ oc_sec_pstat_is_in_dos_state(const oc_sec_pstat_t *ps, unsigned dos_mask) bool oc_device_is_in_dos_state(size_t device, unsigned dos_mask) { - return oc_sec_pstat_is_in_dos_state(&g_pstat[device], dos_mask); + return oc_sec_pstat_is_in_dos_state(oc_sec_get_pstat(device), dos_mask); } void 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/sonar-project.properties b/sonar-project.properties index 1630b96740..eecfec2008 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,8 +13,6 @@ sonar.exclusions=apps/**,deps/**,patches/**,tests/**,tools/**,**/*.java,**/*.py, # Encoding of the source code. Default is default system encoding #sonar.sourceEncoding=UTF-8 -sonar.cfamily.build-wrapper-output=build_wrapper_output_directory - sonar.python.version=3 sonar.coverageReportPaths=tools/coverage.xml diff --git a/tools/doxygen.ini b/tools/doxygen.ini index 2c017bc899..a024282583 100644 --- a/tools/doxygen.ini +++ b/tools/doxygen.ini @@ -2239,8 +2239,6 @@ PREDEFINED = OC_API \ OC_HAS_FEATURE_PUSH \ OC_RESOURCE_ACCESS_IN_RFOTM \ OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM \ - PLGD_DEV_TIME \ - OC_HAS_FEATURE_PLGD_TIME \ OC_ETAG \ OC_HAS_FEATURE_ETAG \ OC_HAS_FEATURE_ETAG_INCREMENTAL_CHANGES \ @@ -2249,7 +2247,11 @@ PREDEFINED = OC_API \ OC_SIMPLE_MAIN_LOOP \ OC_HAS_FEATURE_LOOP_EVENT \ OC_HAS_FEATURE_SIMPLE_MAIN_LOOP \ - OC_HAS_FEATURE_ENDPOINT_ADDRESS_LIST + OC_HAS_FEATURE_ENDPOINT_ADDRESS_LIST \ + PLGD_DEV_TIME \ + OC_HAS_FEATURE_PLGD_TIME \ + PLGD_DEV_DEVICE_PROVISIONING \ + OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/tools/utils.cmake b/tools/utils.cmake new file mode 100644 index 0000000000..cc502c8899 --- /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") OR ("${level}" STREQUAL "")) + 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_features.h b/util/oc_features.h index 4b7b67e28d..a24808d6cf 100644 --- a/util/oc_features.h +++ b/util/oc_features.h @@ -88,4 +88,10 @@ #define OC_HAS_FEATURE_ENDPOINT_ADDRESS_LIST #endif /* OC_CLOUD */ +#if defined(PLGD_DEV_DEVICE_PROVISIONING) && defined(OC_SERVER) && \ + defined(OC_CLIENT) && defined(OC_HAS_FEATURE_PLGD_TIME) && \ + defined(OC_SECURITY) +#define OC_HAS_FEATURE_PLGD_DEVICE_PROVISIONING +#endif /* PLGD_DEV_DEVICE_PROVISIONING */ + #endif /* OC_FEATURES_H */ 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 }