From 9e56efda0a3ac73eba5d0361a513e5ebc9d8113e Mon Sep 17 00:00:00 2001 From: Dane Walton Date: Tue, 25 Oct 2022 15:03:44 -0700 Subject: [PATCH 1/3] Add ADU capabilities to feature branch (#27) --- .github/workflows/main.yml | 5 +- README.md | 10 +- .../Azure_IoT_Adu_ESP32/AzIoTSasToken.cpp | 288 +++++ examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.h | 32 + .../Azure_IoT_Adu_ESP32.ino | 1054 +++++++++++++++++ examples/Azure_IoT_Adu_ESP32/SampleAduJWS.cpp | 970 +++++++++++++++ examples/Azure_IoT_Adu_ESP32/SampleAduJWS.h | 57 + examples/Azure_IoT_Adu_ESP32/SerialLogger.cpp | 63 + examples/Azure_IoT_Adu_ESP32/SerialLogger.h | 23 + examples/Azure_IoT_Adu_ESP32/docs/lib.png | Bin 0 -> 41495 bytes .../Azure_IoT_Adu_ESP32/docs/tagged-twin.png | Bin 0 -> 57973 bytes examples/Azure_IoT_Adu_ESP32/iot_configs.h | 73 ++ examples/Azure_IoT_Adu_ESP32/readme.md | 272 +++++ library.properties | 20 +- src/az_base64.c | 425 +++---- src/az_base64.h | 35 + src/az_iot.h | 1 + src/az_iot_adu_client.c | 872 ++++++++++++++ src/az_iot_adu_client.h | 620 ++++++++++ src/az_iot_common.h | 3 + src/az_iot_hub_client_properties.c | 5 - src/az_json.h | 27 + src/az_json_private.h | 18 + src/az_json_reader.c | 18 - src/az_json_token.c | 56 + src/az_span.h | 13 +- src/az_version.h | 9 +- src/azure_ca.h | 440 +++---- tools/Update-Library.ps1 | 7 +- 29 files changed, 4856 insertions(+), 560 deletions(-) create mode 100644 examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.cpp create mode 100644 examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.h create mode 100644 examples/Azure_IoT_Adu_ESP32/Azure_IoT_Adu_ESP32.ino create mode 100644 examples/Azure_IoT_Adu_ESP32/SampleAduJWS.cpp create mode 100644 examples/Azure_IoT_Adu_ESP32/SampleAduJWS.h create mode 100644 examples/Azure_IoT_Adu_ESP32/SerialLogger.cpp create mode 100644 examples/Azure_IoT_Adu_ESP32/SerialLogger.h create mode 100644 examples/Azure_IoT_Adu_ESP32/docs/lib.png create mode 100644 examples/Azure_IoT_Adu_ESP32/docs/tagged-twin.png create mode 100644 examples/Azure_IoT_Adu_ESP32/iot_configs.h create mode 100644 examples/Azure_IoT_Adu_ESP32/readme.md create mode 100644 src/az_iot_adu_client.c create mode 100644 src/az_iot_adu_client.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index afc4b5ef..6b92be02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, feature/* ] workflow_dispatch: @@ -31,3 +31,6 @@ jobs: - name: Build Azure_IoT_Central_ESP32 run: arduino --verify --board esp32:esp32:esp32 -v --preserve-temp-files $GITHUB_WORKSPACE/examples/Azure_IoT_Central_ESP32/Azure_IoT_Central_ESP32.ino + + - name: Build Azure_IoT_Adu_ESP32 + run: arduino --verify --board esp32:esp32:esp32 -v --preserve-temp-files $GITHUB_WORKSPACE/examples/Azure_IoT_Adu_ESP32/Azure_IoT_Adu_ESP32.ino diff --git a/README.md b/README.md index 7ad90fca..37e1de63 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,18 @@ This library package contains the following samples. Please refer to their docum - [Azure IoT Central ESPRESSIF ESP32](examples/Azure_IoT_Central_ESP32/readme.md) -- [Azure IoT Hub ESPRESSIF ESP-8266](examples/Azure_IoT_Hub_ESP8266/readme.md) +- [Azure IoT Hub ESPRESSIF ESP8266](examples/Azure_IoT_Hub_ESP8266/readme.md) -- [Azure IoT Hub ESPRESSIF ESP-32](examples/Azure_IoT_Hub_ESP32/readme.md) +- [Azure IoT Hub ESPRESSIF ESP32](examples/Azure_IoT_Hub_ESP32/readme.md) - [Azure IoT Hub Realtek AmebaD](examples/Azure_IoT_Hub_RealtekAmebaD/readme.md) +- [Azure IoT Device Update ESP32](examples/Azure_IoT_Adu_ESP32/readme.md) + What is the difference between **IoT Hub** and **IoT Central** samples? -1. IoT Hub samples will get devices connected directly to [Azure IoT Hub](https://docs.microsoft.com/en-us/azure/iot-hub/iot-concepts-and-iot-hub) -1. IoT Central samples will leverage DPS ([Device Provisioning Service](https://docs.microsoft.com/en-us/azure/iot-dps/about-iot-dps)) to provision the device and then connect it to [Azure IoT Central](https://docs.microsoft.com/en-us/azure/iot-central/core/overview-iot-central). +1. IoT Hub samples will get devices connected directly to [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-concepts-and-iot-hub) +2. IoT Central samples will leverage DPS ([Device Provisioning Service](https://docs.microsoft.com/azure/iot-dps/about-iot-dps)) to provision the device and then connect it to [Azure IoT Central](https://docs.microsoft.com/azure/iot-central/core/overview-iot-central). Please note that provisioning through DPS is mandatory for IoT Central scenarios, but DPS can also be used for IoT Hub devices as well. diff --git a/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.cpp b/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.cpp new file mode 100644 index 00000000..28f0927f --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.cpp @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "AzIoTSasToken.h" +#include "SerialLogger.h" +#include +#include +#include +#include +#include +#include + +#define INDEFINITE_TIME ((time_t)-1) + +#define az_span_is_content_equal(x, AZ_SPAN_EMPTY) \ + (az_span_size(x) == az_span_size(AZ_SPAN_EMPTY) && az_span_ptr(x) == az_span_ptr(AZ_SPAN_EMPTY)) + +static uint32_t getSasTokenExpiration(const char* sasToken) +{ + const char SE[] = { '&', 's', 'e', '=' }; + uint32_t se_as_unix_time = 0; + + int i, j; + for (i = 0, j = 0; sasToken[i] != '\0'; i++) + { + if (sasToken[i] == SE[j]) + { + j++; + if (j == sizeof(SE)) + { + // i is still at the '=' position. We must advance it by 1. + i++; + break; + } + } + else + { + j = 0; + } + } + + if (j != sizeof(SE)) + { + Logger.Error("Failed finding `se` field in SAS token"); + } + else + { + int k = i; + while (sasToken[k] != '\0' && sasToken[k] != '&') + { + k++; + } + + if (az_result_failed( + az_span_atou32(az_span_create((uint8_t*)sasToken + i, k - i), &se_as_unix_time))) + { + Logger.Error("Failed parsing SAS token expiration timestamp"); + } + } + + return se_as_unix_time; +} + +static void mbedtls_hmac_sha256(az_span key, az_span payload, az_span signed_payload) +{ + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, (const unsigned char*)az_span_ptr(key), az_span_size(key)); + mbedtls_md_hmac_update(&ctx, (const unsigned char*)az_span_ptr(payload), az_span_size(payload)); + mbedtls_md_hmac_finish(&ctx, (byte*)az_span_ptr(signed_payload)); + mbedtls_md_free(&ctx); +} + +static void hmac_sha256_sign_signature( + az_span decoded_key, + az_span signature, + az_span signed_signature, + az_span* out_signed_signature) +{ + mbedtls_hmac_sha256(decoded_key, signature, signed_signature); + *out_signed_signature = az_span_slice(signed_signature, 0, 32); +} + +static void base64_encode_bytes( + az_span decoded_bytes, + az_span base64_encoded_bytes, + az_span* out_base64_encoded_bytes) +{ + size_t len; + if (mbedtls_base64_encode( + az_span_ptr(base64_encoded_bytes), + (size_t)az_span_size(base64_encoded_bytes), + &len, + az_span_ptr(decoded_bytes), + (size_t)az_span_size(decoded_bytes)) + != 0) + { + Logger.Error("mbedtls_base64_encode fail"); + } + + *out_base64_encoded_bytes = az_span_create(az_span_ptr(base64_encoded_bytes), (int32_t)len); +} + +static int decode_base64_bytes( + az_span base64_encoded_bytes, + az_span decoded_bytes, + az_span* out_decoded_bytes) +{ + memset(az_span_ptr(decoded_bytes), 0, (size_t)az_span_size(decoded_bytes)); + + size_t len; + if (mbedtls_base64_decode( + az_span_ptr(decoded_bytes), + (size_t)az_span_size(decoded_bytes), + &len, + az_span_ptr(base64_encoded_bytes), + (size_t)az_span_size(base64_encoded_bytes)) + != 0) + { + Logger.Error("mbedtls_base64_decode fail"); + return 1; + } + else + { + *out_decoded_bytes = az_span_create(az_span_ptr(decoded_bytes), (int32_t)len); + return 0; + } +} + +static int iot_sample_generate_sas_base64_encoded_signed_signature( + az_span sas_base64_encoded_key, + az_span sas_signature, + az_span sas_base64_encoded_signed_signature, + az_span* out_sas_base64_encoded_signed_signature) +{ + // Decode the sas base64 encoded key to use for HMAC signing. + char sas_decoded_key_buffer[32]; + az_span sas_decoded_key = AZ_SPAN_FROM_BUFFER(sas_decoded_key_buffer); + + if (decode_base64_bytes(sas_base64_encoded_key, sas_decoded_key, &sas_decoded_key) != 0) + { + Logger.Error("Failed generating encoded signed signature"); + return 1; + } + + // HMAC-SHA256 sign the signature with the decoded key. + char sas_hmac256_signed_signature_buffer[32]; + az_span sas_hmac256_signed_signature = AZ_SPAN_FROM_BUFFER(sas_hmac256_signed_signature_buffer); + hmac_sha256_sign_signature( + sas_decoded_key, sas_signature, sas_hmac256_signed_signature, &sas_hmac256_signed_signature); + + // Base64 encode the result of the HMAC signing. + base64_encode_bytes( + sas_hmac256_signed_signature, + sas_base64_encoded_signed_signature, + out_sas_base64_encoded_signed_signature); + + return 0; +} + +int64_t iot_sample_get_epoch_expiration_time_from_minutes(uint32_t minutes) +{ + time_t now = time(NULL); + return (int64_t)(now + minutes * 60); +} + +az_span generate_sas_token( + az_iot_hub_client* hub_client, + az_span device_key, + az_span sas_signature, + unsigned int expiryTimeInMinutes, + az_span sas_token) +{ + az_result rc; + // Create the POSIX expiration time from input minutes. + uint64_t sas_duration = iot_sample_get_epoch_expiration_time_from_minutes(expiryTimeInMinutes); + + // Get the signature that will later be signed with the decoded key. + // az_span sas_signature = AZ_SPAN_FROM_BUFFER(signature); + rc = az_iot_hub_client_sas_get_signature(hub_client, sas_duration, sas_signature, &sas_signature); + if (az_result_failed(rc)) + { + Logger.Error("Could not get the signature for SAS key: az_result return code " + rc); + return AZ_SPAN_EMPTY; + } + + // Generate the encoded, signed signature (b64 encoded, HMAC-SHA256 signing). + char b64enc_hmacsha256_signature[64]; + az_span sas_base64_encoded_signed_signature = AZ_SPAN_FROM_BUFFER(b64enc_hmacsha256_signature); + + if (iot_sample_generate_sas_base64_encoded_signed_signature( + device_key, + sas_signature, + sas_base64_encoded_signed_signature, + &sas_base64_encoded_signed_signature) + != 0) + { + Logger.Error("Failed generating SAS token signed signature"); + return AZ_SPAN_EMPTY; + } + + // Get the resulting MQTT password, passing the base64 encoded, HMAC signed + // bytes. + size_t mqtt_password_length; + rc = az_iot_hub_client_sas_get_password( + hub_client, + sas_duration, + sas_base64_encoded_signed_signature, + AZ_SPAN_EMPTY, + (char*)az_span_ptr(sas_token), + az_span_size(sas_token), + &mqtt_password_length); + + if (az_result_failed(rc)) + { + Logger.Error("Could not get the password: az_result return code " + rc); + return AZ_SPAN_EMPTY; + } + else + { + return az_span_slice(sas_token, 0, mqtt_password_length); + } +} + +AzIoTSasToken::AzIoTSasToken( + az_iot_hub_client* client, + az_span deviceKey, + az_span signatureBuffer, + az_span sasTokenBuffer) +{ + this->client = client; + this->deviceKey = deviceKey; + this->signatureBuffer = signatureBuffer; + this->sasTokenBuffer = sasTokenBuffer; + this->expirationUnixTime = 0; + this->sasToken = AZ_SPAN_EMPTY; +} + +int AzIoTSasToken::Generate(unsigned int expiryTimeInMinutes) +{ + this->sasToken = generate_sas_token( + this->client, + this->deviceKey, + this->signatureBuffer, + expiryTimeInMinutes, + this->sasTokenBuffer); + + if (az_span_is_content_equal(this->sasToken, AZ_SPAN_EMPTY)) + { + Logger.Error("Failed generating SAS token"); + return 1; + } + else + { + this->expirationUnixTime = getSasTokenExpiration((const char*)az_span_ptr(this->sasToken)); + + if (this->expirationUnixTime == 0) + { + Logger.Error("Failed getting the SAS token expiration time"); + this->sasToken = AZ_SPAN_EMPTY; + return 1; + } + else + { + return 0; + } + } +} + +bool AzIoTSasToken::IsExpired() +{ + time_t now = time(NULL); + + if (now == INDEFINITE_TIME) + { + Logger.Error("Failed getting current time"); + return true; + } + else + { + return (now >= this->expirationUnixTime); + } +} + +az_span AzIoTSasToken::Get() { return this->sasToken; } diff --git a/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.h b/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.h new file mode 100644 index 00000000..60ef9644 --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/AzIoTSasToken.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef AZIOTSASTOKEN_H +#define AZIOTSASTOKEN_H + +#include +#include +#include + +class AzIoTSasToken +{ +public: + AzIoTSasToken( + az_iot_hub_client* client, + az_span deviceKey, + az_span signatureBuffer, + az_span sasTokenBuffer); + int Generate(unsigned int expiryTimeInMinutes); + bool IsExpired(); + az_span Get(); + +private: + az_iot_hub_client* client; + az_span deviceKey; + az_span signatureBuffer; + az_span sasTokenBuffer; + az_span sasToken; + uint32_t expirationUnixTime; +}; + +#endif // AZIOTSASTOKEN_H diff --git a/examples/Azure_IoT_Adu_ESP32/Azure_IoT_Adu_ESP32.ino b/examples/Azure_IoT_Adu_ESP32/Azure_IoT_Adu_ESP32.ino new file mode 100644 index 00000000..2b347467 --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/Azure_IoT_Adu_ESP32.ino @@ -0,0 +1,1054 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/* + * This is an Arduino-based Azure IoT Hub sample for ESPRESSIF ESP32 boards. + * It uses our Azure Embedded SDK for C to help interact with Azure IoT. + * For reference, please visit https://github.com/azure/azure-sdk-for-c. + * + * To connect and work with Azure IoT Hub you need an MQTT client, connecting, + * subscribing and publishing to specific topics to use the messaging features + * of the hub. Our azure-sdk-for-c is an MQTT client support library, helping + * composing and parsing the MQTT topic names and messages exchanged with the + * Azure IoT Hub. + * + * This sample performs the following tasks: + * - Synchronize the device clock with a NTP server; + * - Initialize our "az_iot_hub_client" (struct for data, part of our + * azure-sdk-for-c); + * - Initialize the MQTT client (here we use ESPRESSIF's esp_mqtt_client, which + * also handle the tcp connection and TLS); + * - Connect the MQTT client (using server-certificate validation, SAS-tokens + * for client authentication); + * - Periodically send telemetry data to the Azure IoT Hub. + * + * To properly connect to your Azure IoT Hub, please fill the information in the + * `iot_configs.h` file. + */ + +// C99 libraries +#include +#include +#include + +// Libraries for MQTT client and WiFi connection +#include +#include +#include +#include + +// Azure IoT SDK for C includes +#include +#include +#include + +// Additional sample headers +#include "AzIoTSasToken.h" +#include "SampleAduJWS.h" +#include "SerialLogger.h" +#include "iot_configs.h" + +#include "esp_ota_ops.h" +#include "esp_system.h" + +// When developing for your own Arduino-based platform, +// please follow the format '(ard;)'. +#define AZURE_SDK_CLIENT_USER_AGENT "c%2f" AZ_SDK_VERSION_STRING "(ard;esp32)" + +#define ADU_UPDATE_ID "{\"provider\":\"" ADU_UPDATE_PROVIDER "\",\"name\":\"" ADU_UPDATE_NAME "\",\"version\":\"" ADU_DEVICE_VERSION "\"}" + +#define SAMPLE_MQTT_TOPIC_LENGTH 128 +#define SAMPLE_MQTT_PAYLOAD_LENGTH 1024 + +// ADU Values +#define ADU_DEVICE_SHA_SIZE 32 +#define ADU_SHA_PARTITION_READ_BUFFER_SIZE 32 +#define HTTP_DOWNLOAD_CHUNK 4096 +#define ADU_PNP_ACCEPT_CODE 200 +#define ADU_PNP_REJECT_CODE 406 + +// ADU Feature Values +static az_iot_adu_client adu_client; +static az_iot_adu_client_update_request adu_update_request; +static az_iot_adu_client_update_manifest adu_update_manifest; +static char adu_new_version[16]; +static bool process_update_request = false; +static bool did_update = false; +static char adu_scratch_buffer[10000]; +static char adu_manifest_unescape_buffer[2000]; +static char adu_verification_buffer[jwsSCRATCH_BUFFER_SIZE]; +static char adu_sha_buffer[ADU_DEVICE_SHA_SIZE]; +static char adu_calculated_sha_buffer[ADU_DEVICE_SHA_SIZE]; +static char partition_read_buffer[ADU_SHA_PARTITION_READ_BUFFER_SIZE]; +static int chunked_data_index; + +static az_span pnp_components[] = { AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_PROPERTIES_COMPONENT_NAME) }; +az_iot_adu_client_device_properties adu_device_information + = { .manufacturer = AZ_SPAN_LITERAL_FROM_STR(ADU_DEVICE_MANUFACTURER), + .model = AZ_SPAN_LITERAL_FROM_STR(ADU_DEVICE_MODEL), + .adu_version = AZ_SPAN_LITERAL_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_VERSION), + .delivery_optimization_agent_version = AZ_SPAN_EMPTY, + .update_id = AZ_SPAN_LITERAL_FROM_STR(ADU_UPDATE_ID) }; + +// Utility macros and defines +#define sizeofarray(a) (sizeof(a) / sizeof(a[0])) +#define NTP_SERVERS "pool.ntp.org", "time.nist.gov" +#define MQTT_QOS1 1 +#define DO_NOT_RETAIN_MSG 0 +#define SAS_TOKEN_DURATION_IN_MINUTES 60 +#define UNIX_TIME_NOV_13_2017 1510592825 + +#define PST_TIME_ZONE -8 +#define PST_TIME_ZONE_DAYLIGHT_SAVINGS_DIFF 1 + +#define GMT_OFFSET_SECS (PST_TIME_ZONE * 3600) +#define GMT_OFFSET_SECS_DST ((PST_TIME_ZONE + PST_TIME_ZONE_DAYLIGHT_SAVINGS_DIFF) * 3600) + +// Translate iot_configs.h defines into variables used by the sample +static const char* ssid = IOT_CONFIG_WIFI_SSID; +static const char* password = IOT_CONFIG_WIFI_PASSWORD; +static const char* host = IOT_CONFIG_IOTHUB_FQDN; +static const char* mqtt_broker_uri = "mqtts://" IOT_CONFIG_IOTHUB_FQDN; +static const char* device_id = IOT_CONFIG_DEVICE_ID; +static const int mqtt_port = AZ_IOT_DEFAULT_MQTT_CONNECT_PORT; + +// Memory allocated for the sample's variables and structures. +static esp_mqtt_client_handle_t mqtt_client; +static az_iot_hub_client hub_client; +static HTTPClient http_client; +bool didNewlyConnect = true; + +// MQTT Connection Values +static uint16_t connection_request_id = 0; +static char connection_request_id_buffer[16]; +static char mqtt_client_id[256]; +static char mqtt_username[256]; +static char mqtt_password[200]; +static uint8_t sas_signature_buffer[256]; +static unsigned long next_telemetry_send_time_ms = 0; +static char telemetry_topic[128]; +static uint8_t telemetry_payload[100]; +static uint32_t telemetry_send_count = 0; + +static char incoming_topic[SAMPLE_MQTT_TOPIC_LENGTH]; +static char incoming_data[SAMPLE_MQTT_PAYLOAD_LENGTH]; + +// Auxiliary functions +#ifndef IOT_CONFIG_USE_X509_CERT +static AzIoTSasToken sasToken( + &hub_client, + AZ_SPAN_FROM_STR(IOT_CONFIG_DEVICE_KEY), + AZ_SPAN_FROM_BUFFER(sas_signature_buffer), + AZ_SPAN_FROM_BUFFER(mqtt_password)); +#endif // IOT_CONFIG_USE_X509_CERT + +static void connect_to_wifi() +{ + Logger.Info("Connecting to WIFI SSID " + String(ssid)); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + + Logger.Info("WiFi connected, IP address: " + WiFi.localIP().toString()); +} + +static void initialize_time() +{ + Logger.Info("Setting time using SNTP"); + + configTime(GMT_OFFSET_SECS, GMT_OFFSET_SECS_DST, NTP_SERVERS); + time_t now = time(NULL); + while (now < UNIX_TIME_NOV_13_2017) + { + delay(500); + Serial.print("."); + now = time(nullptr); + } + Serial.println(""); + Logger.Info("Time initialized!"); +} + +// get_request_id sets a request Id into connection_request_id_buffer and +// monotonically increases the counter for the next MQTT operation. +static az_span get_request_id(void) +{ + az_span remainder; + az_span out_span = az_span_create( + (uint8_t*)connection_request_id_buffer, sizeof(connection_request_id_buffer)); + + connection_request_id++; + if (connection_request_id == UINT16_MAX) + { + // Connection id has looped. Reset. + connection_request_id = 1; + } + + az_result rc = az_span_u32toa(out_span, connection_request_id, &remainder); + if (az_result_failed(rc)) + { + Logger.Info("Failed to get request id"); + } + + return az_span_slice(out_span, 0, az_span_size(out_span) - az_span_size(remainder)); +} + +static void prvParseAduUrl(az_span xUrl, az_span* pxHost, az_span* pxPath) +{ + xUrl = az_span_slice_to_end(xUrl, sizeof("http://") - 1); + int32_t lPathPosition = az_span_find(xUrl, AZ_SPAN_FROM_STR("/")); + *pxHost = az_span_slice(xUrl, 0, lPathPosition); + *pxPath = az_span_slice_to_end(xUrl, lPathPosition); +} + +void update_started() { Serial.println("CALLBACK: HTTP update process started"); } + +void update_finished() { Serial.println("CALLBACK: HTTP update process finished"); } + +void update_progress(int cur, int total) +{ + Serial.printf("CALLBACK: HTTP update process at %d of %d bytes...\n", cur, total); +} + +void update_error(int err) { Serial.printf("CALLBACK: HTTP update fatal error code %d\n", err); } + +static az_result download_and_write_to_flash(void) +{ + az_span url_host_span; + az_span url_path_span; + prvParseAduUrl(adu_update_request.file_urls[0].url, &url_host_span, &url_path_span); + + WiFiClient client; + + httpUpdate.onStart(update_started); + httpUpdate.onEnd(update_finished); + httpUpdate.onProgress(update_progress); + httpUpdate.onError(update_error); + httpUpdate.rebootOnUpdate(false); + + char null_terminated_host[128]; + (void)memcpy(null_terminated_host, az_span_ptr(url_host_span), az_span_size(url_host_span)); + null_terminated_host[az_span_size(url_host_span)] = '\0'; + + char null_terminated_path[128]; + (void)memcpy(null_terminated_path, az_span_ptr(url_path_span), az_span_size(url_path_span)); + null_terminated_path[az_span_size(url_path_span)] = '\0'; + + t_httpUpdate_return ret + = httpUpdate.update(client, null_terminated_host, 80, null_terminated_path); + + if (ret != HTTP_UPDATE_OK) + { + Logger.Error("Image download failed"); + return AZ_ERROR_CANCELED; + } + else + { + Logger.Info("Download of image succeeded"); + return AZ_OK; + } +} + +static az_result verify_image(az_span sha256_hash, int32_t update_size) +{ + az_result result; + esp_err_t espErr; + + int32_t out_size; + int32_t read_size; + az_span out_buffer = AZ_SPAN_FROM_BUFFER(adu_sha_buffer); + + Logger.Info("Verifying downloaded image with manifest SHA256 hash"); + + const esp_partition_t* current_partition = esp_ota_get_running_partition(); + const esp_partition_t* update_partition = esp_ota_get_next_update_partition(current_partition); + + if (az_result_failed(result = az_base64_decode(out_buffer, sha256_hash, &out_size))) + { + Logger.Error("az_base64_decode failed: core error=0x" + String(result, HEX)); + return result; + } + else + { + Logger.Info("Unencoded the base64 encoding\r\n"); + result = AZ_OK; + } + + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0); + mbedtls_md_starts(&ctx); + + Logger.Info("Starting the mbedtls calculation: image size " + String(update_size)); + for (size_t offset_index = 0; offset_index < update_size; + offset_index += ADU_SHA_PARTITION_READ_BUFFER_SIZE) + { + read_size = update_size - offset_index < ADU_SHA_PARTITION_READ_BUFFER_SIZE + ? update_size - offset_index + : ADU_SHA_PARTITION_READ_BUFFER_SIZE; + + espErr + = esp_partition_read_raw(update_partition, offset_index, partition_read_buffer, read_size); + (void)espErr; + + mbedtls_md_update(&ctx, (const unsigned char*)partition_read_buffer, read_size); + } + + printf("\r\n"); + + Logger.Info("Done\r\n"); + + mbedtls_md_finish(&ctx, (unsigned char*)adu_calculated_sha_buffer); + mbedtls_md_free(&ctx); + + if (memcmp(adu_sha_buffer, adu_calculated_sha_buffer, ADU_DEVICE_SHA_SIZE) == 0) + { + Logger.Info("SHAs match\r\n"); + result = AZ_OK; + } + else + { + Logger.Info("SHAs do not match\r\n"); + result = AZ_ERROR_ITEM_NOT_FOUND; + } + + return result; +} + +// request_all_properties sends a request to Azure IoT Hub to request all +// properties for the device. This call does not block. Properties will be +// received on a topic previously subscribed to +// (AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_SUBSCRIBE_TOPIC.) +static void request_all_properties(void) +{ + az_result rc; + + Logger.Info("Client requesting device property document from service."); + + // Get the topic to publish the property document request. + char property_document_topic_buffer[SAMPLE_MQTT_TOPIC_LENGTH]; + rc = az_iot_hub_client_properties_document_get_publish_topic( + &hub_client, + get_request_id(), + property_document_topic_buffer, + sizeof(property_document_topic_buffer), + NULL); + if (az_result_failed(rc)) + { + Logger.Info("Failed to get the property document topic"); + } + + if (esp_mqtt_client_publish( + mqtt_client, property_document_topic_buffer, NULL, 0, MQTT_QOS1, DO_NOT_RETAIN_MSG) + < 0) + { + Logger.Error("Failed publishing"); + } + else + { + Logger.Info("Message published successfully"); + } +} + +// send_adu_device_reported_property writes a property payload reporting device +// state and then sends it to Azure IoT Hub. +static void send_adu_device_information_property(int32_t agent_state, az_iot_adu_client_workflow * workflow) +{ + az_result rc; + + // Get the property topic to send a reported property update. + char property_update_topic_buffer[SAMPLE_MQTT_TOPIC_LENGTH]; + rc = az_iot_hub_client_properties_get_reported_publish_topic( + &hub_client, + get_request_id(), + property_update_topic_buffer, + sizeof(property_update_topic_buffer), + NULL); + if (az_result_failed(rc)) + { + Logger.Error("Failed to get the property update topic"); + } + + // Write the updated reported property message. + char reported_property_payload_buffer[SAMPLE_MQTT_PAYLOAD_LENGTH] = { 0 }; + az_span reported_property_payload = AZ_SPAN_FROM_BUFFER(reported_property_payload_buffer); + az_json_writer adu_payload; + + rc = az_json_writer_init(&adu_payload, reported_property_payload, NULL); + if (az_result_failed(rc)) + { + Logger.Error("Failed to get the adu device information payload"); + } + + rc = az_iot_adu_client_get_agent_state_payload( + &adu_client, + &adu_device_information, + agent_state, + workflow, + NULL, + &adu_payload); + if (az_result_failed(rc)) + { + Logger.Error("Failed to get the adu device information payload"); + } + + reported_property_payload = az_json_writer_get_bytes_used_in_destination(&adu_payload); + + Logger.Info("Topic " + String(property_update_topic_buffer)); + Logger.Info("Payload " + String(reported_property_payload_buffer)); + + if (esp_mqtt_client_publish( + mqtt_client, + property_update_topic_buffer, + (const char*)az_span_ptr(reported_property_payload), + az_span_size(reported_property_payload), + 0, + DO_NOT_RETAIN_MSG) + < 0) + { + Logger.Error("Failed publishing"); + } + else + { + Logger.Info("Client published the device's information."); + } +} + +static void send_adu_accept_manifest_property(int32_t version_number, + int32_t response_code) +{ + az_result rc; + + // Get the topic to publish the property document request. + char property_accept_topic_buffer[SAMPLE_MQTT_TOPIC_LENGTH]; + rc = az_iot_hub_client_properties_get_reported_publish_topic( + &hub_client, + get_request_id(), + property_accept_topic_buffer, + sizeof(property_accept_topic_buffer), + NULL); + if (az_result_failed(rc)) + { + Logger.Error("Could not get properties topic"); + return; + } + + char property_payload_buffer[SAMPLE_MQTT_PAYLOAD_LENGTH]; + az_span property_buffer = AZ_SPAN_FROM_BUFFER(property_payload_buffer); + + az_json_writer adu_payload; + + rc = az_json_writer_init(&adu_payload, property_buffer, NULL); + if (az_result_failed(rc)) + { + Logger.Error("Failed to get the adu device information payload"); + } + + rc = az_iot_adu_client_get_service_properties_response( + &adu_client, version_number, response_code, &adu_payload); + if (az_result_failed(rc)) + { + Logger.Error("Could not get service properties response"); + return; + } + + if (esp_mqtt_client_publish( + mqtt_client, + property_accept_topic_buffer, + (const char*)az_span_ptr(property_buffer), + az_span_size(property_buffer), + MQTT_QOS1, + DO_NOT_RETAIN_MSG) + < 0) + { + Logger.Error("Failed publishing"); + } + else + { + Logger.Info("Message published successfully"); + } +} + +static bool is_update_already_applied(void) +{ + return ( + (az_span_size(adu_update_manifest.instructions.steps[0].handler_properties.installed_criteria) + == sizeof(ADU_DEVICE_VERSION) - 1) + && strncmp( + ADU_DEVICE_VERSION, + (char*)az_span_ptr( + adu_update_manifest.instructions.steps[0].handler_properties.installed_criteria), + az_span_size( + adu_update_manifest.instructions.steps[0].handler_properties.installed_criteria)) + == 0); +} + +// process_device_property_message handles incoming properties from Azure IoT +// Hub. +static void process_device_property_message( + az_span message_span, + az_iot_hub_client_properties_message_type message_type) +{ + az_json_reader jr; + az_json_reader jr_adu_manifest; + int32_t out_manifest_size; + az_result rc = az_json_reader_init(&jr, message_span, NULL); + if (az_result_failed(rc)) + { + Logger.Error("Could not initialize json reader"); + return; + } + + int32_t version_number; + rc = az_iot_hub_client_properties_get_properties_version( + &hub_client, &jr, message_type, &version_number); + if (az_result_failed(rc)) + { + Logger.Error("Could not get property version"); + return; + } + + rc = az_json_reader_init(&jr, message_span, NULL); + if (az_result_failed(rc)) + { + Logger.Error("Could not initialize json reader"); + return; + } + + az_span component_name; + + az_span scratch_buffer_span + = az_span_create((uint8_t*)adu_scratch_buffer, (int32_t)sizeof(adu_scratch_buffer)); + + // Applications call az_iot_hub_client_properties_get_next_component_property + // to enumerate properties received. + while (az_result_succeeded(az_iot_hub_client_properties_get_next_component_property( + &hub_client, &jr, message_type, AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE, &component_name))) + { + if (az_iot_adu_client_is_component_device_update(&adu_client, component_name)) + { + // ADU Component + rc = az_iot_adu_client_parse_service_properties( + &adu_client, &jr, &adu_update_request); + + if (az_result_failed(rc)) + { + Logger.Error("az_iot_adu_client_parse_service_properties failed" + String(rc)); + return; + } + else + { + if(adu_update_request.workflow.action != AZ_IOT_ADU_CLIENT_SERVICE_ACTION_CANCEL) + { + rc = az_json_string_unescape(adu_update_request.update_manifest, (char*)adu_manifest_unescape_buffer, sizeof(adu_manifest_unescape_buffer), &out_manifest_size); + + if (az_result_failed(rc)) + { + Logger.Error("az_json_string_unescape failed" + String(rc)); + return; + } + + az_span manifest_unescaped = az_span_create((uint8_t*)adu_manifest_unescape_buffer, out_manifest_size); + + rc = az_json_reader_init(&jr_adu_manifest, manifest_unescaped, NULL); + + if (az_result_failed(rc)) + { + Logger.Error("az_iot_adu_client_parse_update_manifest failed" + String(rc)); + return; + } + + rc = az_iot_adu_client_parse_update_manifest( + &adu_client, &jr_adu_manifest, &adu_update_manifest); + + if (az_result_failed(rc)) + { + Logger.Error("az_iot_adu_client_parse_update_manifest failed" + String(rc)); + return; + } + + Logger.Info("Parsed Azure device update manifest."); + + rc = SampleJWS::ManifestAuthenticate( + manifest_unescaped, + adu_update_request.update_manifest_signature, + AZ_SPAN_FROM_BUFFER(adu_verification_buffer)); + if (az_result_failed(rc)) + { + Logger.Error("ManifestAuthenticate failed " + String(rc)); + process_update_request = false; + return; + } + + Logger.Info("Manifest authenticated successfully"); + + if (is_update_already_applied()) + { + Logger.Info("Update already applied"); + send_adu_accept_manifest_property(version_number, ADU_PNP_REJECT_CODE); + process_update_request = false; + } + else + { + Logger.Info("Sending manifest property accept"); + send_adu_accept_manifest_property(version_number, ADU_PNP_ACCEPT_CODE); + + process_update_request = true; + } + } + else + { + Logger.Info("ADU action received: cancelled deployment"); + } + } + } + else + { + Logger.Info("Unknown Property Received"); + // The JSON reader must be advanced regardless of whether the property + // is of interest or not. + rc = az_json_reader_next_token(&jr); + if (az_result_failed(rc)) + { + Logger.Error("Invalid JSON. Could not move to next property value"); + } + + // Skip children in case the property value is an object + rc = az_json_reader_skip_children(&jr); + if (az_result_failed(rc)) + { + Logger.Error("Invalid JSON. Could not skip children"); + } + + rc = az_json_reader_next_token(&jr); + if (az_result_failed(rc)) + { + Logger.Error("Invalid JSON. Could not move to next property name"); + } + } + } +} + +// handle_device_property_message handles incoming properties from Azure IoT +// Hub. +static void handle_device_property_message( + byte* payload, + unsigned int length, + az_iot_hub_client_properties_message const* property_message) +{ + az_span const message_span = az_span_create((uint8_t*)payload, length); + + // Invoke appropriate action per message type (3 types only). + switch (property_message->message_type) + { + // A message from a property GET publish message with the property document as + // a payload. + case AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE: + Logger.Info("Message Type: GET"); + process_device_property_message(message_span, property_message->message_type); + break; + + // An update to the desired properties with the properties as a payload. + case AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED: + Logger.Info("Message Type: Desired Properties"); + process_device_property_message(message_span, property_message->message_type); + break; + + // When the device publishes a property update, this message type arrives when + // server acknowledges this. + case AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_ACKNOWLEDGEMENT: + Logger.Info("Message Type: IoT Hub has acknowledged properties that the " + "device sent"); + break; + + // An error has occurred + case AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_ERROR: + Logger.Error("Message Type: Request Error"); + break; + } +} + +void received_callback(char* topic, byte* payload, unsigned int length) +{ + az_result rc; + + az_iot_hub_client_properties_message property_message; + + az_span topic_span = az_span_create((uint8_t*)topic, (int32_t)strlen(topic)); + + rc = az_iot_hub_client_properties_parse_received_topic( + &hub_client, topic_span, &property_message); + if (az_result_succeeded(rc)) + { + Logger.Info("Client received a properties topic."); + Logger.Info("Status: " + String(property_message.status)); + + handle_device_property_message(payload, length, &property_message); + } + else + { + Logger.Error( + "Error: " + String(rc) + " Received a message from an unknown topic: " + String(topic)); + } +} + +static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event) +{ + switch (event->event_id) + { + int i, r; + + case MQTT_EVENT_ERROR: + Logger.Info("MQTT event MQTT_EVENT_ERROR"); + Logger.Info("ESP ERROR " + String(event->error_handle->esp_tls_last_esp_err)); + Logger.Info("TLS ERROR " + String(event->error_handle->esp_tls_stack_err)); + Logger.Info("ERROR TYPE " + String(event->error_handle->error_type)); + Logger.Info( + "ESP Error " + String(esp_err_to_name(event->error_handle->esp_tls_last_esp_err))); + break; + case MQTT_EVENT_CONNECTED: + Logger.Info("MQTT event MQTT_EVENT_CONNECTED"); + + r = esp_mqtt_client_subscribe(mqtt_client, AZ_IOT_HUB_CLIENT_C2D_SUBSCRIBE_TOPIC, 1); + if (r == -1) + { + Logger.Error("Could not subscribe for cloud-to-device messages."); + } + else + { + Logger.Info("Subscribed for cloud-to-device messages; message id:" + String(r)); + } + + r = esp_mqtt_client_subscribe( + mqtt_client, AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_SUBSCRIBE_TOPIC, 1); + if (r == -1) + { + Logger.Error("Could not subscribe to properties messages."); + } + else + { + Logger.Info("Subscribed to properties messages; message id:" + String(r)); + } + + r = esp_mqtt_client_subscribe( + mqtt_client, AZ_IOT_HUB_CLIENT_PROPERTIES_WRITABLE_UPDATES_SUBSCRIBE_TOPIC, 1); + if (r == -1) + { + Logger.Error("Could not subscribe to writable property updates."); + } + else + { + Logger.Info("Subscribed to writable property updates; message id:" + String(r)); + } + + break; + case MQTT_EVENT_DISCONNECTED: + Logger.Info("MQTT event MQTT_EVENT_DISCONNECTED"); + break; + case MQTT_EVENT_SUBSCRIBED: + Logger.Info("MQTT event MQTT_EVENT_SUBSCRIBED"); + break; + case MQTT_EVENT_UNSUBSCRIBED: + Logger.Info("MQTT event MQTT_EVENT_UNSUBSCRIBED"); + break; + case MQTT_EVENT_PUBLISHED: + Logger.Info("MQTT event MQTT_EVENT_PUBLISHED"); + break; + case MQTT_EVENT_DATA: + Logger.Info("MQTT event MQTT_EVENT_DATA"); + + // Chunked data will not have a topic. Only copy when topic length is greater + // than 0. + if (event->topic_len > 0) + { + for (i = 0; i < (SAMPLE_MQTT_TOPIC_LENGTH - 1) && i < event->topic_len; i++) + { + incoming_topic[i] = event->topic[i]; + } + incoming_topic[i] = '\0'; + Logger.Info("Topic: " + String(incoming_topic)); + } + + for (i = 0; i < (SAMPLE_MQTT_PAYLOAD_LENGTH - 1) && i < event->data_len; i++) + { + incoming_data[i] = event->data[i]; + } + incoming_data[i] = '\0'; + + if (event->total_data_len > event->data_len) + { + Logger.Info("Received Chunked Payload Data"); + chunked_data_index = event->current_data_offset != 0 ? chunked_data_index : 0; + // This data is going to be incoming in chunks. Piece it together. + memcpy((void*)&adu_scratch_buffer[chunked_data_index], event->data, event->data_len); + chunked_data_index += event->data_len; + + if (chunked_data_index == event->total_data_len) + { + Logger.Info("Received all of the chunked data. Moving to process."); + adu_scratch_buffer[chunked_data_index] = '\0'; + Logger.Info("Data: " + String(adu_scratch_buffer)); + received_callback(incoming_topic, (byte*)adu_scratch_buffer, event->total_data_len); + } + } + else + { + received_callback(incoming_topic, (byte*)incoming_data, event->data_len); + } + + break; + case MQTT_EVENT_BEFORE_CONNECT: + Logger.Info("MQTT event MQTT_EVENT_BEFORE_CONNECT"); + break; + default: + Logger.Error("MQTT event UNKNOWN"); + break; + } + + return ESP_OK; +} + +static void initialize_iot_hub_client() +{ + Logger.Info("------------------------------------------------------------------------------"); + Logger.Info("ADU SAMPLE"); + Logger.Info("Version: " + String(ADU_DEVICE_VERSION)); + Logger.Info("------------------------------------------------------------------------------"); + + az_iot_hub_client_options options = az_iot_hub_client_options_default(); + options.user_agent = AZ_SPAN_FROM_STR(AZURE_SDK_CLIENT_USER_AGENT); + options.model_id = AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_MODEL_ID); + options.component_names = pnp_components; + options.component_names_length = sizeof(pnp_components) / sizeof(pnp_components[0]); + + if (az_result_failed(az_iot_hub_client_init( + &hub_client, + az_span_create((uint8_t*)host, strlen(host)), + az_span_create((uint8_t*)device_id, strlen(device_id)), + &options))) + { + Logger.Error("Failed initializing Azure IoT Hub client"); + return; + } + + if (az_result_failed(az_iot_adu_client_init(&adu_client, NULL))) + { + Logger.Error("Failed initializing Azure IoT Adu client"); + return; + } + + size_t client_id_length; + if (az_result_failed(az_iot_hub_client_get_client_id( + &hub_client, mqtt_client_id, sizeof(mqtt_client_id) - 1, &client_id_length))) + { + Logger.Error("Failed getting client id"); + return; + } + + if (az_result_failed(az_iot_hub_client_get_user_name( + &hub_client, mqtt_username, sizeofarray(mqtt_username), NULL))) + { + Logger.Error("Failed to get MQTT clientId, return code"); + return; + } + + Logger.Info("Client ID: " + String(mqtt_client_id)); + Logger.Info("Username: " + String(mqtt_username)); +} + +static int initialize_mqtt_client() +{ +#ifndef IOT_CONFIG_USE_X509_CERT + if (sasToken.Generate(SAS_TOKEN_DURATION_IN_MINUTES) != 0) + { + Logger.Error("Failed generating SAS token"); + return 1; + } +#endif + + esp_mqtt_client_config_t mqtt_config; + memset(&mqtt_config, 0, sizeof(mqtt_config)); + mqtt_config.uri = mqtt_broker_uri; + mqtt_config.port = mqtt_port; + mqtt_config.client_id = mqtt_client_id; + mqtt_config.username = mqtt_username; + +#ifdef IOT_CONFIG_USE_X509_CERT + Logger.Info("MQTT client using X509 Certificate authentication"); + mqtt_config.client_cert_pem = IOT_CONFIG_DEVICE_CERT; + mqtt_config.client_key_pem = IOT_CONFIG_DEVICE_CERT_PRIVATE_KEY; +#else // Using SAS key + mqtt_config.password = (const char*)az_span_ptr(sasToken.Get()); +#endif + + mqtt_config.keepalive = 30; + mqtt_config.disable_clean_session = 0; + mqtt_config.disable_auto_reconnect = false; + mqtt_config.event_handle = mqtt_event_handler; + mqtt_config.user_context = NULL; + mqtt_config.cert_pem = (const char*)ca_pem; + mqtt_config.buffer_size = 2048; + + mqtt_client = esp_mqtt_client_init(&mqtt_config); + + if (mqtt_client == NULL) + { + Logger.Error("Failed creating mqtt client"); + return 1; + } + + esp_err_t start_result = esp_mqtt_client_start(mqtt_client); + + if (start_result != ESP_OK) + { + Logger.Error("Could not start mqtt client; error code:" + start_result); + return 1; + } + else + { + Logger.Info("MQTT client started"); + return 0; + } +} + +/* + * @brief Gets the number of seconds since UNIX epoch until now. + * @return uint32_t Number of seconds. + */ +static uint32_t get_epoch_time_in_seconds() { return (uint32_t)time(NULL); } + +static void establish_connection() +{ + connect_to_wifi(); + initialize_time(); + initialize_iot_hub_client(); + (void)initialize_mqtt_client(); +} + +static void get_telemetry_payload(az_span payload, az_span* out_payload) +{ + az_result rc; + az_span original_payload = payload; + + payload = az_span_copy(payload, AZ_SPAN_FROM_STR("{ \"msgCount\": ")); + rc = az_span_u32toa(payload, telemetry_send_count++, &payload); + (void)rc; + payload = az_span_copy(payload, AZ_SPAN_FROM_STR(" }")); + payload = az_span_copy_u8(payload, '\0'); + + *out_payload = az_span_slice( + original_payload, 0, az_span_size(original_payload) - az_span_size(payload) - 1); +} + +static void send_telemetry() +{ + az_span telemetry = AZ_SPAN_FROM_BUFFER(telemetry_payload); + + Logger.Info("Sending telemetry ..."); + + // The topic could be obtained just once during setup, + // however if properties are used the topic need to be generated again to + // reflect the current values of the properties. + if (az_result_failed(az_iot_hub_client_telemetry_get_publish_topic( + &hub_client, NULL, telemetry_topic, sizeof(telemetry_topic), NULL))) + { + Logger.Error("Failed az_iot_hub_client_telemetry_get_publish_topic"); + return; + } + + get_telemetry_payload(telemetry, &telemetry); + + if (esp_mqtt_client_publish( + mqtt_client, + telemetry_topic, + (const char*)az_span_ptr(telemetry), + az_span_size(telemetry), + MQTT_QOS1, + DO_NOT_RETAIN_MSG) + < 0) + { + Logger.Error("Failed publishing"); + } + else + { + Logger.Info("Message published successfully"); + } +} + +// Arduino setup and loop main functions. + +void setup() { establish_connection(); } + +bool send_init_state = true; + +void loop() +{ + az_result result; + + if (WiFi.status() != WL_CONNECTED) + { + Logger.Info("Connecting to WiFI"); + connect_to_wifi(); + send_init_state = true; + } +#ifndef IOT_CONFIG_USE_X509_CERT + else if (sasToken.IsExpired()) + { + Logger.Info("SAS token expired; reconnecting with a new one."); + (void)esp_mqtt_client_destroy(mqtt_client); + initialize_mqtt_client(); + send_init_state = true; + } +#endif + else if (millis() > next_telemetry_send_time_ms) + { + if(send_init_state) + { + Logger.Info("Requesting all device properties"); + request_all_properties(); + Logger.Info("Sending ADU device information"); + send_adu_device_information_property(AZ_IOT_ADU_CLIENT_AGENT_STATE_IDLE, NULL); + + send_init_state = false; + } + send_telemetry(); + next_telemetry_send_time_ms = millis() + TELEMETRY_FREQUENCY_MILLISECS; + + if(process_update_request) + { + send_adu_device_information_property(AZ_IOT_ADU_CLIENT_AGENT_STATE_DEPLOYMENT_IN_PROGRESS, &adu_update_request.workflow); + + result = download_and_write_to_flash(); + + if (result == AZ_OK) + { + if (adu_update_request.workflow.action == AZ_IOT_ADU_CLIENT_SERVICE_ACTION_CANCEL) + { + Logger.Info("Cancellation request was received during download. " + "Aborting update."); + process_update_request = false; + } + else + { + result = verify_image( + adu_update_manifest.files[0].hashes->hash_value, + adu_update_manifest.files[0].size_in_bytes); + + if (result == AZ_OK) + { + // All is verified. Reboot device to new update. + esp_restart(); + } + } + } + } + } +} \ No newline at end of file diff --git a/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.cpp b/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.cpp new file mode 100644 index 00000000..8739c2ba --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.cpp @@ -0,0 +1,970 @@ +/* Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. */ + +#include "SampleAduJWS.h" + +#include +#include + +#include "mbedtls/base64.h" +#include "mbedtls/cipher.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/entropy.h" +#include "mbedtls/pk.h" +#include "mbedtls/rsa.h" + +/* For logging */ +#include "SerialLogger.h" + +/** + * @brief Convenience macro to return if an operation failed. + */ +#define _az_adu_jws_return_if_failed(exp) \ + do \ + { \ + az_result const _azResult = (exp); \ + if (_azResult != AZ_OK) \ + { \ + return _azResult; \ + } \ + } while (0) + +const az_span jws_sha256_json_value = AZ_SPAN_FROM_STR("sha256"); +const az_span jws_sjwk_json_value = AZ_SPAN_FROM_STR("sjwk"); +const az_span jws_kid_json_value = AZ_SPAN_FROM_STR("kid"); +const az_span jws_n_json_value = AZ_SPAN_FROM_STR("n"); +const az_span jws_e_json_value = AZ_SPAN_FROM_STR("e"); +const az_span jws_alg_json_value = AZ_SPAN_FROM_STR("alg"); +const az_span jws_alg_rs256 = AZ_SPAN_FROM_STR("RS256"); + +typedef struct prvJWSValidationContext +{ + az_span jwk_header; + az_span jwk_payload; + az_span jwk_signature; + az_span scratch_calculation_buffer; + az_span jws_header; + az_span jws_payload; + az_span jws_signature; + az_span signing_key_n; + az_span signing_key_e; + az_span manifest_sha_calculation; + az_span parsed_manifest_sha; + az_span base64_encoded_header; + az_span base64_encoded_payload; + az_span base64_encoded_signature; + az_span jwk_base64_encoded_header; + az_span jwk_base64_encoded_payload; + az_span jwk_base64_encoded_signature; + int32_t base64_signature_length; + int32_t out_parsed_manifest_sha_size; + int32_t out_signing_key_e_length; + int32_t out_signing_key_n_length; + int32_t out_jws_header_length; + int32_t out_jws_payload_length; + int32_t out_jws_signature_length; + int32_t out_jwk_header_length; + int32_t out_jwk_payload_length; + int32_t out_jwk_signature_length; + az_span kid_span; + az_span sha256_span; + az_span base64_encoded_n_span; + az_span base64_encoded_e_span; + az_span alg_span; + az_span jwk_manifest_span; +} jws_validation_context; + +/* split_jws takes a JWS payload and returns pointers to its constituent header, + * payload, and signature parts. */ +static az_result split_jws( + az_span jws_span, + az_span* header_span, + az_span* payload_span, + az_span* signature_span) +{ + uint8_t* first_dot; + uint8_t* second_dot; + int32_t dot_count = 0; + int32_t index = 0; + int32_t jws_length = az_span_size(jws_span); + uint8_t* jws_ptr = az_span_ptr(jws_span); + + while (index < jws_length) + { + if (*jws_ptr == '.') + { + dot_count++; + + if (dot_count == 1) + { + first_dot = jws_ptr; + } + else if (dot_count == 2) + { + second_dot = jws_ptr; + } + else if (dot_count > 2) + { + Logger.Error("JWS had more '.' than required (2)"); + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + + jws_ptr++; + index++; + } + + if ((dot_count != 2) || (second_dot >= (az_span_ptr(jws_span) + jws_length - 1))) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + *header_span = az_span_create(az_span_ptr(jws_span), first_dot - az_span_ptr(jws_span)); + *payload_span = az_span_create(first_dot + 1, second_dot - first_dot - 1); + *signature_span + = az_span_create(second_dot + 1, az_span_ptr(jws_span) + jws_length - second_dot - 1); + + return AZ_OK; +} + +/* Usual base64 encoded characters use `+` and `/` for the two extra characters + */ +/* In URL encoded schemes, those aren't allowed, so the characters are swapped + */ +/* for `-` and `_`. We have to swap them back to the usual characters. */ +static void swap_to_url_encoding_chars(az_span signature_span) +{ + int32_t index = 0; + uint8_t* signature_ptr = az_span_ptr(signature_span); + int32_t signature_length = az_span_size(signature_span); + + while (index < signature_length) + { + if (*signature_ptr == '-') + { + *signature_ptr = '+'; + } + else if (*signature_ptr == '_') + { + *signature_ptr = '/'; + } + + signature_ptr++; + index++; + } +} + +/** + * @brief Calculate the SHA256 over a buffer of bytes + * + * @param input_span The input span over which to calculate the SHA256. + * @param output_span The output span into which the SHA256. It must be 32 bytes + * in length. + * @return az_result The result of the operation. + * @retval AZ_OK if successful. + */ +static az_result jws_sha256_calculate(az_span input_span, az_span output_span) +{ + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0); + mbedtls_md_starts(&ctx); + mbedtls_md_update(&ctx, az_span_ptr(input_span), az_span_size(input_span)); + mbedtls_md_finish(&ctx, az_span_ptr(output_span)); + mbedtls_md_free(&ctx); + + return AZ_OK; +} + +/** + * @brief Verify the manifest via RS256 for the JWS. + * + * @param input_span The input span over which the RS256 will be verified. + * @param signature_span The encrypted signature span which will be decrypted by \p + * n_span and \p e_span. + * @param n_span The key's modulus which is used to decrypt \p signature. + * @param e_span The exponent used for the key. + * @param buffer_span The buffer used as scratch space to make the calculations. + * It should be at least `jwsRSA3072_SIZE` + `jwsSHA256_SIZE` in size. + * @return az_result The result of the operation. + * @retval AZ_OK if successful. + */ +static az_result jws_rs256_verify( + az_span input_span, + az_span signature_span, + az_span n_span, + az_span e_span, + az_span buffer_span) +{ + az_result result; + int32_t mbed_tls_result; + uint8_t* sha_buffer_ptr; + size_t decrypted_length; + mbedtls_rsa_context ctx; + int sha_match_result; + + if (az_span_size(buffer_span) < jwsSHA_CALCULATION_SCRATCH_SIZE) + { + Logger.Error("[JWS] Buffer Not Large Enough"); + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + sha_buffer_ptr = az_span_ptr(buffer_span) + jwsRSA3072_SIZE; + + /* The signature is encrypted using the input key. We need to decrypt the */ + /* signature which gives us the SHA256 inside a PKCS7 structure. We then + * compare */ + /* that to the SHA256 of the input. */ + mbedtls_rsa_init(&ctx, MBEDTLS_RSA_PKCS_V15, 0); + + mbed_tls_result = mbedtls_rsa_import_raw( + &ctx, + az_span_ptr(n_span), + az_span_size(n_span), + NULL, + 0, + NULL, + 0, + NULL, + 0, + az_span_ptr(e_span), + az_span_size(e_span)); + + if (mbed_tls_result != 0) + { + Logger.Error("[JWS] mbedtls_rsa_import_raw res: " + String(mbed_tls_result)); + mbedtls_rsa_free(&ctx); + return AZ_ERROR_NOT_SUPPORTED; + } + + mbed_tls_result = mbedtls_rsa_complete(&ctx); + + if (mbed_tls_result != 0) + { + Logger.Error("[JWS] mbedtls_rsa_complete res: " + String(mbed_tls_result)); + mbedtls_rsa_free(&ctx); + return AZ_ERROR_NOT_SUPPORTED; + } + + mbed_tls_result = mbedtls_rsa_check_pubkey(&ctx); + + if (mbed_tls_result != 0) + { + Logger.Error("[JWS] mbedtls_rsa_check_pubkey res: " + String(mbed_tls_result)); + mbedtls_rsa_free(&ctx); + return AZ_ERROR_NOT_SUPPORTED; + } + + /* RSA */ + mbed_tls_result = mbedtls_rsa_pkcs1_decrypt( + &ctx, + NULL, + NULL, + MBEDTLS_RSA_PUBLIC, + &decrypted_length, + az_span_ptr(signature_span), + az_span_ptr(buffer_span), + jwsRSA3072_SIZE); + + if (mbed_tls_result != 0) + { + Logger.Error("[JWS] mbedtls_rsa_pkcs1_decrypt res: " + String(mbed_tls_result)); + mbedtls_rsa_free(&ctx); + return AZ_ERROR_NOT_SUPPORTED; + } + + mbedtls_rsa_free(&ctx); + + result = jws_sha256_calculate(input_span, az_span_create(sha_buffer_ptr, jwsSHA256_SIZE)); + + if (result != AZ_OK) + { + Logger.Error("[JWS] jws_sha256_calculate failed"); + return result; + } + + /* TODO: remove this once we have a valid PKCS7 parser. */ + sha_match_result + = memcmp(az_span_ptr(buffer_span) + jwsPKCS7_PAYLOAD_OFFSET, sha_buffer_ptr, jwsSHA256_SIZE); + + if (sha_match_result) + { + Logger.Error("[JWS] SHA of JWK does NOT match"); + result = AZ_ERROR_NOT_SUPPORTED; + } + + return AZ_OK; +} + +static az_result find_sjwk_value(az_json_reader* payload_json_reader, az_span* jwk_value_ptr) +{ + az_result result = AZ_OK; + + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + + while (result == AZ_OK) + { + if (az_json_token_is_text_equal(&payload_json_reader->token, jws_sjwk_json_value)) + { + /* Found name, move to value */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + break; + } + else + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + _az_adu_jws_return_if_failed(az_json_reader_skip_children(payload_json_reader)); + result = az_json_reader_next_token(payload_json_reader); + } + } + + if (result != AZ_OK) + { + Logger.Error("[JWS] Parse JSK JSON Payload Error: " + String(result, HEX)); + return result; + } + + if (payload_json_reader->token.kind != AZ_JSON_TOKEN_STRING) + { + Logger.Error("[JWS] JSON token type wrong | type: " + String(payload_json_reader->token.kind)); + return AZ_ERROR_JSON_INVALID_STATE; + } + + *jwk_value_ptr = payload_json_reader->token.slice; + + return AZ_OK; +} + +static az_result find_root_key_value(az_json_reader* payload_json_reader, az_span* kid_span_ptr) +{ + az_result result = AZ_OK; + + /*Begin object */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + /*Property Name */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + + while (result == AZ_OK) + { + if (az_json_token_is_text_equal(&payload_json_reader->token, jws_kid_json_value)) + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + *kid_span_ptr = payload_json_reader->token.slice; + + break; + } + else + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + _az_adu_jws_return_if_failed(az_json_reader_skip_children(payload_json_reader)); + result = az_json_reader_next_token(payload_json_reader); + } + } + + if (result != AZ_OK) + { + Logger.Error("[JWS] Parse Root Key Error: " + String(result, HEX)); + return result; + } + + if (payload_json_reader->token.kind != AZ_JSON_TOKEN_STRING) + { + Logger.Error("[JWS] JSON token type wrong | type: " + String(payload_json_reader->token.kind)); + return AZ_ERROR_JSON_INVALID_STATE; + } + + return AZ_OK; +} + +static az_result find_key_parts( + az_json_reader* payload_json_reader, + az_span* base64_encoded_n_span_ptr, + az_span* base64_encoded_e_span_ptr, + az_span* alg_span_ptr) +{ + az_result result = AZ_OK; + + /*Begin object */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + /*Property Name */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + + while (result == AZ_OK + && (az_span_size(*base64_encoded_n_span_ptr) == 0 + || az_span_size(*base64_encoded_e_span_ptr) == 0 || az_span_size(*alg_span_ptr) == 0)) + { + if (az_json_token_is_text_equal(&payload_json_reader->token, jws_n_json_value)) + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + *base64_encoded_n_span_ptr = payload_json_reader->token.slice; + + result = az_json_reader_next_token(payload_json_reader); + } + else if (az_json_token_is_text_equal(&payload_json_reader->token, jws_e_json_value)) + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + *base64_encoded_e_span_ptr = payload_json_reader->token.slice; + + result = az_json_reader_next_token(payload_json_reader); + } + else if (az_json_token_is_text_equal(&payload_json_reader->token, jws_alg_json_value)) + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + *alg_span_ptr = payload_json_reader->token.slice; + + result = az_json_reader_next_token(payload_json_reader); + } + else + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + _az_adu_jws_return_if_failed(az_json_reader_skip_children(payload_json_reader)); + result = az_json_reader_next_token(payload_json_reader); + } + } + + if ((result != AZ_OK) || (az_span_size(*base64_encoded_n_span_ptr) == 0) + || (az_span_size(*base64_encoded_e_span_ptr) == 0) || (az_span_size(*alg_span_ptr) == 0)) + { + Logger.Error("[JWS] Parse Signing Key Payload Error: " + String(result, HEX)); + return result; + } + + return AZ_OK; +} + +static az_result find_manifest_sha(az_json_reader* payload_json_reader, az_span* sha_span_ptr) +{ + az_result result = AZ_OK; + + /*Begin object */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + /*Property Name */ + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + + while (result == AZ_OK) + { + if (az_json_token_is_text_equal(&payload_json_reader->token, jws_sha256_json_value)) + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + break; + } + else + { + _az_adu_jws_return_if_failed(az_json_reader_next_token(payload_json_reader)); + _az_adu_jws_return_if_failed(az_json_reader_skip_children(payload_json_reader)); + result = az_json_reader_next_token(payload_json_reader); + } + } + + if (result != AZ_OK) + { + Logger.Error("[JWS] Parse manifest SHA error: " + String(result, HEX)); + return result; + } + + if (payload_json_reader->token.kind != AZ_JSON_TOKEN_STRING) + { + Logger.Error("[JWS] JSON token type wrong | type: " + String(payload_json_reader->token.kind)); + return AZ_ERROR_JSON_INVALID_STATE; + } + + *sha_span_ptr = payload_json_reader->token.slice; + + return AZ_OK; +} + +static az_result base64_decode_jwk(jws_validation_context* manifest_context) +{ + az_result result; + + result = az_base64_url_decode( + manifest_context->jwk_header, + manifest_context->jwk_base64_encoded_header, + &manifest_context->out_jwk_header_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWK header az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsJWK_HEADER_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->jwk_header + = az_span_slice(manifest_context->jwk_header, 0, manifest_context->out_jwk_header_length); + + result = az_base64_url_decode( + manifest_context->jwk_payload, + manifest_context->jwk_base64_encoded_payload, + &manifest_context->out_jwk_payload_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWK payload az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsJWK_PAYLOAD_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->jwk_payload + = az_span_slice(manifest_context->jwk_payload, 0, manifest_context->out_jwk_payload_length); + + result = az_base64_url_decode( + manifest_context->jwk_signature, + manifest_context->jwk_base64_encoded_signature, + &manifest_context->out_jwk_signature_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWK signature az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsSIGNATURE_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->jwk_signature = az_span_slice( + manifest_context->jwk_signature, 0, manifest_context->out_jwk_signature_length); + + return AZ_OK; +} + +static az_result base64_decode_signing_key(jws_validation_context* manifest_context) +{ + az_result result; + + result = az_base64_decode( + manifest_context->signing_key_n, + manifest_context->base64_encoded_n_span, + &manifest_context->out_signing_key_n_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] Signing key n az_base64_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsRSA3072_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->signing_key_n = az_span_slice( + manifest_context->signing_key_n, 0, manifest_context->out_signing_key_n_length); + + result = az_base64_decode( + manifest_context->signing_key_e, + manifest_context->base64_encoded_e_span, + &manifest_context->out_signing_key_e_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] Signing key e az_base64_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error( + "[JWS] Decode buffer was too small: " + String(jwsSIGNING_KEY_E_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->signing_key_e = az_span_slice( + manifest_context->signing_key_e, 0, manifest_context->out_signing_key_e_length); + + return AZ_OK; +} + +static az_result base64_decode_jws_header_and_payload(jws_validation_context* manifest_context) +{ + az_result result; + + result = az_base64_url_decode( + manifest_context->jws_payload, + manifest_context->base64_encoded_payload, + &manifest_context->out_jws_payload_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWS payload az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsJWS_PAYLOAD_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->jws_payload + = az_span_slice(manifest_context->jws_payload, 0, manifest_context->out_jws_payload_length); + + result = az_base64_url_decode( + manifest_context->jws_signature, + manifest_context->base64_encoded_signature, + &manifest_context->out_jws_signature_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWS signature az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsSIGNATURE_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->jws_signature = az_span_slice( + manifest_context->jws_signature, 0, manifest_context->out_jws_signature_length); + + return AZ_OK; +} + +static az_result validate_root_key(jws_validation_context* manifest_context) +{ + az_result result; + az_json_reader json_reader; + + result = az_json_reader_init(&json_reader, manifest_context->jwk_header, NULL); + if (az_result_failed(result)) + { + Logger.Error("[JWS] az_json_reader_init failed: result " + String(result, HEX)); + + return result; + } + + if (find_root_key_value(&json_reader, &manifest_context->kid_span) != AZ_OK) + { + Logger.Error("Could not find kid in JSON"); + return AZ_ERROR_ITEM_NOT_FOUND; + } + + if (!az_span_is_content_equal( + az_span_create( + (uint8_t*)azure_iot_adu_root_key_id, sizeof(azure_iot_adu_root_key_id) - 1), + manifest_context->kid_span)) + { + Logger.Error("[JWS] Using the wrong root key"); + return AZ_ERROR_NOT_SUPPORTED; + } + + return AZ_OK; +} + +static az_result verify_sha_match(jws_validation_context* manifest_context, az_span manifest_span) +{ + az_json_reader json_reader; + az_result verification_result; + az_result result; + + verification_result + = jws_sha256_calculate(manifest_span, manifest_context->manifest_sha_calculation); + + if (verification_result != AZ_OK) + { + Logger.Error("[JWS] SHA256 Calculation failed"); + return verification_result; + } + + result = az_json_reader_init(&json_reader, manifest_context->jws_payload, NULL); + if (az_result_failed(result)) + { + Logger.Error("[JWS] az_json_reader_init failed: result " + String(result, HEX)); + + return result; + } + + if (find_manifest_sha(&json_reader, &manifest_context->sha256_span) != AZ_OK) + { + Logger.Error("Error finding manifest signature SHA"); + return AZ_ERROR_ITEM_NOT_FOUND; + } + + result = az_base64_decode( + manifest_context->parsed_manifest_sha, + manifest_context->sha256_span, + &manifest_context->out_parsed_manifest_sha_size); + + if (az_result_failed(result)) + { + Logger.Error( + "[JWS] Parsed manifest SHA az_base64_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsSHA256_SIZE) + " bytes"); + } + + return result; + } + + manifest_context->parsed_manifest_sha = az_span_slice( + manifest_context->parsed_manifest_sha, 0, manifest_context->out_parsed_manifest_sha_size); + + if (manifest_context->out_parsed_manifest_sha_size != jwsSHA256_SIZE) + { + Logger.Error( + "[JWS] Base64 decoded SHA256 is not the correct length | expected: " + + String(jwsSHA256_SIZE) + + " | actual: " + String(manifest_context->out_parsed_manifest_sha_size)); + return AZ_ERROR_ITEM_NOT_FOUND; + } + + int32_t comparison_result = memcmp( + az_span_ptr(manifest_context->manifest_sha_calculation), + az_span_ptr(manifest_context->parsed_manifest_sha), + jwsSHA256_SIZE); + + if (comparison_result != 0) + { + Logger.Error("[JWS] Calculated manifest SHA does not match SHA in payload"); + return AZ_ERROR_NOT_SUPPORTED; + } + else + { + Logger.Info(("[JWS] Calculated manifest SHA matches parsed SHA")); + } + + return AZ_OK; +} + +az_result SampleJWS::ManifestAuthenticate( + az_span manifest_span, + az_span jws_span, + az_span scratch_buffer_span) +{ + az_result result; + az_json_reader json_reader; + jws_validation_context manifest_context = { 0 }; + + /* Break up scratch buffer for reusable and persistent sections */ + uint8_t* persistent_scratch_space_head = az_span_ptr(scratch_buffer_span); + uint8_t* reusable_scratch_space_root + = persistent_scratch_space_head + jwsJWS_HEADER_SIZE + jwsJWK_PAYLOAD_SIZE; + uint8_t* reusable_scratch_space_head = reusable_scratch_space_root; + + /*------------------- Parse and Decode the JWS Header + * ------------------------*/ + + result = split_jws( + jws_span, + &manifest_context.base64_encoded_header, + &manifest_context.base64_encoded_payload, + &manifest_context.base64_encoded_signature); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] split_jws failed"); + return result; + } + + /* Note that we do not use mbedTLS to base64 decode values since we need the + * ability to assume padding characters. */ + /* mbedTLS will stop the decoding short and we would then need to add in the + * remaining characters. */ + manifest_context.jws_header = az_span_create(persistent_scratch_space_head, jwsJWS_HEADER_SIZE); + persistent_scratch_space_head += jwsJWS_HEADER_SIZE; + result = az_base64_url_decode( + manifest_context.jws_header, + manifest_context.base64_encoded_header, + &manifest_context.out_jws_header_length); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] JWS header az_base64_url_decode failed: result " + String(result, HEX)); + + if (result == AZ_ERROR_NOT_ENOUGH_SPACE) + { + Logger.Error("[JWS] Decode buffer was too small: " + String(jwsJWS_HEADER_SIZE) + " bytes"); + } + + return result; + } + + manifest_context.jws_header + = az_span_slice(manifest_context.jws_header, 0, manifest_context.out_jws_header_length); + + /*------------------- Parse SJWK JSON Payload ------------------------*/ + + /* The "sjwk" is the signed signing public key */ + result = az_json_reader_init(&json_reader, manifest_context.jws_header, NULL); + if (az_result_failed(result)) + { + Logger.Error("[JWS] az_json_reader_init failed: result " + String(result, HEX)); + return result; + } + + result = find_sjwk_value(&json_reader, &manifest_context.jwk_manifest_span); + if (az_result_failed(result)) + { + Logger.Error("Error finding sjwk value in payload"); + return AZ_ERROR_ITEM_NOT_FOUND; + } + + /*------------------- Split JWK and Base64 Decode the JWK Payload + * ------------------------*/ + + result = split_jws( + manifest_context.jwk_manifest_span, + &manifest_context.jwk_base64_encoded_header, + &manifest_context.jwk_base64_encoded_payload, + &manifest_context.jwk_base64_encoded_signature); + + if (az_result_failed(result)) + { + Logger.Error("[JWS] split_jws failed"); + return result; + } + + manifest_context.jwk_header = az_span_create(reusable_scratch_space_head, jwsJWK_HEADER_SIZE); + reusable_scratch_space_head += jwsJWK_HEADER_SIZE; + /* Needs to be persisted so we can parse the signing key N and E later */ + manifest_context.jwk_payload = az_span_create(persistent_scratch_space_head, jwsJWK_PAYLOAD_SIZE); + persistent_scratch_space_head += jwsJWK_PAYLOAD_SIZE; + manifest_context.jwk_signature = az_span_create(reusable_scratch_space_head, jwsSIGNATURE_SIZE); + reusable_scratch_space_head += jwsSIGNATURE_SIZE; + + result = base64_decode_jwk(&manifest_context); + if (az_result_failed(result)) + { + Logger.Error("[JWS] base64_decode_jwk failed: result " + String(result, HEX)); + return result; + } + + /*------------------- Parse root key id ------------------------*/ + + validate_root_key(&manifest_context); + + /*------------------- Parse necessary pieces for signing key + * ------------------------*/ + + result = az_json_reader_init(&json_reader, manifest_context.jwk_payload, NULL); + if (az_result_failed(result)) + { + Logger.Error("[JWS] az_json_reader_init failed: result " + String(result, HEX)); + return result; + } + + if (find_key_parts( + &json_reader, + &manifest_context.base64_encoded_n_span, + &manifest_context.base64_encoded_e_span, + &manifest_context.alg_span) + != AZ_OK) + { + Logger.Error("Could not find parts for the signing key"); + return AZ_ERROR_ITEM_NOT_FOUND; + } + + /*------------------- Verify the signature ------------------------*/ + + manifest_context.scratch_calculation_buffer + = az_span_create(reusable_scratch_space_head, jwsSHA_CALCULATION_SCRATCH_SIZE); + reusable_scratch_space_head += jwsSHA_CALCULATION_SCRATCH_SIZE; + result = jws_rs256_verify( + az_span_create( + az_span_ptr(manifest_context.jwk_base64_encoded_header), + az_span_size(manifest_context.jwk_base64_encoded_header) + + az_span_size(manifest_context.jwk_base64_encoded_payload) + 1), + manifest_context.jwk_signature, + az_span_create((uint8_t*)azure_iot_adu_root_key_n, sizeof(azure_iot_adu_root_key_n)), + az_span_create((uint8_t*)azure_iot_adu_root_key_e, sizeof(azure_iot_adu_root_key_e)), + manifest_context.scratch_calculation_buffer); + + if (result != AZ_OK) + { + Logger.Error("[JWS] jws_rs256_verify failed"); + return result; + } + + /*------------------- Reuse Buffer Space ------------------------*/ + + /* The JWK verification is now done, so we can reuse the buffers which it + * used. */ + reusable_scratch_space_head = reusable_scratch_space_root; + + /*------------------- Decode remaining values from JWS + * ------------------------*/ + + manifest_context.jws_payload = az_span_create(reusable_scratch_space_head, jwsJWS_PAYLOAD_SIZE); + reusable_scratch_space_head += jwsJWS_PAYLOAD_SIZE; + manifest_context.jws_signature = az_span_create(reusable_scratch_space_head, jwsSIGNATURE_SIZE); + reusable_scratch_space_head += jwsSIGNATURE_SIZE; + + result = base64_decode_jws_header_and_payload(&manifest_context); + + if (result != AZ_OK) + { + Logger.Error("[JWS] base64_decode_jws_header_and_payload failed"); + return result; + } + + /*------------------- Base64 decode the signing key ------------------------*/ + + manifest_context.signing_key_n = az_span_create(reusable_scratch_space_head, jwsRSA3072_SIZE); + reusable_scratch_space_head += jwsRSA3072_SIZE; + manifest_context.signing_key_e + = az_span_create(reusable_scratch_space_head, jwsSIGNING_KEY_E_SIZE); + reusable_scratch_space_head += jwsSIGNING_KEY_E_SIZE; + + result = base64_decode_signing_key(&manifest_context); + + if (result != AZ_OK) + { + Logger.Error("[JWS] base64_decode_signing_key failed"); + return result; + } + + /*------------------- Verify that the signature was signed by signing key + * ------------------------*/ + + if (!az_span_is_content_equal(manifest_context.alg_span, jws_alg_rs256)) + { + Logger.Error("[JWS] Algorithm not supported"); + return AZ_ERROR_NOT_SUPPORTED; + } + + result = jws_rs256_verify( + az_span_create( + az_span_ptr(manifest_context.base64_encoded_header), + az_span_size(manifest_context.base64_encoded_header) + + az_span_size(manifest_context.base64_encoded_payload) + 1), + manifest_context.jws_signature, + manifest_context.signing_key_n, + manifest_context.signing_key_e, + manifest_context.scratch_calculation_buffer); + + if (result != AZ_OK) + { + Logger.Error("[JWS] Verification of signed manifest SHA failed"); + return result; + } + + /*------------------- Verify that the SHAs match ------------------------*/ + + manifest_context.manifest_sha_calculation + = az_span_create(reusable_scratch_space_head, jwsSHA256_SIZE); + reusable_scratch_space_head += jwsSHA256_SIZE; + manifest_context.parsed_manifest_sha + = az_span_create(reusable_scratch_space_head, jwsSHA256_SIZE); + reusable_scratch_space_head += jwsSHA256_SIZE; + + return verify_sha_match(&manifest_context, manifest_span); +} diff --git a/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.h b/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.h new file mode 100644 index 00000000..6b6d6f6a --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/SampleAduJWS.h @@ -0,0 +1,57 @@ +/* Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. */ + +/** + * @file + * + * @brief APIs to authenticate an ADU manifest. + * + */ + +#ifndef SAMPLEADUJWS_H +#define SAMPLEADUJWS_H + +#include + +#include + +#define jwsPKCS7_PAYLOAD_OFFSET 19 + +#define jwsRSA3072_SIZE 384 +#define jwsSHA256_SIZE 32 +#define jwsJWS_HEADER_SIZE 1400 +#define jwsJWS_PAYLOAD_SIZE 60 +#define jwsJWK_HEADER_SIZE 48 +#define jwsJWK_PAYLOAD_SIZE 700 +#define jwsSIGNATURE_SIZE 400 +#define jwsSIGNING_KEY_E_SIZE 10 +#define jwsSIGNING_KEY_N_SIZE jwsRSA3072_SIZE +#define jwsSHA_CALCULATION_SCRATCH_SIZE jwsRSA3072_SIZE + jwsSHA256_SIZE + +/* This is the minimum amount of space needed to store values which are held at + * the same time. jwsJWS_PAYLOAD_SIZE, one jwsSIGNATURE_SIZE, and one + * jwsSHA256_SIZE are excluded since they will reuse buffer space. */ +#define jwsSCRATCH_BUFFER_SIZE \ + (jwsJWS_HEADER_SIZE + jwsJWK_HEADER_SIZE + jwsJWK_PAYLOAD_SIZE + jwsSIGNATURE_SIZE \ + + jwsSIGNING_KEY_N_SIZE + jwsSIGNING_KEY_E_SIZE + jwsSHA_CALCULATION_SCRATCH_SIZE) + +namespace SampleJWS +{ +/** + * @brief Authenticate the manifest from ADU. + * + * @param[in] manifest_span The escaped manifest from the ADU twin property. + * @param[in] jws_span The JWS used to authenticate \p manifest_span. + * @param[in] scratch_buffer_span Scratch buffer space for calculations. It + * should be `jwsSCRATCH_BUFFER_SIZE` in length. + * @return az_result The return value of this function. + * @retval AZ_OK if successful. + * @retval Otherwise if failed. + */ +az_result ManifestAuthenticate( + az_span manifest_span, + az_span jws_span, + az_span scratch_buffer_span); +}; // namespace SampleJWS + +#endif /* SAMPLEADUJWS_H */ diff --git a/examples/Azure_IoT_Adu_ESP32/SerialLogger.cpp b/examples/Azure_IoT_Adu_ESP32/SerialLogger.cpp new file mode 100644 index 00000000..1e16dd04 --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/SerialLogger.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "SerialLogger.h" +#include + +#define UNIX_EPOCH_START_YEAR 1900 + +static void writeTime() +{ + struct tm* ptm; + time_t now = time(NULL); + + ptm = gmtime(&now); + + Serial.print(ptm->tm_year + UNIX_EPOCH_START_YEAR); + Serial.print("/"); + Serial.print(ptm->tm_mon + 1); + Serial.print("/"); + Serial.print(ptm->tm_mday); + Serial.print(" "); + + if (ptm->tm_hour < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_hour); + Serial.print(":"); + + if (ptm->tm_min < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_min); + Serial.print(":"); + + if (ptm->tm_sec < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_sec); +} + +SerialLogger::SerialLogger() { Serial.begin(SERIAL_LOGGER_BAUD_RATE); } + +void SerialLogger::Info(String message) +{ + writeTime(); + Serial.print(" [INFO] "); + Serial.println(message); +} + +void SerialLogger::Error(String message) +{ + writeTime(); + Serial.print(" [ERROR] "); + Serial.println(message); +} + +SerialLogger Logger; diff --git a/examples/Azure_IoT_Adu_ESP32/SerialLogger.h b/examples/Azure_IoT_Adu_ESP32/SerialLogger.h new file mode 100644 index 00000000..66f918b6 --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/SerialLogger.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef SERIALLOGGER_H +#define SERIALLOGGER_H + +#include + +#ifndef SERIAL_LOGGER_BAUD_RATE +#define SERIAL_LOGGER_BAUD_RATE 115200 +#endif + +class SerialLogger +{ +public: + SerialLogger(); + void Info(String message); + void Error(String message); +}; + +extern SerialLogger Logger; + +#endif // SERIALLOGGER_H diff --git a/examples/Azure_IoT_Adu_ESP32/docs/lib.png b/examples/Azure_IoT_Adu_ESP32/docs/lib.png new file mode 100644 index 0000000000000000000000000000000000000000..ee91ec4b25eedbb63fadf34a3fbc01fc1410ba71 GIT binary patch literal 41495 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 literal 0 HcmV?d00001 diff --git a/examples/Azure_IoT_Adu_ESP32/docs/tagged-twin.png b/examples/Azure_IoT_Adu_ESP32/docs/tagged-twin.png new file mode 100644 index 0000000000000000000000000000000000000000..fc76c4976901cfde531c21e2084372c6ac083135 GIT binary patch literal 57973 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 literal 0 HcmV?d00001 diff --git a/examples/Azure_IoT_Adu_ESP32/iot_configs.h b/examples/Azure_IoT_Adu_ESP32/iot_configs.h new file mode 100644 index 00000000..ad3949ab --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/iot_configs.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +// Wifi +#define IOT_CONFIG_WIFI_SSID "SSID" +#define IOT_CONFIG_WIFI_PASSWORD "PWD" + +// Enable macro IOT_CONFIG_USE_X509_CERT to use an x509 certificate to +// authenticate the IoT device. The two main modes of authentication are through +// SAS tokens (automatically generated by the sample using the provided device +// symmetric key) or through x509 certificates. Please choose the appropriate +// option according to your device authentication mode. + +// #define IOT_CONFIG_USE_X509_CERT + +#ifdef IOT_CONFIG_USE_X509_CERT + +/* + * Please set the define IOT_CONFIG_DEVICE_CERT below with + * the content of your device x509 certificate. + * + * Example: + * #define IOT_CONFIG_DEVICE_CERT "-----BEGIN CERTIFICATE-----\r\n" \ + * "MIIBJDCBywIUfeHrebBVa2eZAbouBgACp9R3BncwCgYIKoZIzj0EAwIwETEPMA0G\r\n" \ + * "A1UEAwwGRFBTIENBMB4XDTIyMDMyMjazMTAzN1oXDTIzMDMyMjIzMTAzN1owGTEX\r\n" \ + * "MBUGA1UEAwwOY29udG9zby1kZXZpY2UwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC\r\n" \ + * ....... + * "YmGzdaHTb6P1W+p+jmc+jJn1MAoGCXqGSM49BAMCA0gAMEUCIEnbEMsAdGFroMwl\r\n" \ + * "vTfQahwsxN3xink9z1gtirrjQlqDAiEAyU+6TUJcG6d9JF+uJqsLFpsbbF3IzGAw\r\n" \ + * "yC+koNRC0MU=\r\n" \ + * "-----END CERTIFICATE-----" + * + */ +#define IOT_CONFIG_DEVICE_CERT "Device Certificate" + +/* + * Please set the define IOT_CONFIG_DEVICE_CERT_PRIVATE_KEY below with + * the content of your device x509 private key. + * + * Example: + * + * #define IOT_CONFIG_DEVICE_CERT_PRIVATE_KEY "-----BEGIN EC PRIVATE + * KEY-----\r\n" \ + * "MHcCAQEEIKGXkMMiO9D7jYpUjUGTBn7gGzeKPeNzCP83wbfQfLd9obAoGCCqGSM49\r\n" \ + * "AwEHoUQDQgAEU6nQoYbjgJvBwaeD6MyAYmOSDg0QhEdyyV337qrlIbDEKvFsn1El\r\n" \ + * "yRabc4dNp2Jhs3Xh02+j9Vvqfo5nPoyZ9Q==\r\n" \ + * "-----END EC PRIVATE KEY-----" + * + * Note the type of key may different in your case. Such as BEGIN PRIVATE KEY + * or BEGIN RSA PRIVATE KEY. + * + */ +#define IOT_CONFIG_DEVICE_CERT_PRIVATE_KEY "Device Certificate Private Key" + +#endif // IOT_CONFIG_USE_X509_CERT + +// Azure IoT +#define IOT_CONFIG_IOTHUB_FQDN "[your Azure IoT host name].azure-devices.net" +#define IOT_CONFIG_DEVICE_ID "Device ID" +// Use device key if not using certificates +#ifndef IOT_CONFIG_USE_X509_CERT +#define IOT_CONFIG_DEVICE_KEY "Device Key" +#endif // IOT_CONFIG_USE_X509_CERT + +// Azure IoT ADU values +#define ADU_DEVICE_MANUFACTURER "ESPRESSIF" +#define ADU_DEVICE_MODEL "ESP32-Embedded" +#define ADU_UPDATE_PROVIDER "ESPRESSIF" +#define ADU_UPDATE_NAME "ESP32-Embedded" +#define ADU_DEVICE_VERSION "1.0" + +// Publish 1 message every 2 seconds +#define TELEMETRY_FREQUENCY_MILLISECS 2000 diff --git a/examples/Azure_IoT_Adu_ESP32/readme.md b/examples/Azure_IoT_Adu_ESP32/readme.md new file mode 100644 index 00000000..aa684e2b --- /dev/null +++ b/examples/Azure_IoT_Adu_ESP32/readme.md @@ -0,0 +1,272 @@ +--- +page_type: sample +description: Connecting an ESP32 device to Azure IoT using the Azure SDK for Embedded C +languages: +- c +products: +- azure-iot +- azure-iot-pnp +- azure-iot-dps +- azure-iot-hub +--- + +# How to Setup and Run Azure SDK for Embedded C ADU on Espressif ESP32 + +> ## Before you proceed +> +>_This sample is based on the Azure IoT SDK for C, uses a bare metal (no RTOS) approach and has support for Arduino IDE._ +> +>_There is a different sample for the ESP32 with the Azure IoT middleware for FreeRTOS (requires FreeRTOS to work)._ +> +> _If this is what you’re looking for please visit [this GitHub repo](https://github.com/Azure-Samples/iot-middleware-freertos-samples/blob/main/README.md)_. + +- [Introduction](#introduction) + - [What is Covered](#what-is-covered) +- [Prerequisites](#prerequisites) +- [Setup Instructions](#setup-instructions) +- [New Image Instructions](#new-image-instructions) + - [Generate the ADU Update Manifest](#generate-the-adu-update-manifest) + - [Import the Update Manifest](#import-the-update-manifest) + - [Tag Your Device](#tag-your-device) +- [Upload Base Image Instructions](#upload-base-image-instructions) + - [Deploy Update](#deploy-update) +- [Certificates - Important to know](#certificates---important-to-know) + - [Additional Information](#additional-information) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + - [License](#license) + +## Introduction + +This is a "to-the-point" guide outlining how to run an Azure SDK for Embedded C ADU sample on an ESP32 microcontroller. The command line examples are tailored to Debian/Ubuntu environments. + +### What is Covered + +- Configuration instructions for the Arduino IDE to compile a sample using the [Azure SDK for Embedded C](https://github.com/Azure/azure-sdk-for-c). +- Setting up and configuring the necessary services for the scenario. +- Configuration, build, and run instructions for the IoT ADU sample. + +_The following was run on Windows 11 and Ubuntu Desktop 20.04 environments, with Arduino IDE 1.8.19 and ESP32 board library version 2.0.4._ + +## Prerequisites + +- Have an [Azure account](https://azure.microsoft.com/) created. +- Have an [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal) created. +- Have an [Azure Device Update](https://docs.microsoft.com/azure/iot-hub-device-update/create-device-update-account?tabs=portal) instance created and linked to your Azure IoT Hub. +- Have a logical device created in your Azure IoT Hub: using authentication type "Symmetric Key" or "X.509 self-signed". + - **Symmetric Key**: follow [this guidance](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal#register-a-new-device-in-the-iot-hub) to create a device.In this case, the device keys are used to automatically generate a SAS token for authentication. + - **X.509 self-signed cert**: Instructions on how to create an X.509 cert for tests can be found [here](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/samples/iot/docs/how_to_iot_hub_samples_linux.md#configure-and-run-the-samples) (Step 1). Please note that you might need to install some of the [prerequisites](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/samples/iot/docs/how_to_iot_hub_samples_linux.md#prerequisites) like OpenSSL. +- Have the latest [Arduino IDE](https://www.arduino.cc/en/Main/Software) installed. + +- Have the [ESP32 board support](https://github.com/espressif/arduino-esp32) installed on Arduino IDE. + + - ESP32 boards are not natively supported by Arduino IDE, so you need to add them manually. + - Follow the [instructions](https://github.com/espressif/arduino-esp32) in the official ESP32 repository. + +## Setup Instructions + +1. Run the Arduino IDE. + +1. Install the Azure SDK for Embedded C library. + + - On the Arduino IDE, go to menu `Sketch`, `Include Library`, `Manage Libraries...`. + - Search for and install `azure-sdk-for-c`. + - **Make sure to install the `1.1.0-beta.1` to use the preview version of the Embedded C SDK.** + +1. Open the ESPRESSIF ESP32 sample. + + - On the Arduino IDE, go to menu `File`, `Examples`, `azure-sdk-for-c`. + - Click on `Azure_IoT_Adu_ESP32` to open the sample. + +1. Configure the ESPRESSIF ESP32 sample. + + Enter your Azure IoT Hub and device information into the sample's `iot_configs.h`: + - Add your Wi-Fi SSID to `IOT_CONFIG_WIFI_SSID` + - Add your Wi-Fi password to `IOT_CONFIG_WIFI_PASSWORD` + - Add you IoTHub Name to `IOT_CONFIG_IOTHUB_FQDN` + - Add your Device ID to `IOT_CONFIG_DEVICE_ID` + - If using **X.509 Cert**: + - Uncomment the `#define IOT_CONFIG_USE_X509_CERT` + - Add your cert to `IOT_CONFIG_USE_X509_CERT` + - Add your cert PK to `IOT_CONFIG_DEVICE_CERT_PRIVATE_KEY` + - If using **Symmetric Key**: + - Add your device key to `IOT_CONFIG_DEVICE_KEY` + - **IMPORTANT**: make sure to change the `ADU_DEVICE_VERSION` to version 1.1 so that we can build the new update image. + +1. Select the appropriate partition scheme for your device. Go to `Tools` -> `Partition Scheme` -> `Minimal SPIFFS`. + +## New Image Instructions + +In order to update our device, we have to build the image which our device will update to. We will have to direct the Arduino IDE to specify an output directory so that we can easily find the binary. Open the `preferences.txt` located at `C:\Users\\AppData\Local\Arduino15\` and add `build.path=C:\Arduino-output` (or whichever directory you prefer). + +1. Connect the ESP32 microcontroller to your USB port. + +2. On the Arduino IDE, select the board and port. + + - Go to menu `Tools`, `Board` and select `ESP32`. + - Go to menu `Tools`, `Port` and select the port to which the microcontroller is connected. + +3. Click on "Verify" to build the update image. Make sure you changed the `ADU_DEVICE_VERSION` in your `iot_configs.h` file to `1.1`. You should now have a file called `Azure_IoT_Adu_ESP32.ino.bin` in your output directory. Copy that file to a new directory `C:\ADU-update`, and rename it `Azure_IoT_Adu_ESP32_1.1.bin` + +### Generate the ADU Update Manifest + +Navigate to the `C:\ADU-update` directory in a Powershell prompt. + +Clone the ADU toolset. + +```bash +git clone https://github.com/Azure/iot-hub-device-update +``` + +Generate the update manifest using **powershell**. + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process +Import-Module .\iot-hub-device-update\tools\AduCmdlets\AduUpdate.psm1 +$updateId = New-AduUpdateId -Provider "ESPRESSIF" -Name "ESP32-Embedded" -Version 1.1 +$compat = New-AduUpdateCompatibility -Properties @{ deviceManufacturer = 'ESPRESSIF'; deviceModel = 'ESP32-Embedded' } +$installStep = New-AduInstallationStep -Handler 'microsoft/swupdate:1'-HandlerProperties @{ installedCriteria = '1.1' } -Files C:\ADU-update\Azure_IoT_Adu_ESP32_1.1.bin +$update = New-AduImportManifest -UpdateId $updateId -Compatibility $compat -InstallationSteps $installStep +$update | Out-File "./$($updateId.provider).$($updateId.name).$($updateId.version).importmanifest.json" -Encoding utf8 +``` + +Verify you have the following files in your ADU-update directory: + +- `Azure_IoT_Adu_ESP32_1.1.bin` +- `ESPRESSIF.ESP32-Embedded.1.1.importmanifest.json` + +### Import the Update Manifest + +To import the update (`Azure_IoT_Adu_ESP32_1.1.bin`) and manifest (`ESPRESSIF.ESP32-Embedded.1.1.importmanifest.json`), follow the instructions at the link below: + +- [Import Update and Manifest](https://docs.microsoft.com/azure/iot-hub-device-update/import-update) + +### Tag Your Device + +Add the `"ADUGroup"` tag to the device's top-level twin document. This is used to group device together, and you may choose whichever title you prefer. + +```json +"tags": { + "ADUGroup": "" +}, +``` + +Viewing the device twin on the portal, the "tag" section should look similar to the following. Don't worry if you do or do not have a `"deviceUpdate"` section in the `"ADUGroup"` tag. ADU adds that as a default group. + +![img](./docs/tagged-twin.png) + +## Upload Base Image Instructions + +Now revert the `ADU_DEVICE_VERSION` in your `iot_configs.h` file to `1.0` to create the base image. + +1. Upload the sketch. + + - Go to menu `Sketch` and click on `Upload`. + +
Expected output of the upload: +

+ + ```text + Executable segment sizes: + IROM : 361788 - code in flash (default or ICACHE_FLASH_ATTR) + IRAM : 26972 / 32768 - code in IRAM (ICACHE_RAM_ATTR, ISRs...) + DATA : 1360 ) - initialized variables (global, static) in RAM/HEAP + RODATA : 2152 ) / 81920 - constants (global, static) in RAM/HEAP + BSS : 26528 ) - zeroed variables (global, static) in RAM/HEAP + Sketch uses 392272 bytes (37%) of program storage space. Maximum is 1044464 bytes. + Global variables use 30040 bytes (36%) of dynamic memory, leaving 51880 bytes for local variables. Maximum is 81920 bytes. + /home/user/.arduino15/packages/esp8266/tools/python3/3.7.2-post1/python3 /home/user/.arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/upload.py --chip esp8266 --port /dev/ttyUSB0 --baud 230400 --before default_reset --after hard_reset write_flash 0x0 /tmp/arduino_build_826987/azure_iot_hub_telemetry.ino.bin + esptool.py v2.8 + Serial port /dev/ttyUSB0 + Connecting.... + Chip is ESP8266EX + Features: WiFi + Crystal is 26MHz + MAC: dc:4f:22:5e:a7:09 + Uploading stub... + Running stub... + Stub running... + Changing baud rate to 230400 + Changed. + Configuring flash size... + Auto-detected Flash size: 4MB + Compressed 396432 bytes to 292339... + + Writing at 0x00000000... (5 %) + Writing at 0x00004000... (11 %) + Writing at 0x00008000... (16 %) + Writing at 0x0000c000... (22 %) + Writing at 0x00010000... (27 %) + Writing at 0x00014000... (33 %) + Writing at 0x00018000... (38 %) + Writing at 0x0001c000... (44 %) + Writing at 0x00020000... (50 %) + Writing at 0x00024000... (55 %) + Writing at 0x00028000... (61 %) + Writing at 0x0002c000... (66 %) + Writing at 0x00030000... (72 %) + Writing at 0x00034000... (77 %) + Writing at 0x00038000... (83 %) + Writing at 0x0003c000... (88 %) + Writing at 0x00040000... (94 %) + Writing at 0x00044000... (100 %) + Wrote 396432 bytes (292339 compressed) at 0x00000000 in 13.0 seconds (effective 243.4 kbit/s)... + Hash of data verified. + + Leaving... + Hard resetting via RTS pin... + ``` + +

+
+ +2. Monitor the MCU (microcontroller) locally via the Serial Port. + + - Go to menu `Tools`, `Serial Monitor`. + + If you perform this step right away after uploading the sketch, the serial monitor will show an output similar to the following upon success: + + ```text + Connecting to WIFI SSID buckaroo + .......................WiFi connected, IP address: + 192.168.1.123 + Setting time using SNTP..............................done! + Current time: Thu May 28 02:55:05 2020 + Client ID: mydeviceid + Username: myiothub.azure-devices.net/mydeviceid/?api-version=2018-06-30&DeviceClientType=c%2F1.0.0 + SharedAccessSignature sr=myiothub.azure-devices.net%2Fdevices%2Fmydeviceid&sig=placeholder-password&se=1590620105 + MQTT connecting ... connected. + ``` + +### Deploy Update + +To deploy the update to your ESP32, follow the link below: + +- [Deploy Update](https://docs.microsoft.com/azure/iot-hub-device-update/deploy-update) + + +## Certificates - Important to know + +The Azure IoT service certificates presented during TLS negotiation shall be always validated, on the device, using the appropriate trusted root CA certificate(s). + +The Azure SDK for C Arduino library automatically installs the root certificate used in the United States regions, and adds it to the Arduino sketch project when the library is included. + +For other regions (and private cloud environments), please use the appropriate root CA certificate. + +### Additional Information + +For important information and additional guidance about certificates, please refer to [this blog post](https://techcommunity.microsoft.com/t5/internet-of-things/azure-iot-tls-changes-are-coming-and-why-you-should-care/ba-p/1658456) from the security team. + +## Troubleshooting + +- The error policy for the Embedded C SDK client library is documented [here](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/docs/iot/mqtt_state_machine.md#error-policy). +- File an issue via [Github Issues](https://github.com/Azure/azure-sdk-for-c/issues/new/choose). +- Check [previous questions](https://stackoverflow.com/questions/tagged/azure+c) or ask new ones on StackOverflow using the `azure` and `c` tags. + +## Contributing + +This project welcomes contributions and suggestions. Find more contributing details [here](https://github.com/Azure/azure-sdk-for-c/blob/main/CONTRIBUTING.md). + +### License + +Azure SDK for Embedded C is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-c/blob/main/LICENSE) license. diff --git a/library.properties b/library.properties index 78e8428a..0b8dcf25 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ -name=Azure SDK for C -version=1.0.0 -author=Microsoft Corporation -maintainer=Microsoft Corporation -sentence=Azure SDK for C library for Arduino. -paragraph=This is an Arduino port of the Azure SDK for C (1.3.2). It allows you to use your Arduino device with Azure services like Azure IoT Hub and Azure Device Provisioning Service. See README.md for more details. Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. -category=Communication -url=https://github.com/Azure/azure-sdk-for-c/tree/1.3.2 -architectures=* -includes=az_core.h,az_iot.h,azure_ca.h +name=Azure SDK for C ADU +version=1.1.0-beta.1 +author=Microsoft Corporation +maintainer=Microsoft Corporation +sentence=Azure SDK for C library for Arduino. +paragraph=This is an Arduino port of the Azure SDK for C (1.4.0-beta.1). It allows you to use your Arduino device with Azure services like Azure IoT Hub and Azure Device Provisioning Service. See README.md for more details. Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. +category=Communication +url=https://github.com/Azure/azure-sdk-for-c/tree/1.4.0-beta.1 +architectures=* +includes=az_core.h,az_iot.h,azure_ca.h diff --git a/src/az_base64.c b/src/az_base64.c index 99f2531d..61ad8ff7 100644 --- a/src/az_base64.c +++ b/src/az_base64.c @@ -12,269 +12,15 @@ #define _az_ENCODING_PAD '=' +typedef enum +{ + _az_base64_mode_standard, + _az_base64_mode_url +} _az_base64_mode; + static char const _az_base64_encode_array[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -static int8_t const _az_base64_decode_array[256] = { - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - 62, - -1, - -1, - -1, - 63, // 62 is placed at index 43 (for +), 63 at index 47 (for /) - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - -1, - -1, - -1, - -1, - -1, - -1, // 52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) - -1, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - -1, - -1, - -1, - -1, - -1, // 0-25 are placed at index 65-90 (for A-Z) - -1, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - -1, - -1, - -1, - -1, - -1, // 26-51 are placed at index 97-122 (for a-z) - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, // Bytes over 122 ('z') are invalid and cannot be decoded. Hence, padding the map with 255, - // which indicates invalid input - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, - -1, -}; - static AZ_NODISCARD int32_t _az_base64_encode(uint8_t* three_bytes) { int32_t i = (*three_bytes << 16) | (*(three_bytes + 1) << 8) | *(three_bytes + 2); @@ -370,17 +116,70 @@ AZ_NODISCARD int32_t az_base64_get_max_encoded_size(int32_t source_bytes_size) return (((source_bytes_size + 2) / 3) * 4); } -static AZ_NODISCARD int32_t _az_base64_decode(uint8_t* encoded_bytes) +static int32_t _get_base64_decoded_char(int32_t c, _az_base64_mode mode) +{ + if (mode == _az_base64_mode_url) + { + if (c == '+' || c == '/') + { + return -1; // can't use + or / with URL encoding + } + + if (c == '-') + { + c = '+'; // - becomes a + + } + else if (c == '_') + { + c = '/'; // _ becomes a / + } + } + + if (c >= 'A' && c <= 'Z') + { + return (c - 'A'); + } + + if (c >= 'a' && c <= 'z') + { + return 26 + (c - 'a'); + } + + if (c >= '0' && c <= '9') + { + return 52 + (c - '0'); + } + + if (c == '+') + { + return 62; + } + + if (c == '/') + { + return 63; + } + + return -1; +} + +static AZ_NODISCARD int32_t +_az_base64_decode_four_bytes(uint8_t* encoded_bytes, _az_base64_mode mode) { int32_t i0 = *encoded_bytes; int32_t i1 = *(encoded_bytes + 1); int32_t i2 = *(encoded_bytes + 2); int32_t i3 = *(encoded_bytes + 3); - i0 = _az_base64_decode_array[i0]; - i1 = _az_base64_decode_array[i1]; - i2 = _az_base64_decode_array[i2]; - i3 = _az_base64_decode_array[i3]; + i0 = _get_base64_decoded_char(i0, mode); + i1 = _get_base64_decoded_char(i1, mode); + i2 = _get_base64_decoded_char(i2, mode); + i3 = _get_base64_decoded_char(i3, mode); + + if (i0 == -1 || i1 == -1 || i2 == -1 || i3 == -1) + { + return -1; + } i0 <<= 18; i1 <<= 12; @@ -400,25 +199,18 @@ static void _az_base64_write_three_low_order_bytes(uint8_t* destination, int32_t *(destination + 2) = (uint8_t)(value); } -AZ_NODISCARD az_result -az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* out_written) +static az_result _az_base64_decode( + az_span destination_bytes, + az_span source_base64_url_text, + int32_t* out_written, + _az_base64_mode mode) { - _az_PRECONDITION_VALID_SPAN(destination_bytes, 1, false); - _az_PRECONDITION_VALID_SPAN(source_base64_text, 4, false); - _az_PRECONDITION_NOT_NULL(out_written); - - int32_t source_length = az_span_size(source_base64_text); - uint8_t* source_ptr = az_span_ptr(source_base64_text); + int32_t source_length = az_span_size(source_base64_url_text); + uint8_t* source_ptr = az_span_ptr(source_base64_url_text); int32_t destination_length = az_span_size(destination_bytes); uint8_t* destination_ptr = az_span_ptr(destination_bytes); - // The input must be non-empty and a multiple of 4 to be valid. - if (source_length == 0 || source_length % 4 != 0) - { - return AZ_ERROR_UNEXPECTED_END; - } - if (destination_length < az_base64_get_max_decoded_size(source_length) - 2) { return AZ_ERROR_NOT_ENOUGH_SPACE; @@ -429,7 +221,7 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* while (source_index < source_length - 4) { - int32_t result = _az_base64_decode(source_ptr + source_index); + int32_t result = _az_base64_decode_four_bytes(source_ptr + source_index, mode); if (result < 0) { return AZ_ERROR_UNEXPECTED_CHAR; @@ -440,15 +232,19 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* source_index += 4; } - // We are guaranteed to have an input with at least 4 bytes at this point, with a size that is a - // multiple of 4. - int32_t i0 = *(source_ptr + source_length - 4); - int32_t i1 = *(source_ptr + source_length - 3); - int32_t i2 = *(source_ptr + source_length - 2); - int32_t i3 = *(source_ptr + source_length - 1); + // If using standard base64 decoding, there is a precondition guaranteeing size is divisible by 4. + // Otherwise with url encoding, we can assume padding characters. + // If length is divisible by four, do nothing. Else, we assume up to two padding characters. + int32_t source_length_mod_four = source_length % 4; + int32_t i0 = *(source_ptr + source_index); + int32_t i1 = *(source_ptr + source_index + 1); + int32_t i2 = source_length_mod_four == 2 ? _az_ENCODING_PAD : *(source_ptr + source_index + 2); + int32_t i3 = source_length_mod_four == 2 || source_length_mod_four == 3 + ? _az_ENCODING_PAD + : *(source_ptr + source_index + 3); - i0 = _az_base64_decode_array[i0]; - i1 = _az_base64_decode_array[i1]; + i0 = _get_base64_decoded_char(i0, mode); + i1 = _get_base64_decoded_char(i1, mode); i0 <<= 18; i1 <<= 12; @@ -457,8 +253,8 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* if (i3 != _az_ENCODING_PAD) { - i2 = _az_base64_decode_array[i2]; - i3 = _az_base64_decode_array[i3]; + i2 = _get_base64_decoded_char(i2, mode); + i3 = _get_base64_decoded_char(i3, mode); i2 <<= 6; @@ -478,7 +274,7 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* } else if (i2 != _az_ENCODING_PAD) { - i2 = _az_base64_decode_array[i2]; + i2 = _get_base64_decoded_char(i2, mode); i2 <<= 6; @@ -514,8 +310,55 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* return AZ_OK; } +AZ_NODISCARD az_result +az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* out_written) +{ + _az_PRECONDITION_VALID_SPAN(destination_bytes, 1, false); + _az_PRECONDITION_VALID_SPAN(source_base64_text, 4, false); + _az_PRECONDITION_NOT_NULL(out_written); + + int32_t source_length = az_span_size(source_base64_text); + + // The input must be non-empty and a multiple of 4 to be valid. + if (source_length == 0 || source_length % 4 != 0) + { + return AZ_ERROR_UNEXPECTED_END; + } + + return _az_base64_decode( + destination_bytes, source_base64_text, out_written, _az_base64_mode_standard); +} + AZ_NODISCARD int32_t az_base64_get_max_decoded_size(int32_t source_base64_text_size) { _az_PRECONDITION(source_base64_text_size >= 0); return (source_base64_text_size / 4) * 3; } + +AZ_NODISCARD az_result az_base64_url_decode( + az_span destination_bytes, + az_span source_base64_url_text, + int32_t* out_written) +{ + _az_PRECONDITION_VALID_SPAN(destination_bytes, 1, false); + _az_PRECONDITION_VALID_SPAN(source_base64_url_text, 2, false); + _az_PRECONDITION_NOT_NULL(out_written); + + int32_t source_length = az_span_size(source_base64_url_text); + + // The input must be non-empty and a minimum of two characters long. + // There can only be two assumed padding characters. + if (source_length == 0 || source_length % 4 == 1) + { + return AZ_ERROR_UNEXPECTED_END; + } + + return _az_base64_decode( + destination_bytes, source_base64_url_text, out_written, _az_base64_mode_url); +} + +AZ_NODISCARD int32_t az_base64_url_get_max_decoded_size(int32_t source_base64_url_text_size) +{ + _az_PRECONDITION(source_base64_url_text_size >= 0); + return (source_base64_url_text_size / 4) * 3; +} diff --git a/src/az_base64.h b/src/az_base64.h index dc80abfa..81a29c99 100644 --- a/src/az_base64.h +++ b/src/az_base64.h @@ -84,6 +84,41 @@ az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* */ AZ_NODISCARD int32_t az_base64_get_max_decoded_size(int32_t source_base64_text_size); +/** + * @brief Decodes the span of UTF-8 encoded text represented as base 64 url into binary data. + * + * @param destination_bytes The output #az_span where the decoded binary data should be copied to as + * a result of the operation. + * @param[in] source_base64_url_text The input #az_span that contains the base 64 text to be + * decoded. + * @param[out] out_written A pointer to an `int32_t` that receives the number of bytes written into + * the destination #az_span. This can be used to slice the output for subsequent calls, if + * necessary. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination_bytes is not large enough to contain + * the decoded text. + * @retval #AZ_ERROR_UNEXPECTED_CHAR The input \p source_base64_url_text contains characters outside + * of the expected base 64 range or is incomplete (that is, has length % 4 == 1 characters). + * @retval #AZ_ERROR_UNEXPECTED_END The input \p source_base64_url_text is incomplete (that is, it + * is of a size which is length % 4 == 1 characters). + */ +AZ_NODISCARD az_result az_base64_url_decode( + az_span destination_bytes, + az_span source_base64_url_text, + int32_t* out_written); + +/** + * @brief Returns the maximum length of the result if you were to decode an #az_span of the + * specified length which contained base 64 url encoded text. + * + * @param source_base64_url_text_size The size of the span containing base 64 encoded text. + * + * @return The maximum length of the result. + */ +AZ_NODISCARD int32_t az_base64_url_get_max_decoded_size(int32_t source_base64_url_text_size); + #include <_az_cfg_suffix.h> #endif // _az_BASE64_H diff --git a/src/az_iot.h b/src/az_iot.h index 92b49740..1f733612 100644 --- a/src/az_iot.h +++ b/src/az_iot.h @@ -15,6 +15,7 @@ #ifndef _az_IOT_H #define _az_IOT_H +#include #include #include #include diff --git a/src/az_iot_adu_client.c b/src/az_iot_adu_client.c new file mode 100644 index 00000000..78d4aca6 --- /dev/null +++ b/src/az_iot_adu_client.c @@ -0,0 +1,872 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include + +#include +#include +#include +#include + +/* Define the ADU agent component name. */ +#define AZ_IOT_ADU_CLIENT_AGENT_COMPONENT_NAME "deviceUpdate" + +#define AZ_IOT_ADU_CLIENT_AGENT_INTERFACE_ID "dtmi:azure:iot:deviceUpdate;1" + +/* Define the ADU agent property name "agent" and sub property names. */ +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_AGENT "agent" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DEVICEPROPERTIES "deviceProperties" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MANUFACTURER "manufacturer" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MODEL "model" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INTERFACE_ID "interfaceId" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ADU_VERSION "aduVer" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DO_VERSION "doVer" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_COMPAT_PROPERTY_NAMES "compatPropertyNames" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTALLED_UPDATE_ID "installedUpdateId" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_PROVIDER "provider" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_NAME "name" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_VERSION "version" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_LAST_INSTALL_RESULT "lastInstallResult" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_CODE "resultCode" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_EXTENDED_RESULT_CODE "extendedResultCode" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_DETAILS "resultDetails" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_STEP_RESULTS "stepResults" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_STATE "state" + +/* Define the ADU agent property name "service" and sub property names. */ +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SERVICE "service" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_WORKFLOW "workflow" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ACTION "action" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ID "id" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RETRY_TIMESTAMP "retryTimestamp" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_MANIFEST "updateManifest" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_MANIFEST_SIGNATURE "updateManifestSignature" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILEURLS "fileUrls" + +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MANIFEST_VERSION "manifestVersion" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_ID "updateId" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_COMPATIBILITY "compatibility" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DEVICE_MANUFACTURER "deviceManufacturer" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DEVICE_MODEL "deviceModel" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_GROUP "group" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTRUCTIONS "instructions" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_STEPS "steps" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_TYPE "type" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HANDLER "handler" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HANDLER_PROPERTIES "handlerProperties" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILES "files" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DETACHED_MANIFEST_FILED "detachedManifestFileId" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTALLED_CRITERIA "installedCriteria" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILE_NAME "fileName" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SIZE_IN_BYTES "sizeInBytes" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HASHES "hashes" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SHA256 "sha256" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_CREATED_DATE_TIME "createdDateTime" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DOWNLOAD_HANDLER "downloadHandler" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RELATED_FILES "relatedFiles" +#define AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MIME_TYPE "mimeType" + +#define NULL_TERM_CHAR_SIZE 1 + +#define RESULT_STEP_ID_PREFIX "step_" +#define MAX_UINT32_NUMBER_OF_DIGITS 10 +#define RESULT_STEP_ID_MAX_SIZE (sizeof(RESULT_STEP_ID_PREFIX) - 1 + MAX_UINT32_NUMBER_OF_DIGITS) + +#define RETURN_IF_JSON_TOKEN_NOT_TYPE(jr_ptr, json_token_type) \ + if (jr_ptr->token.kind != json_token_type) \ + { \ + return AZ_ERROR_JSON_INVALID_STATE; \ + } + +#define RETURN_IF_JSON_TOKEN_NOT_TEXT(jr_ptr, literal_text) \ + if (!az_json_token_is_text_equal(&jr_ptr->token, AZ_SPAN_FROM_STR(literal_text))) \ + { \ + return AZ_ERROR_JSON_INVALID_STATE; \ + } + +const uint8_t azure_iot_adu_root_key_id[13] = "ADU.200702.R"; +const uint8_t azure_iot_adu_root_key_n[385] + = { 0x00, 0xd5, 0x42, 0x2e, 0xaf, 0x11, 0x54, 0xa3, 0x50, 0x65, 0x87, 0xa2, 0x4d, 0x5b, 0xba, + 0x1a, 0xfb, 0xa9, 0x32, 0xdf, 0xe9, 0x99, 0x5f, 0x05, 0x45, 0xc8, 0xaf, 0xbd, 0x35, 0x1d, + 0x89, 0xe8, 0x27, 0x27, 0x58, 0xa3, 0xa8, 0xee, 0xc5, 0xc5, 0x1e, 0x4f, 0xf7, 0x92, 0xa6, + 0x12, 0x06, 0x7d, 0x3d, 0x7d, 0xb0, 0x07, 0xf6, 0x2c, 0x7f, 0xde, 0x6d, 0x2a, 0xf5, 0xbc, + 0x49, 0xbc, 0x15, 0xef, 0xf0, 0x81, 0xcb, 0x3f, 0x88, 0x4f, 0x27, 0x1d, 0x88, 0x71, 0x28, + 0x60, 0x08, 0xb6, 0x19, 0xd2, 0xd2, 0x39, 0xd0, 0x05, 0x1f, 0x3c, 0x76, 0x86, 0x71, 0xbb, + 0x59, 0x58, 0xbc, 0xb1, 0x88, 0x7b, 0xab, 0x56, 0x28, 0xbf, 0x31, 0x73, 0x44, 0x32, 0x10, + 0xfd, 0x3d, 0xd3, 0x96, 0x5c, 0xff, 0x4e, 0x5c, 0xb3, 0x6b, 0xff, 0x8b, 0x84, 0x9b, 0x8b, + 0x80, 0xb8, 0x49, 0xd0, 0x7d, 0xfa, 0xd6, 0x40, 0x58, 0x76, 0x4d, 0xc0, 0x72, 0x27, 0x75, + 0xcb, 0x9a, 0x2f, 0x9b, 0xb4, 0x9f, 0x0f, 0x25, 0xf1, 0x1c, 0xc5, 0x1b, 0x0b, 0x5a, 0x30, + 0x7d, 0x2f, 0xb8, 0xef, 0xa7, 0x26, 0x58, 0x53, 0xaf, 0xd5, 0x1d, 0x55, 0x01, 0x51, 0x0d, + 0xe9, 0x1b, 0xa2, 0x0f, 0x3f, 0xd7, 0xe9, 0x1d, 0x20, 0x41, 0xa6, 0xe6, 0x14, 0x0a, 0xae, + 0xfe, 0xf2, 0x1c, 0x2a, 0xd6, 0xe4, 0x04, 0x7b, 0xf6, 0x14, 0x7e, 0xec, 0x0f, 0x97, 0x83, + 0xfa, 0x58, 0xfa, 0x81, 0x36, 0x21, 0xb9, 0xa3, 0x2b, 0xfa, 0xd9, 0x61, 0x0b, 0x1a, 0x94, + 0xf7, 0xc1, 0xbe, 0x7f, 0x40, 0x14, 0x4a, 0xc9, 0xfa, 0x35, 0x7f, 0xef, 0x66, 0x70, 0x00, + 0xb1, 0xfd, 0xdb, 0xd7, 0x61, 0x0d, 0x3b, 0x58, 0x74, 0x67, 0x94, 0x89, 0x75, 0x76, 0x96, + 0x7c, 0x91, 0x87, 0xd2, 0x8e, 0x11, 0x97, 0xee, 0x7b, 0x87, 0x6c, 0x9a, 0x2f, 0x45, 0xd8, + 0x65, 0x3f, 0x52, 0x70, 0x98, 0x2a, 0xcb, 0xc8, 0x04, 0x63, 0xf5, 0xc9, 0x47, 0xcf, 0x70, + 0xf4, 0xed, 0x64, 0xa7, 0x74, 0xa5, 0x23, 0x8f, 0xb6, 0xed, 0xf7, 0x1c, 0xd3, 0xb0, 0x1c, + 0x64, 0x57, 0x12, 0x5a, 0xa9, 0x81, 0x84, 0x1f, 0xa0, 0xe7, 0x50, 0x19, 0x96, 0xb4, 0x82, + 0xb1, 0xac, 0x48, 0xe3, 0xe1, 0x32, 0x82, 0xcb, 0x40, 0x1f, 0xac, 0xc4, 0x59, 0xbc, 0x10, + 0x34, 0x51, 0x82, 0xf9, 0x28, 0x8d, 0xa8, 0x1e, 0x9b, 0xf5, 0x79, 0x45, 0x75, 0xb2, 0xdc, + 0x9a, 0x11, 0x43, 0x08, 0xbe, 0x61, 0xcc, 0x9a, 0xc4, 0xcb, 0x77, 0x36, 0xff, 0x83, 0xdd, + 0xa8, 0x71, 0x4f, 0x51, 0x8e, 0x0e, 0x7b, 0x4d, 0xfa, 0x79, 0x98, 0x8d, 0xbe, 0xfc, 0x82, + 0x7e, 0x40, 0x48, 0xa9, 0x12, 0x01, 0xa8, 0xd9, 0x7e, 0xf3, 0xa5, 0x1b, 0xf1, 0xfb, 0x90, + 0x77, 0x3e, 0x40, 0x87, 0x18, 0xc9, 0xab, 0xd9, 0xf7, 0x79 }; +const uint8_t azure_iot_adu_root_key_e[3] = { 0x01, 0x00, 0x01 }; + +const az_span default_compatibility_properties + = AZ_SPAN_LITERAL_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_DEFAULT_COMPATIBILITY_PROPERTIES); + +AZ_NODISCARD az_iot_adu_client_options az_iot_adu_client_options_default() +{ + return (az_iot_adu_client_options){ .device_compatibility_properties + = default_compatibility_properties }; +} + +AZ_NODISCARD az_result +az_iot_adu_client_init(az_iot_adu_client* client, az_iot_adu_client_options* options) +{ + _az_PRECONDITION_NOT_NULL(client); + + client->_internal.options = options == NULL ? az_iot_adu_client_options_default() : *options; + + return AZ_OK; +} + +AZ_NODISCARD bool az_iot_adu_client_is_component_device_update( + az_iot_adu_client* client, + az_span component_name) +{ + _az_PRECONDITION_NOT_NULL(client); + + (void)client; + + return az_span_is_content_equal( + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_COMPONENT_NAME), component_name); +} + +static az_result generate_step_id(az_span buffer, uint32_t step_index, az_span* step_id) +{ + az_result result; + *step_id = buffer; + buffer = az_span_copy(buffer, AZ_SPAN_FROM_STR(RESULT_STEP_ID_PREFIX)); + + result = az_span_u32toa(buffer, step_index, &buffer); + _az_RETURN_IF_FAILED(result); + + *step_id = az_span_slice(*step_id, 0, az_span_size(*step_id) - az_span_size(buffer)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_adu_client_get_agent_state_payload( + az_iot_adu_client* client, + az_iot_adu_client_device_properties* device_properties, + int32_t agent_state, + az_iot_adu_client_workflow* workflow, + az_iot_adu_client_install_result* last_install_result, + az_json_writer* ref_json_writer) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(device_properties); + _az_PRECONDITION_VALID_SPAN(device_properties->manufacturer, 1, false); + _az_PRECONDITION_VALID_SPAN(device_properties->model, 1, false); + _az_PRECONDITION_VALID_SPAN(device_properties->update_id, 1, false); + _az_PRECONDITION_VALID_SPAN(device_properties->adu_version, 1, false); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + + uint8_t step_id_scratch_buffer[7]; + + /* Update reported property */ + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + /* Fill the ADU agent component name. */ + _az_RETURN_IF_FAILED(az_iot_hub_client_properties_writer_begin_component( + NULL, ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_COMPONENT_NAME))); + + /* Fill the agent property name. */ + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_AGENT))); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + /* Fill the deviceProperties. */ + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DEVICEPROPERTIES))); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MANUFACTURER))); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(ref_json_writer, device_properties->manufacturer)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MODEL))); + _az_RETURN_IF_FAILED(az_json_writer_append_string(ref_json_writer, device_properties->model)); + + if (device_properties->custom_properties != NULL) + { + for (int32_t custom_property_index = 0; + custom_property_index < device_properties->custom_properties->count; + custom_property_index++) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, device_properties->custom_properties->names[custom_property_index])); + _az_RETURN_IF_FAILED(az_json_writer_append_string( + ref_json_writer, device_properties->custom_properties->values[custom_property_index])); + } + } + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INTERFACE_ID))); + _az_RETURN_IF_FAILED(az_json_writer_append_string( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_INTERFACE_ID))); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ADU_VERSION))); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(ref_json_writer, device_properties->adu_version)); + + if (!az_span_is_content_equal( + device_properties->delivery_optimization_agent_version, AZ_SPAN_EMPTY)) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DO_VERSION))); + _az_RETURN_IF_FAILED(az_json_writer_append_string( + ref_json_writer, device_properties->delivery_optimization_agent_version)); + } + + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + + /* Fill the compatibility property names. */ + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_COMPAT_PROPERTY_NAMES))); + _az_RETURN_IF_FAILED(az_json_writer_append_string( + ref_json_writer, client->_internal.options.device_compatibility_properties)); + + /* Add last installed update information */ + if (last_install_result != NULL) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_LAST_INSTALL_RESULT))); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_CODE))); + _az_RETURN_IF_FAILED( + az_json_writer_append_int32(ref_json_writer, last_install_result->result_code)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_EXTENDED_RESULT_CODE))); + _az_RETURN_IF_FAILED( + az_json_writer_append_int32(ref_json_writer, last_install_result->extended_result_code)); + + if (!az_span_is_content_equal(last_install_result->result_details, AZ_SPAN_EMPTY)) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_DETAILS))); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(ref_json_writer, last_install_result->result_details)); + } + + for (int32_t i = 0; i < last_install_result->step_results_count; i++) + { + az_span step_id = AZ_SPAN_FROM_BUFFER(step_id_scratch_buffer); + _az_RETURN_IF_FAILED(generate_step_id(step_id, (uint32_t)i, &step_id)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name(ref_json_writer, step_id)); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_CODE))); + _az_RETURN_IF_FAILED(az_json_writer_append_int32( + ref_json_writer, last_install_result->step_results[i].result_code)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_EXTENDED_RESULT_CODE))); + _az_RETURN_IF_FAILED(az_json_writer_append_int32( + ref_json_writer, last_install_result->step_results[i].extended_result_code)); + + if (!az_span_is_content_equal( + last_install_result->step_results[i].result_details, AZ_SPAN_EMPTY)) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RESULT_DETAILS))); + _az_RETURN_IF_FAILED(az_json_writer_append_string( + ref_json_writer, last_install_result->step_results[i].result_details)); + } + + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + } + + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + } + + /* Fill the agent state. */ + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_STATE))); + _az_RETURN_IF_FAILED(az_json_writer_append_int32(ref_json_writer, agent_state)); + + /* Fill the workflow. */ + if (workflow != NULL && (az_span_ptr(workflow->id) != NULL && az_span_size(workflow->id) > 0)) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_WORKFLOW))); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ACTION))); + _az_RETURN_IF_FAILED(az_json_writer_append_int32(ref_json_writer, workflow->action)); + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ID))); + _az_RETURN_IF_FAILED(az_json_writer_append_string(ref_json_writer, workflow->id)); + + /* Append retry timestamp in workflow if existed. */ + if (!az_span_is_content_equal(workflow->retry_timestamp, AZ_SPAN_EMPTY)) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RETRY_TIMESTAMP))); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(ref_json_writer, workflow->retry_timestamp)); + } + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + } + + /* Fill installed update id. */ + _az_RETURN_IF_FAILED(az_json_writer_append_property_name( + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTALLED_UPDATE_ID))); + _az_RETURN_IF_FAILED(az_json_writer_append_string(ref_json_writer, device_properties->update_id)); + + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + + _az_RETURN_IF_FAILED(az_iot_hub_client_properties_writer_end_component(NULL, ref_json_writer)); + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_adu_client_parse_service_properties( + az_iot_adu_client* client, + az_json_reader* ref_json_reader, + az_iot_adu_client_update_request* update_request) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_reader); + _az_PRECONDITION_NOT_NULL(update_request); + + (void)client; + + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_PROPERTY_NAME); + RETURN_IF_JSON_TOKEN_NOT_TEXT(ref_json_reader, AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SERVICE); + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + update_request->workflow.action = 0; + update_request->workflow.id = AZ_SPAN_EMPTY; + update_request->workflow.retry_timestamp = AZ_SPAN_EMPTY; + update_request->update_manifest = AZ_SPAN_EMPTY; + update_request->update_manifest_signature = AZ_SPAN_EMPTY; + update_request->file_urls_count = 0; + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_WORKFLOW))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ACTION))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + _az_RETURN_IF_FAILED( + az_json_token_get_int32(&ref_json_reader->token, &update_request->workflow.action)); + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_ID))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + update_request->workflow.id = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RETRY_TIMESTAMP))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + update_request->workflow.retry_timestamp = ref_json_reader->token.slice; + } + else + { + _az_LOG_WRITE( + AZ_LOG_IOT_ADU, + AZ_SPAN_FROM_STR("Unexpected property found in ADU manifest workflow:")); + _az_LOG_WRITE(AZ_LOG_IOT_ADU, ref_json_reader->token.slice); + return AZ_ERROR_JSON_INVALID_STATE; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_MANIFEST))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_NULL) + { + update_request->update_manifest = ref_json_reader->token.slice; + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_MANIFEST_SIGNATURE))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_NULL) + { + update_request->update_manifest_signature = ref_json_reader->token.slice; + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILEURLS))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_NULL) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE(ref_json_reader, AZ_JSON_TOKEN_PROPERTY_NAME); + + update_request->file_urls[update_request->file_urls_count].id + = ref_json_reader->token.slice; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_NULL) + { + update_request->file_urls[update_request->file_urls_count].url + = ref_json_reader->token.slice; + + update_request->file_urls_count++; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_adu_client_get_service_properties_response( + az_iot_adu_client* client, + int32_t version, + int32_t status, + az_json_writer* ref_json_writer) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + + (void)client; + + // Component and response status + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + _az_RETURN_IF_FAILED(az_iot_hub_client_properties_writer_begin_component( + NULL, ref_json_writer, AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_COMPONENT_NAME))); + _az_RETURN_IF_FAILED(az_iot_hub_client_properties_writer_begin_response_status( + NULL, + ref_json_writer, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SERVICE), + status, + version, + AZ_SPAN_EMPTY)); + + // It is not necessary to send the properties back in the acknowledgement. + // We opt not to send them to reduce the size of the payload. + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + + _az_RETURN_IF_FAILED( + az_iot_hub_client_properties_writer_end_response_status(NULL, ref_json_writer)); + _az_RETURN_IF_FAILED(az_iot_hub_client_properties_writer_end_component(NULL, ref_json_writer)); + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(ref_json_writer)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_adu_client_parse_update_manifest( + az_iot_adu_client* client, + az_json_reader* ref_json_reader, + az_iot_adu_client_update_manifest* update_manifest) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_reader); + _az_PRECONDITION_NOT_NULL(update_manifest); + + (void)client; + + // Initialize the update_manifest with empty values. + update_manifest->manifest_version = AZ_SPAN_EMPTY; + update_manifest->update_id.name = AZ_SPAN_EMPTY; + update_manifest->update_id.provider = AZ_SPAN_EMPTY; + update_manifest->update_id.version = AZ_SPAN_EMPTY; + update_manifest->instructions.steps_count = 0; + update_manifest->files_count = 0; + update_manifest->create_date_time = AZ_SPAN_EMPTY; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + bool property_parsed = true; + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MANIFEST_VERSION))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->manifest_version = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTRUCTIONS))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_STEPS))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_ARRAY); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + update_manifest->instructions.steps_count = 0; + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_ARRAY) + { + uint32_t step_index = update_manifest->instructions.steps_count; + + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HANDLER))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + + update_manifest->instructions.steps[step_index].handler + = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILES))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_ARRAY); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + update_manifest->instructions.steps[step_index].files_count = 0; + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_ARRAY) + { + uint32_t file_index = update_manifest->instructions.steps[step_index].files_count; + + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + + update_manifest->instructions.steps[step_index].files[file_index] + = ref_json_reader->token.slice; + update_manifest->instructions.steps[step_index].files_count++; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR( + AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HANDLER_PROPERTIES))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_INSTALLED_CRITERIA))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->instructions.steps[step_index] + .handler_properties.installed_criteria + = ref_json_reader->token.slice; + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_END_OBJECT); + } + else + { + return AZ_ERROR_JSON_INVALID_STATE; + } + } + else + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + update_manifest->instructions.steps_count++; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_END_OBJECT); + } + else + { + _az_LOG_WRITE( + AZ_LOG_IOT_ADU, AZ_SPAN_FROM_STR("Unexpected property found in ADU manifest steps:")); + _az_LOG_WRITE(AZ_LOG_IOT_ADU, ref_json_reader->token.slice); + return AZ_ERROR_JSON_INVALID_STATE; + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_UPDATE_ID))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_PROVIDER))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->update_id.provider = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_NAME))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->update_id.name = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_VERSION))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->update_id.version = ref_json_reader->token.slice; + } + else + { + _az_LOG_WRITE( + AZ_LOG_IOT_ADU, + AZ_SPAN_FROM_STR("Unexpected property found in ADU update id object:")); + _az_LOG_WRITE(AZ_LOG_IOT_ADU, ref_json_reader->token.slice); + return AZ_ERROR_JSON_INVALID_STATE; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_COMPATIBILITY))) + { + /* + * According to ADU design, the ADU service compatibility properties + * are not intended to be consumed by the ADU agent. + * To save on processing, the properties are not being exposed. + */ + _az_RETURN_IF_FAILED(az_json_reader_skip_children(ref_json_reader)); + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILES))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + uint32_t files_index = update_manifest->files_count; + + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + update_manifest->files[files_index].id = ref_json_reader->token.slice; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + + if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_FILE_NAME))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->files[files_index].file_name = ref_json_reader->token.slice; + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_SIZE_IN_BYTES))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_NUMBER); + + _az_RETURN_IF_FAILED(az_json_token_get_uint32( + &ref_json_reader->token, &update_manifest->files[files_index].size_in_bytes)); + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_HASHES))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_BEGIN_OBJECT); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + update_manifest->files[files_index].hashes_count = 0; + + while (ref_json_reader->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + uint32_t hashes_count = update_manifest->files[files_index].hashes_count; + + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_PROPERTY_NAME); + update_manifest->files[files_index].hashes[hashes_count].hash_type + = ref_json_reader->token.slice; + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->files[files_index].hashes[hashes_count].hash_value + = ref_json_reader->token.slice; + + update_manifest->files[files_index].hashes_count++; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + /* + * C SDK will not support delta updates at this time, so relatedFiles, + * downloadHandler, and mimeType are not exposed or processed. + */ + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_RELATED_FILES))) + { + _az_RETURN_IF_FAILED(az_json_reader_skip_children(ref_json_reader)); + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_DOWNLOAD_HANDLER))) + { + _az_RETURN_IF_FAILED(az_json_reader_skip_children(ref_json_reader)); + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_MIME_TYPE))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + else + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + update_manifest->files_count++; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + } + else if (az_json_token_is_text_equal( + &ref_json_reader->token, + AZ_SPAN_FROM_STR(AZ_IOT_ADU_CLIENT_AGENT_PROPERTY_NAME_CREATED_DATE_TIME))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + RETURN_IF_JSON_TOKEN_NOT_TYPE((ref_json_reader), AZ_JSON_TOKEN_STRING); + update_manifest->create_date_time = ref_json_reader->token.slice; + } + else + { + property_parsed = false; + } + + if (!property_parsed) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + _az_RETURN_IF_FAILED(az_json_reader_skip_children(ref_json_reader)); + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + return AZ_OK; +} diff --git a/src/az_iot_adu_client.h b/src/az_iot_adu_client.h new file mode 100644 index 00000000..abea3474 --- /dev/null +++ b/src/az_iot_adu_client.h @@ -0,0 +1,620 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Definition for the Azure IoT ADU Client + * + * @note More details about Azure Device Update can be found online + * at https://docs.microsoft.com/azure/iot-hub-device-update/understand-device-update + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + * + */ + +#ifndef _az_IOT_ADU_H +#define _az_IOT_ADU_H + +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Define the ADU agent model ID. + */ +#define AZ_IOT_ADU_CLIENT_AGENT_MODEL_ID "dtmi:azure:iot:deviceUpdateModel;1" + +/** + * @brief ADU Agent Version + */ +#define AZ_IOT_ADU_CLIENT_AGENT_VERSION "DU;agent/0.8.0-rc1-public-preview" + +/** + * @brief ADU PnP Component Name + */ +#define AZ_IOT_ADU_CLIENT_PROPERTIES_COMPONENT_NAME "deviceUpdate" + +/** + * @brief ADU Service Response (Accepted) + */ +#define AZ_IOT_ADU_CLIENT_REQUEST_ACCEPTED 200 + +/** + * @brief ADU Service Response (Rejected) + */ +#define AZ_IOT_ADU_CLIENT_REQUEST_REJECTED 406 + +/** + * @brief ADU Service Action (Apply) + */ +#define AZ_IOT_ADU_CLIENT_SERVICE_ACTION_APPLY_DEPLOYMENT 3 + +/** + * @brief ADU Service Action (Cancel) + */ +#define AZ_IOT_ADU_CLIENT_SERVICE_ACTION_CANCEL 255 + +/** + * @brief ADU Agent State (Idle) + */ +#define AZ_IOT_ADU_CLIENT_AGENT_STATE_IDLE 0 + +/** + * @brief ADU Agent State (In Progress) + */ +#define AZ_IOT_ADU_CLIENT_AGENT_STATE_DEPLOYMENT_IN_PROGRESS 6 + +/** + * @brief ADU Agent State (Failed) + */ +#define AZ_IOT_ADU_CLIENT_AGENT_STATE_FAILED 255 + +/** + * @brief Maximum Number of Files Handled by this ADU Agent (Number of URLs) + */ +#define AZ_IOT_ADU_CLIENT_MAX_FILE_URL_COUNT 10 + +/** + * @brief Maximum Number of Files Handled by this ADU Agent (Steps) + */ +#define AZ_IOT_ADU_CLIENT_MAX_INSTRUCTIONS_STEPS 10 + +/** + * @brief Maximum Number of Files Handled by this ADU Agent (File Hashes) + */ +#define AZ_IOT_ADU_CLIENT_MAX_FILE_HASH_COUNT 2 + +/** + * @brief Maximum Number of Custom Device Properties + */ +#define AZ_IOT_ADU_CLIENT_MAX_DEVICE_CUSTOM_PROPERTIES 5 + +/** + * @brief Default Agent Compatibility Properties + */ +#define AZ_IOT_ADU_CLIENT_AGENT_DEFAULT_COMPATIBILITY_PROPERTIES "manufacturer,model" + +/* The following key is used to validate the Azure Device Update update manifest signature */ +/* For more details, please see + * https://docs.microsoft.com/azure/iot-hub-device-update/device-update-security */ + +/** + * @brief The root key id used to identify the key. + */ +extern const uint8_t azure_iot_adu_root_key_id[13]; + +/** + * @brief The root key n (modulus) used to verify the manifest. + */ +extern const uint8_t azure_iot_adu_root_key_n[385]; + +/** + * @brief The root key e (exponent) used to verify the manifest. + */ +extern const uint8_t azure_iot_adu_root_key_e[3]; + +/** + * @brief Identity of the update request. + * @remark This version refers to the update request itself. + * For verifying if an update request is applicable to an + * ADU agent, use the update manifest instructions steps "installed criteria". + */ +typedef struct +{ + /** + * The provider for the update. + */ + az_span provider; + /** + * The name for the update. + */ + az_span name; + /** + * The version for the update. + */ + az_span version; +} az_iot_adu_update_id; + +/** + * @brief Holds any user-defined custom properties of the device. + * @remark Implementer can define other device properties to be used + * for the compatibility check while targeting the update deployment. + */ +typedef struct +{ + /** + * An array holding the custom names for the device properties. + */ + az_span names[AZ_IOT_ADU_CLIENT_MAX_DEVICE_CUSTOM_PROPERTIES]; + /** + * An array holding the custom values for the device properties. + */ + az_span values[AZ_IOT_ADU_CLIENT_MAX_DEVICE_CUSTOM_PROPERTIES]; + /** + * The number of custom names and values. + */ + int32_t count; +} az_iot_adu_device_custom_properties; + +/** + * @brief Holds the ADU agent device properties. + * @remarks These properties are used by the ADU service for matching + * update groups and verifying the current update deployed. + * https://docs.microsoft.com/azure/iot-hub-device-update/device-update-plug-and-play + */ +typedef struct +{ + /** + * The device manufacturer of the device, reported through deviceProperties. + */ + az_span manufacturer; + /** + * The device model of the device, reported through deviceProperties. + */ + az_span model; + /** + * Implementer can define other device properties to be used for the + * compatibility check while targeting the update deployment. + */ + az_iot_adu_device_custom_properties* custom_properties; + /** + * Version of the Device Update agent running on the device. + * @remark Must be set to AZ_IOT_ADU_CLIENT_AGENT_VERSION. + */ + az_span adu_version; + /** + * Version of the Delivery Optimization agent. + * @remark Please see Azure Device Update documentation on how to use + * the delivery optimization agent. If unused, set to #AZ_SPAN_EMPTY. + * + * https://docs.microsoft.com/azure/iot-hub-device-update/device-update-plug-and-play#device-properties + */ + az_span delivery_optimization_agent_version; + /** + * An ID of the update that is currently installed. + */ + az_span update_id; +} az_iot_adu_client_device_properties; + +/** + * @brief The update step result reported by the agent. + * + * This details results for a specific step of the update process. + * + */ +typedef struct +{ + /** + * A code that contains information about the result of the last update action. + * Example: 700 + */ + int32_t result_code; + /** + * A code that contains additional information about the result. + * Example: 0x80004005 + */ + int32_t extended_result_code; + /** + * Customer-defined free form string to provide additional result details. + */ + az_span result_details; +} az_iot_adu_client_step_result; + +/** + * @brief The update result reported by the agent. + * + * This details the result for the overall update. + */ +typedef struct +{ + /** + * A code that contains information about the result of the last update action. + * Example: 700 + */ + int32_t result_code; + /** + * A code that contains additional information about the result. + * Example: 0x80004006 + */ + int32_t extended_result_code; + /** + * Customer-defined free form string to provide additional result details. + */ + az_span result_details; + /** + * Number of items in \p step_results. + */ + int32_t step_results_count; + /** + * The results for each step in the update manifest instructions. + * The number of steps MUST match the number of steps in the + * update manifest for the resulting state to be property generated. + */ + az_iot_adu_client_step_result step_results[AZ_IOT_ADU_CLIENT_MAX_INSTRUCTIONS_STEPS]; +} az_iot_adu_client_install_result; + +/** + * @brief A set of values that indicate which deployment the agent is currently working on. + * + */ +typedef struct +{ + /** + * An integer that corresponds to an action the agent should perform. + * @remark Refer to the following defines for the expected values: + * AZ_IOT_ADU_CLIENT_SERVICE_ACTION_APPLY_DEPLOYMENT + * AZ_IOT_ADU_CLIENT_SERVICE_ACTION_CANCEL + */ + int32_t action; + /** + * ID of current deployment. + */ + az_span id; + /** + * Time of last deployment retry. + */ + az_span retry_timestamp; +} az_iot_adu_client_workflow; + +/** + * @brief A map of file ID to download url. + */ +typedef struct +{ + /** + * File ID, mapped in the updated manifest. + */ + az_span id; + /** + * Complete url to a file. + */ + az_span url; +} az_iot_adu_client_file_url; + +/** + * @brief Structure that holds the parsed contents of the ADU + * request in the Plug and Play writable properties sent + * by the ADU service. + */ +typedef struct +{ + /** + * A set of values that indicate which deployment the agent is currently working on. + */ + az_iot_adu_client_workflow workflow; + /** + * Description of the content of an update. + * @note This will come as an escaped string. The user must unescape it using + * an API such as az_json_string_unescape() before subsequently calling + * az_iot_adu_client_parse_update_manifest() with it. + */ + az_span update_manifest; + /** + * A JSON Web Signature (JWS) with JSON Web Keys used for source verification. + */ + az_span update_manifest_signature; + /** + * Tells the agent which files to download and the hash to use to verify that the files + * were downloaded correctly. + */ + az_iot_adu_client_file_url file_urls[AZ_IOT_ADU_CLIENT_MAX_FILE_URL_COUNT]; + /** + * Number of items in \p file_urls. + */ + uint32_t file_urls_count; +} az_iot_adu_client_update_request; + +/** + * @brief User-defined properties for handling an update request. + * + */ +typedef struct +{ + /** + * The installed criteria which defines a successful installation. + */ + az_span installed_criteria; +} az_iot_adu_client_update_manifest_instructions_step_handler_properties; + +/** + * @brief Step in the instructions of an update manifest. + * + */ +typedef struct +{ + /** + * Name of the component that is expected to handle the step. + */ + az_span handler; + /** + * Files related to this update step. + */ + az_span files[AZ_IOT_ADU_CLIENT_MAX_FILE_URL_COUNT]; + /** + * Number of items in \p files. + */ + uint32_t files_count; + /** + * Additional user-defined properties for the update step handler. + */ + az_iot_adu_client_update_manifest_instructions_step_handler_properties handler_properties; +} az_iot_adu_client_update_manifest_instructions_step; + +/** + * @brief Instructions in the update manifest. + */ +typedef struct +{ + /** + * Steps of the instructions in an update request. + */ + az_iot_adu_client_update_manifest_instructions_step + steps[AZ_IOT_ADU_CLIENT_MAX_INSTRUCTIONS_STEPS]; + /** + * Number of items in \p steps. + */ + uint32_t steps_count; +} az_iot_adu_client_update_manifest_instructions; + +/** + * @brief Hash value for a given file. + * + */ +typedef struct +{ + /** + * The hash type for the file (Example: sha256). + */ + az_span hash_type; + /** + * The value of the hash. + */ + az_span hash_value; +} az_iot_adu_client_update_manifest_file_hash; + +/** + * @brief Details of a file referenced in the update request. + * + */ +typedef struct +{ + /** + * Identity of a file, referenced in the update request. + */ + az_span id; + /** + * Name of the file. + */ + az_span file_name; + /** + * Size of a file, in bytes. + */ + uint32_t size_in_bytes; + /** + * Hashes provided for a given file in the update request. + */ + az_iot_adu_client_update_manifest_file_hash hashes[AZ_IOT_ADU_CLIENT_MAX_FILE_HASH_COUNT]; + /** + * Number of items in \p hashes. + */ + uint32_t hashes_count; +} az_iot_adu_client_update_manifest_file; + +/** + * @brief Structure that holds the parsed contents of the update manifest + * sent by the ADU service. + */ +typedef struct +{ + /** + * Version of the update manifest schema. + */ + az_span manifest_version; + /** + * User-defined identity of the update manifest. + */ + az_iot_adu_update_id update_id; + /** + * Instructions of the update manifest. + */ + az_iot_adu_client_update_manifest_instructions instructions; + /** + * Download urls for the files referenced in the update manifest instructions. + */ + az_iot_adu_client_update_manifest_file files[AZ_IOT_ADU_CLIENT_MAX_FILE_URL_COUNT]; + /** + * Number of items in \p files. + */ + uint32_t files_count; + /** + * The creation date and time. + */ + az_span create_date_time; +} az_iot_adu_client_update_manifest; + +/** + * @brief User-defined options for the Azure IoT ADU client. + * + */ +typedef struct +{ + /** + * The custom device compatibility properties for the device. + */ + az_span device_compatibility_properties; +} az_iot_adu_client_options; + +/** + * @brief Structure that holds the state of the Azure IoT ADU client. + * + */ +typedef struct +{ + struct + { + az_iot_adu_client_options options; + } _internal; +} az_iot_adu_client; + +/** + * @brief Gets the default Azure IoT ADU Client options. + * @details Call this to obtain an initialized #az_iot_adu_client_options structure that can be + * afterwards modified and passed to #az_iot_adu_client_init. + * + * @return #az_iot_adu_client_options. + */ +AZ_NODISCARD az_iot_adu_client_options az_iot_adu_client_options_default(); + +/** + * @brief Initializes an Azure IoT ADU Client. + * + * @param client The #az_iot_adu_client to use for this call. + * @param options A reference to an #az_iot_adu_client_options structure. If `NULL` is passed, + * the adu client will use the default options. If using custom options, please initialize first by + * calling az_iot_adu_client_options_default() and then populating relevant options with your own + * values. + * @pre \p client must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result +az_iot_adu_client_init(az_iot_adu_client* client, az_iot_adu_client_options* options); + +/** + * @brief Verifies if the Azure Plug-and-Play writable properties component + * is for ADU device update. + * + * @param[in] client The #az_iot_adu_client to use for this call. + * @param[in] component_name #az_span pointing to the component name in the + * writable properties. + * @return A boolean indicating if the component name is for ADU device update. + */ +AZ_NODISCARD bool az_iot_adu_client_is_component_device_update( + az_iot_adu_client* client, + az_span component_name); + +/** + * @brief Generates the Azure Plug-and-Play (reported) properties payload + * with the state of the ADU agent. + * + * @param[in] client The #az_iot_adu_client to use for this call. + * @param[in] device_properties A pointer to a #az_iot_adu_client_device_properties + * structure with all the details of the device, + * as required by the ADU service. + * @param[in] agent_state An integer value indicating the current state of + * the ADU agent. Use the values defined by the + * AZ_IOT_ADU_CLIENT_AGENT_STATE macros in this header. + * Please see the ADU online documentation for more + * details. + * @param[in] workflow A pointer to a #az_iot_adu_client_workflow instance + * indicating the current ADU workflow being processed, + * if an ADU service workflow was received. Use NULL + * if no device update is in progress. + * @param[in] last_install_result A pointer to a #az_iot_adu_client_install_result + * instance with the results of the current or past + * device update workflow, if available. Use NULL + * if no results are available. + * @param[in,out] ref_json_writer An #az_json_writer initialized with the memory where + * to write the property payload. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_adu_client_get_agent_state_payload( + az_iot_adu_client* client, + az_iot_adu_client_device_properties* device_properties, + int32_t agent_state, + az_iot_adu_client_workflow* workflow, + az_iot_adu_client_install_result* last_install_result, + az_json_writer* ref_json_writer); + +/** + * @brief Parses the json content from the ADU service writable properties into + * a pre-defined structure. + * + * @param[in] client The #az_iot_adu_client to use for this call. + * @param[in] ref_json_reader A #az_json_reader initialized with the ADU + * service writable properties json, set to the + * beginning of the json object that is the value + * of the ADU component. + * @param[out] update_request A pointer to the #az_iot_adu_client_update_request + * structure where to store the parsed contents + * read from the `ref_json_reader` json reader. + * In summary, this structure holds #az_span + * instances that point to the actual data + * parsed from `ref_json_reader` and copied to `buffer`. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_adu_client_parse_service_properties( + az_iot_adu_client* client, + az_json_reader* ref_json_reader, + az_iot_adu_client_update_request* update_request); + +/** + * @brief Generates the payload necessary to respond to the service + after receiving incoming properties. + * + * @param[in] client The #az_iot_adu_client to use for this call. + * @param[in] version Version of the writable properties. + * @param[in] status Azure Plug-and-Play status code for the + * writable properties acknowledgement. + * @param[in] ref_json_writer An #az_json_writer pointing to the memory buffer where to + * write the resulting Azure Plug-and-Play properties. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_adu_client_get_service_properties_response( + az_iot_adu_client* client, + int32_t version, + int32_t status, + az_json_writer* ref_json_writer); + +/** + * @brief Parses the json content from the ADU service update manifest into + * a pre-defined structure. + * + * @param[in] client The #az_iot_adu_client to use for this call. + * @param[in] ref_json_reader ADU update manifest, as initialized json reader. + * @param[out] update_manifest The structure where the parsed values of the + * manifest are stored. Values are not copied from + * `payload`, the fields of the structure just + * point to the positions in `payload` where the + * data is present, except for numeric and boolean + * values (which are parsed into the respective + * data types). + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_adu_client_parse_update_manifest( + az_iot_adu_client* client, + az_json_reader* ref_json_reader, + az_iot_adu_client_update_manifest* update_manifest); + +#include <_az_cfg_suffix.h> + +#endif // _az_IOT_ADU_H diff --git a/src/az_iot_common.h b/src/az_iot_common.h index 0282e8c6..9d479218 100644 --- a/src/az_iot_common.h +++ b/src/az_iot_common.h @@ -57,6 +57,9 @@ enum az_log_classification_iot AZ_LOG_IOT_AZURERTOS = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT, 3), ///< Azure IoT classification for Azure RTOS. + + AZ_LOG_IOT_ADU + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT, 4), ///< Azure IoT classification for ADU APIs. }; enum diff --git a/src/az_iot_hub_client_properties.c b/src/az_iot_hub_client_properties.c index 9487aee2..61b80ef0 100644 --- a/src/az_iot_hub_client_properties.c +++ b/src/az_iot_hub_client_properties.c @@ -65,7 +65,6 @@ AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_component( az_json_writer* ref_json_writer, az_span component_name) { - _az_PRECONDITION_NOT_NULL(client); _az_PRECONDITION_NOT_NULL(ref_json_writer); _az_PRECONDITION_VALID_SPAN(component_name, 1, false); @@ -85,7 +84,6 @@ AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_component( az_iot_hub_client const* client, az_json_writer* ref_json_writer) { - _az_PRECONDITION_NOT_NULL(client); _az_PRECONDITION_NOT_NULL(ref_json_writer); (void)client; @@ -101,7 +99,6 @@ AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_response_status int32_t version, az_span description) { - _az_PRECONDITION_NOT_NULL(client); _az_PRECONDITION_NOT_NULL(ref_json_writer); _az_PRECONDITION_VALID_SPAN(property_name, 1, false); @@ -133,7 +130,6 @@ AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_response_status( az_iot_hub_client const* client, az_json_writer* ref_json_writer) { - _az_PRECONDITION_NOT_NULL(client); _az_PRECONDITION_NOT_NULL(ref_json_writer); (void)client; @@ -201,7 +197,6 @@ AZ_NODISCARD az_result az_iot_hub_client_properties_get_properties_version( az_iot_hub_client_properties_message_type message_type, int32_t* out_version) { - _az_PRECONDITION_NOT_NULL(client); _az_PRECONDITION_NOT_NULL(ref_json_reader); _az_PRECONDITION( (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED) diff --git a/src/az_json.h b/src/az_json.h index 4da105ca..10570bcb 100644 --- a/src/az_json.h +++ b/src/az_json.h @@ -792,6 +792,33 @@ AZ_NODISCARD az_result az_json_reader_next_token(az_json_reader* ref_json_reader */ AZ_NODISCARD az_result az_json_reader_skip_children(az_json_reader* ref_json_reader); +/** + * @brief Unescapes the JSON string within the provided #az_span. + * + * @param[in] json_string The #az_span that contains the string to be unescaped. + * @param destination Pointer to the buffer that will contain the output. + * @param[in] destination_max_size The maximum available space within the buffer referred to by + * \p destination. + * @param[out] out_string_length __[nullable]__ Contains the number of bytes written to the + * \p destination which denote the length of the unescaped string. If `NULL` is passed, the + * \p parameter is ignored. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The string is returned. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE \p destination does not have enough size. + * + * @remarks The buffer referred to by \p destination must have a size that is at least 1 byte bigger + * than the \p json_string #az_span for the \p destination string to be zero-terminated. + * Content is copied from the source buffer, while unescaping and then `\0` is added at the end. + * + * @remarks This API can also be used to perform in place unescaping. + */ +AZ_NODISCARD az_result az_json_string_unescape( + az_span json_string, + char* destination, + int32_t destination_max_size, + int32_t* out_string_length); + #include <_az_cfg_suffix.h> #endif // _az_JSON_H diff --git a/src/az_json_private.h b/src/az_json_private.h index 22ea0eff..74c24e4d 100644 --- a/src/az_json_private.h +++ b/src/az_json_private.h @@ -120,6 +120,24 @@ AZ_NODISCARD AZ_INLINE _az_json_stack_item _az_json_stack_peek(_az_json_bit_stac : _az_JSON_STACK_ARRAY; } +AZ_NODISCARD AZ_INLINE bool _az_is_valid_escaped_character(uint8_t byte) +{ + switch (byte) + { + case '\\': + case '"': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + return true; + default: + return false; + } +} + #include <_az_cfg_suffix.h> #endif // _az_SPAN_PRIVATE_H diff --git a/src/az_json_reader.c b/src/az_json_reader.c index d71ebed4..01863d66 100644 --- a/src/az_json_reader.c +++ b/src/az_json_reader.c @@ -237,24 +237,6 @@ AZ_NODISCARD static az_result _az_json_reader_process_container_start( return AZ_OK; } -AZ_NODISCARD static bool _az_is_valid_escaped_character(uint8_t byte) -{ - switch (byte) - { - case '\\': - case '"': - case '/': - case 'b': - case 'f': - case 'n': - case 'r': - case 't': - return true; - default: - return false; - } -} - AZ_NODISCARD static az_result _az_json_reader_process_string(az_json_reader* ref_json_reader) { // Move past the first '"' character diff --git a/src/az_json_token.c b/src/az_json_token.c index 6d443add..109da868 100644 --- a/src/az_json_token.c +++ b/src/az_json_token.c @@ -325,6 +325,62 @@ AZ_NODISCARD static az_result _az_json_token_get_string_helper( return AZ_OK; } +AZ_NODISCARD az_result az_json_string_unescape( + az_span json_string, + char* destination, + int32_t destination_max_size, + int32_t* out_string_length) +{ + _az_PRECONDITION_VALID_SPAN(json_string, 1, false); + _az_PRECONDITION_NOT_NULL(destination); + // The destination needs to be larger than the input, for null terminator. + _az_PRECONDITION(destination_max_size > az_span_size(json_string)); + + int32_t position = 0; + int32_t span_size = az_span_size(json_string); + uint8_t* span_ptr = az_span_ptr(json_string); + for (int32_t i = 0; i < span_size; i++) + { + uint8_t current_char = span_ptr[i]; + if (current_char == '\\' && i < span_size - 1) + { + uint8_t next_char = span_ptr[i + 1]; + // check that we have something to escape + if (_az_is_valid_escaped_character(next_char)) + { + current_char = _az_json_unescape_single_byte(next_char); + i++; + } + else + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + else if (current_char == '\\') + { + // At this point, we are at the last character, i == span_size - 1 + return AZ_ERROR_UNEXPECTED_END; + } + + if (position > destination_max_size) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + destination[position] = (char)current_char; + position++; + } + + destination[position] = 0; + + if (out_string_length != NULL) + { + *out_string_length = position; + } + + return AZ_OK; +} + AZ_NODISCARD az_result az_json_token_get_string( az_json_token const* json_token, char* destination, diff --git a/src/az_span.h b/src/az_span.h index c6956466..ac15d45b 100644 --- a/src/az_span.h +++ b/src/az_span.h @@ -76,17 +76,24 @@ AZ_NODISCARD az_span az_span_create(uint8_t* ptr, int32_t size); #endif // AZ_NO_PRECONDITION_CHECKING /** - * @brief An empty #az_span. + * @brief An empty #az_span literal. * * @remark There is no guarantee that the pointer backing this span will be `NULL` and the caller * shouldn't rely on it. However, the size will be 0. */ -#define AZ_SPAN_EMPTY \ - (az_span) \ +#define AZ_SPAN_LITERAL_EMPTY \ { \ ._internal = {.ptr = NULL, .size = 0 } \ } +/** + * @brief An empty #az_span. + * + * @remark There is no guarantee that the pointer backing this span will be `NULL` and the caller + * shouldn't rely on it. However, the size will be 0. + */ +#define AZ_SPAN_EMPTY (az_span) AZ_SPAN_LITERAL_EMPTY + // Returns the size (in bytes) of a literal string. // Note: Concatenating "" to S produces a compiler error if S is not a literal string // The stored string's length does not include the \0 terminator. diff --git a/src/az_version.h b/src/az_version.h index a8391237..06260f8e 100644 --- a/src/az_version.h +++ b/src/az_version.h @@ -17,19 +17,18 @@ /// The version in string format used for telemetry following the `semver.org` standard /// (https://semver.org). -#define AZ_SDK_VERSION_STRING "1.3.2" +#define AZ_SDK_VERSION_STRING "1.4.0-beta.1" /// Major numeric identifier. #define AZ_SDK_VERSION_MAJOR 1 /// Minor numeric identifier. -#define AZ_SDK_VERSION_MINOR 3 +#define AZ_SDK_VERSION_MINOR 4 /// Patch numeric identifier. -#define AZ_SDK_VERSION_PATCH 2 +#define AZ_SDK_VERSION_PATCH 0 /// Optional pre-release identifier. SDK is in a pre-release state when present. -#define AZ_SDK_VERSION_PRERELEASE -#undef AZ_SDK_VERSION_PRERELEASE +#define AZ_SDK_VERSION_PRERELEASE "beta.1" #endif //_az_VERSION_H diff --git a/src/azure_ca.h b/src/azure_ca.h index 6e65d145..a17adc39 100644 --- a/src/azure_ca.h +++ b/src/azure_ca.h @@ -1,220 +1,220 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// SPDX-License-Identifier: MIT - -unsigned char ca_pem[] = { - 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, - 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, - 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x64, 0x7a, 0x43, 0x43, - 0x41, 0x6c, 0x2b, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, - 0x41, 0x67, 0x41, 0x41, 0x75, 0x54, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, - 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, - 0x41, 0x44, 0x42, 0x61, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, - 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x4a, 0x0a, 0x52, 0x54, 0x45, - 0x53, 0x4d, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, - 0x4a, 0x51, 0x6d, 0x46, 0x73, 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, - 0x6c, 0x4d, 0x52, 0x4d, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, - 0x4c, 0x45, 0x77, 0x70, 0x44, 0x65, 0x57, 0x4a, 0x6c, 0x63, 0x6c, 0x52, - 0x79, 0x64, 0x58, 0x4e, 0x30, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, - 0x44, 0x0a, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6c, 0x43, 0x59, 0x57, - 0x78, 0x30, 0x61, 0x57, 0x31, 0x76, 0x63, 0x6d, 0x55, 0x67, 0x51, 0x33, - 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x43, - 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, - 0x41, 0x77, 0x4d, 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x45, 0x34, 0x4e, 0x44, - 0x59, 0x77, 0x4d, 0x46, 0x6f, 0x58, 0x0a, 0x44, 0x54, 0x49, 0x31, 0x4d, - 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x49, 0x7a, 0x4e, 0x54, 0x6b, 0x77, 0x4d, - 0x46, 0x6f, 0x77, 0x57, 0x6a, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, - 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x53, 0x55, 0x55, 0x78, 0x45, - 0x6a, 0x41, 0x51, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x6f, 0x54, 0x43, - 0x55, 0x4a, 0x68, 0x62, 0x48, 0x52, 0x70, 0x62, 0x57, 0x39, 0x79, 0x0a, - 0x5a, 0x54, 0x45, 0x54, 0x4d, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, - 0x43, 0x78, 0x4d, 0x4b, 0x51, 0x33, 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, - 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, - 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4d, 0x5a, 0x51, 0x6d, 0x46, 0x73, - 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, 0x6c, 0x49, 0x45, 0x4e, 0x35, - 0x59, 0x6d, 0x56, 0x79, 0x0a, 0x56, 0x48, 0x4a, 0x31, 0x63, 0x33, 0x51, - 0x67, 0x55, 0x6d, 0x39, 0x76, 0x64, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, - 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, - 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, - 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6f, 0x43, 0x67, 0x67, 0x45, - 0x42, 0x41, 0x4b, 0x4d, 0x45, 0x75, 0x79, 0x4b, 0x72, 0x0a, 0x6d, 0x44, - 0x31, 0x58, 0x36, 0x43, 0x5a, 0x79, 0x6d, 0x72, 0x56, 0x35, 0x31, 0x43, - 0x6e, 0x69, 0x34, 0x65, 0x69, 0x56, 0x67, 0x4c, 0x47, 0x77, 0x34, 0x31, - 0x75, 0x4f, 0x4b, 0x79, 0x6d, 0x61, 0x5a, 0x4e, 0x2b, 0x68, 0x58, 0x65, - 0x32, 0x77, 0x43, 0x51, 0x56, 0x74, 0x32, 0x79, 0x67, 0x75, 0x7a, 0x6d, - 0x4b, 0x69, 0x59, 0x76, 0x36, 0x30, 0x69, 0x4e, 0x6f, 0x53, 0x36, 0x7a, - 0x6a, 0x72, 0x0a, 0x49, 0x5a, 0x33, 0x41, 0x51, 0x53, 0x73, 0x42, 0x55, - 0x6e, 0x75, 0x49, 0x64, 0x39, 0x4d, 0x63, 0x6a, 0x38, 0x65, 0x36, 0x75, - 0x59, 0x69, 0x31, 0x61, 0x67, 0x6e, 0x6e, 0x63, 0x2b, 0x67, 0x52, 0x51, - 0x4b, 0x66, 0x52, 0x7a, 0x4d, 0x70, 0x69, 0x6a, 0x53, 0x33, 0x6c, 0x6a, - 0x77, 0x75, 0x6d, 0x55, 0x4e, 0x4b, 0x6f, 0x55, 0x4d, 0x4d, 0x6f, 0x36, - 0x76, 0x57, 0x72, 0x4a, 0x59, 0x65, 0x4b, 0x0a, 0x6d, 0x70, 0x59, 0x63, - 0x71, 0x57, 0x65, 0x34, 0x50, 0x77, 0x7a, 0x56, 0x39, 0x2f, 0x6c, 0x53, - 0x45, 0x79, 0x2f, 0x43, 0x47, 0x39, 0x56, 0x77, 0x63, 0x50, 0x43, 0x50, - 0x77, 0x42, 0x4c, 0x4b, 0x42, 0x73, 0x75, 0x61, 0x34, 0x64, 0x6e, 0x4b, - 0x4d, 0x33, 0x70, 0x33, 0x31, 0x76, 0x6a, 0x73, 0x75, 0x66, 0x46, 0x6f, - 0x52, 0x45, 0x4a, 0x49, 0x45, 0x39, 0x4c, 0x41, 0x77, 0x71, 0x53, 0x75, - 0x0a, 0x58, 0x6d, 0x44, 0x2b, 0x74, 0x71, 0x59, 0x46, 0x2f, 0x4c, 0x54, - 0x64, 0x42, 0x31, 0x6b, 0x43, 0x31, 0x46, 0x6b, 0x59, 0x6d, 0x47, 0x50, - 0x31, 0x70, 0x57, 0x50, 0x67, 0x6b, 0x41, 0x78, 0x39, 0x58, 0x62, 0x49, - 0x47, 0x65, 0x76, 0x4f, 0x46, 0x36, 0x75, 0x76, 0x55, 0x41, 0x36, 0x35, - 0x65, 0x68, 0x44, 0x35, 0x66, 0x2f, 0x78, 0x58, 0x74, 0x61, 0x62, 0x7a, - 0x35, 0x4f, 0x54, 0x5a, 0x79, 0x0a, 0x64, 0x63, 0x39, 0x33, 0x55, 0x6b, - 0x33, 0x7a, 0x79, 0x5a, 0x41, 0x73, 0x75, 0x54, 0x33, 0x6c, 0x79, 0x53, - 0x4e, 0x54, 0x50, 0x78, 0x38, 0x6b, 0x6d, 0x43, 0x46, 0x63, 0x42, 0x35, - 0x6b, 0x70, 0x76, 0x63, 0x59, 0x36, 0x37, 0x4f, 0x64, 0x75, 0x68, 0x6a, - 0x70, 0x72, 0x6c, 0x33, 0x52, 0x6a, 0x4d, 0x37, 0x31, 0x6f, 0x47, 0x44, - 0x48, 0x77, 0x65, 0x49, 0x31, 0x32, 0x76, 0x2f, 0x79, 0x65, 0x0a, 0x6a, - 0x6c, 0x30, 0x71, 0x68, 0x71, 0x64, 0x4e, 0x6b, 0x4e, 0x77, 0x6e, 0x47, - 0x6a, 0x6b, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4e, 0x46, 0x4d, - 0x45, 0x4d, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, - 0x42, 0x59, 0x45, 0x46, 0x4f, 0x57, 0x64, 0x57, 0x54, 0x43, 0x43, 0x52, - 0x31, 0x6a, 0x4d, 0x72, 0x50, 0x6f, 0x49, 0x56, 0x44, 0x61, 0x47, 0x65, - 0x7a, 0x71, 0x31, 0x0a, 0x42, 0x45, 0x33, 0x77, 0x4d, 0x42, 0x49, 0x47, - 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, - 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x4d, 0x77, - 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, - 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x41, 0x30, 0x47, - 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0a, 0x44, 0x51, 0x45, - 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, - 0x46, 0x44, 0x46, 0x32, 0x4f, 0x35, 0x47, 0x39, 0x52, 0x61, 0x45, 0x49, - 0x46, 0x6f, 0x4e, 0x32, 0x37, 0x54, 0x79, 0x63, 0x6c, 0x68, 0x41, 0x4f, - 0x39, 0x39, 0x32, 0x54, 0x39, 0x4c, 0x64, 0x63, 0x77, 0x34, 0x36, 0x51, - 0x51, 0x46, 0x2b, 0x76, 0x61, 0x4b, 0x53, 0x6d, 0x32, 0x65, 0x54, 0x39, - 0x32, 0x0a, 0x39, 0x68, 0x6b, 0x54, 0x49, 0x37, 0x67, 0x51, 0x43, 0x76, - 0x6c, 0x59, 0x70, 0x4e, 0x52, 0x68, 0x63, 0x4c, 0x30, 0x45, 0x59, 0x57, - 0x6f, 0x53, 0x69, 0x68, 0x66, 0x56, 0x43, 0x72, 0x33, 0x46, 0x76, 0x44, - 0x42, 0x38, 0x31, 0x75, 0x6b, 0x4d, 0x4a, 0x59, 0x32, 0x47, 0x51, 0x45, - 0x2f, 0x73, 0x7a, 0x4b, 0x4e, 0x2b, 0x4f, 0x4d, 0x59, 0x33, 0x45, 0x55, - 0x2f, 0x74, 0x33, 0x57, 0x67, 0x78, 0x0a, 0x6a, 0x6b, 0x7a, 0x53, 0x73, - 0x77, 0x46, 0x30, 0x37, 0x72, 0x35, 0x31, 0x58, 0x67, 0x64, 0x49, 0x47, - 0x6e, 0x39, 0x77, 0x2f, 0x78, 0x5a, 0x63, 0x68, 0x4d, 0x42, 0x35, 0x68, - 0x62, 0x67, 0x46, 0x2f, 0x58, 0x2b, 0x2b, 0x5a, 0x52, 0x47, 0x6a, 0x44, - 0x38, 0x41, 0x43, 0x74, 0x50, 0x68, 0x53, 0x4e, 0x7a, 0x6b, 0x45, 0x31, - 0x61, 0x6b, 0x78, 0x65, 0x68, 0x69, 0x2f, 0x6f, 0x43, 0x72, 0x30, 0x0a, - 0x45, 0x70, 0x6e, 0x33, 0x6f, 0x30, 0x57, 0x43, 0x34, 0x7a, 0x78, 0x65, - 0x39, 0x5a, 0x32, 0x65, 0x74, 0x63, 0x69, 0x65, 0x66, 0x43, 0x37, 0x49, - 0x70, 0x4a, 0x35, 0x4f, 0x43, 0x42, 0x52, 0x4c, 0x62, 0x66, 0x31, 0x77, - 0x62, 0x57, 0x73, 0x61, 0x59, 0x37, 0x31, 0x6b, 0x35, 0x68, 0x2b, 0x33, - 0x7a, 0x76, 0x44, 0x79, 0x6e, 0x79, 0x36, 0x37, 0x47, 0x37, 0x66, 0x79, - 0x55, 0x49, 0x68, 0x7a, 0x0a, 0x6b, 0x73, 0x4c, 0x69, 0x34, 0x78, 0x61, - 0x4e, 0x6d, 0x6a, 0x49, 0x43, 0x71, 0x34, 0x34, 0x59, 0x33, 0x65, 0x6b, - 0x51, 0x45, 0x65, 0x35, 0x2b, 0x4e, 0x61, 0x75, 0x51, 0x72, 0x7a, 0x34, - 0x77, 0x6c, 0x48, 0x72, 0x51, 0x4d, 0x7a, 0x32, 0x6e, 0x5a, 0x51, 0x2f, - 0x31, 0x2f, 0x49, 0x36, 0x65, 0x59, 0x73, 0x39, 0x48, 0x52, 0x43, 0x77, - 0x42, 0x58, 0x62, 0x73, 0x64, 0x74, 0x54, 0x4c, 0x53, 0x0a, 0x52, 0x39, - 0x49, 0x34, 0x4c, 0x74, 0x44, 0x2b, 0x67, 0x64, 0x77, 0x79, 0x61, 0x68, - 0x36, 0x31, 0x37, 0x6a, 0x7a, 0x56, 0x2f, 0x4f, 0x65, 0x42, 0x48, 0x52, - 0x6e, 0x44, 0x4a, 0x45, 0x4c, 0x71, 0x59, 0x7a, 0x6d, 0x70, 0x0a, 0x2d, - 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, - 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, - 0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, - 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, - 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x6a, 0x6a, - 0x43, 0x43, 0x41, 0x6e, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, - 0x49, 0x51, 0x41, 0x7a, 0x72, 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, - 0x43, 0x37, 0x4b, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6e, 0x36, 0x35, 0x54, - 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, - 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x68, 0x0a, 0x4d, - 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, - 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x45, 0x56, 0x4d, 0x42, 0x4d, 0x47, 0x41, - 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, 0x4d, 0x52, 0x47, 0x6c, 0x6e, 0x61, - 0x55, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6a, 0x4d, - 0x52, 0x6b, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4c, 0x45, - 0x78, 0x42, 0x33, 0x0a, 0x64, 0x33, 0x63, 0x75, 0x5a, 0x47, 0x6c, 0x6e, - 0x61, 0x57, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, - 0x4d, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, - 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, - 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, - 0x55, 0x6d, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x0a, 0x4d, 0x6a, 0x41, - 0x65, 0x46, 0x77, 0x30, 0x78, 0x4d, 0x7a, 0x41, 0x34, 0x4d, 0x44, 0x45, - 0x78, 0x4d, 0x6a, 0x41, 0x77, 0x4d, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, - 0x7a, 0x4f, 0x44, 0x41, 0x78, 0x4d, 0x54, 0x55, 0x78, 0x4d, 0x6a, 0x41, - 0x77, 0x4d, 0x44, 0x42, 0x61, 0x4d, 0x47, 0x45, 0x78, 0x43, 0x7a, 0x41, - 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, - 0x54, 0x0a, 0x4d, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, - 0x51, 0x4b, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, - 0x56, 0x79, 0x64, 0x43, 0x42, 0x4a, 0x62, 0x6d, 0x4d, 0x78, 0x47, 0x54, - 0x41, 0x58, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, - 0x64, 0x33, 0x64, 0x79, 0x35, 0x6b, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, - 0x56, 0x79, 0x64, 0x43, 0x35, 0x6a, 0x0a, 0x62, 0x32, 0x30, 0x78, 0x49, - 0x44, 0x41, 0x65, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, 0x54, 0x46, - 0x30, 0x52, 0x70, 0x5a, 0x32, 0x6c, 0x44, 0x5a, 0x58, 0x4a, 0x30, 0x49, - 0x45, 0x64, 0x73, 0x62, 0x32, 0x4a, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, - 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4d, 0x49, 0x49, 0x42, 0x49, - 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x0a, - 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, - 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4b, 0x43, - 0x41, 0x51, 0x45, 0x41, 0x75, 0x7a, 0x66, 0x4e, 0x4e, 0x4e, 0x78, 0x37, - 0x61, 0x38, 0x6d, 0x79, 0x61, 0x4a, 0x43, 0x74, 0x53, 0x6e, 0x58, 0x2f, - 0x52, 0x72, 0x6f, 0x68, 0x43, 0x67, 0x69, 0x4e, 0x39, 0x52, 0x6c, 0x55, - 0x79, 0x66, 0x75, 0x49, 0x0a, 0x32, 0x2f, 0x4f, 0x75, 0x38, 0x6a, 0x71, - 0x4a, 0x6b, 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6d, 0x76, - 0x50, 0x72, 0x43, 0x33, 0x6f, 0x58, 0x67, 0x6b, 0x6b, 0x52, 0x4c, 0x70, - 0x69, 0x6d, 0x6e, 0x37, 0x57, 0x6f, 0x36, 0x68, 0x2b, 0x34, 0x46, 0x52, - 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, 0x4c, 0x65, 0x63, 0x59, 0x78, 0x70, - 0x73, 0x4d, 0x4e, 0x7a, 0x61, 0x48, 0x78, 0x6d, 0x78, 0x0a, 0x31, 0x78, - 0x37, 0x65, 0x2f, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4e, 0x36, - 0x37, 0x73, 0x48, 0x30, 0x4e, 0x4f, 0x33, 0x58, 0x73, 0x73, 0x30, 0x72, - 0x30, 0x75, 0x70, 0x53, 0x2f, 0x6b, 0x71, 0x62, 0x69, 0x74, 0x4f, 0x74, - 0x53, 0x5a, 0x70, 0x4c, 0x59, 0x6c, 0x36, 0x5a, 0x74, 0x72, 0x41, 0x47, - 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6b, 0x59, 0x39, 0x32, - 0x65, 0x51, 0x0a, 0x71, 0x32, 0x45, 0x47, 0x6e, 0x49, 0x2f, 0x79, 0x75, - 0x75, 0x6d, 0x30, 0x36, 0x5a, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7a, 0x56, - 0x2b, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4d, 0x48, 0x61, 0x75, 0x56, 0x42, - 0x4a, 0x56, 0x4a, 0x38, 0x7a, 0x55, 0x74, 0x6c, 0x75, 0x4e, 0x4a, 0x62, - 0x64, 0x31, 0x33, 0x34, 0x2f, 0x74, 0x4a, 0x53, 0x37, 0x53, 0x73, 0x56, - 0x51, 0x65, 0x70, 0x6a, 0x35, 0x57, 0x7a, 0x0a, 0x74, 0x43, 0x4f, 0x37, - 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, 0x77, - 0x74, 0x50, 0x31, 0x4d, 0x56, 0x59, 0x77, 0x6e, 0x53, 0x6c, 0x63, 0x55, - 0x66, 0x49, 0x4b, 0x64, 0x7a, 0x58, 0x4f, 0x53, 0x30, 0x78, 0x5a, 0x4b, - 0x42, 0x67, 0x79, 0x4d, 0x55, 0x4e, 0x47, 0x50, 0x48, 0x67, 0x6d, 0x2b, - 0x46, 0x36, 0x48, 0x6d, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2b, 0x55, 0x51, - 0x0a, 0x76, 0x49, 0x4f, 0x6c, 0x43, 0x73, 0x52, 0x6e, 0x4b, 0x50, 0x5a, - 0x7a, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6e, 0x62, 0x44, 0x68, 0x78, 0x53, - 0x4a, 0x49, 0x54, 0x52, 0x4e, 0x72, 0x77, 0x39, 0x46, 0x44, 0x4b, 0x5a, - 0x4a, 0x6f, 0x62, 0x71, 0x37, 0x6e, 0x4d, 0x57, 0x78, 0x4d, 0x34, 0x4d, - 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6f, 0x30, 0x49, - 0x77, 0x51, 0x44, 0x41, 0x50, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, - 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, - 0x48, 0x2f, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, - 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6a, - 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, - 0x51, 0x55, 0x54, 0x69, 0x4a, 0x55, 0x49, 0x42, 0x69, 0x56, 0x0a, 0x35, - 0x75, 0x4e, 0x75, 0x35, 0x67, 0x2f, 0x36, 0x2b, 0x72, 0x6b, 0x53, 0x37, - 0x51, 0x59, 0x58, 0x6a, 0x7a, 0x6b, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, - 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x4c, 0x42, - 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6e, 0x4b, - 0x4a, 0x52, 0x76, 0x44, 0x6b, 0x68, 0x6a, 0x36, 0x7a, 0x48, 0x64, 0x36, - 0x6d, 0x63, 0x59, 0x0a, 0x31, 0x59, 0x6c, 0x39, 0x50, 0x4d, 0x57, 0x4c, - 0x53, 0x6e, 0x2f, 0x70, 0x76, 0x74, 0x73, 0x72, 0x46, 0x39, 0x2b, 0x77, - 0x58, 0x33, 0x4e, 0x33, 0x4b, 0x6a, 0x49, 0x54, 0x4f, 0x59, 0x46, 0x6e, - 0x51, 0x6f, 0x51, 0x6a, 0x38, 0x6b, 0x56, 0x6e, 0x4e, 0x65, 0x79, 0x49, - 0x76, 0x2f, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4d, 0x4e, 0x4b, 0x53, 0x75, - 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x0a, 0x4e, 0x65, 0x46, - 0x32, 0x32, 0x64, 0x2b, 0x6d, 0x51, 0x72, 0x76, 0x48, 0x52, 0x41, 0x69, - 0x47, 0x66, 0x7a, 0x5a, 0x30, 0x4a, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, - 0x55, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6b, 0x6e, 0x64, 0x74, 0x68, 0x2f, - 0x4a, 0x73, 0x77, 0x31, 0x48, 0x4b, 0x6a, 0x32, 0x5a, 0x4c, 0x37, 0x74, - 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4f, 0x47, 0x5a, 0x58, 0x31, 0x4e, - 0x47, 0x0a, 0x46, 0x64, 0x74, 0x6f, 0x6d, 0x2f, 0x44, 0x7a, 0x4d, 0x4e, - 0x55, 0x2b, 0x4d, 0x65, 0x4b, 0x4e, 0x68, 0x4a, 0x37, 0x6a, 0x69, 0x74, - 0x72, 0x61, 0x6c, 0x6a, 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, - 0x6c, 0x77, 0x55, 0x48, 0x42, 0x48, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, - 0x37, 0x41, 0x6a, 0x36, 0x34, 0x47, 0x78, 0x4a, 0x55, 0x54, 0x46, 0x79, - 0x38, 0x62, 0x4a, 0x5a, 0x39, 0x31, 0x0a, 0x38, 0x72, 0x47, 0x4f, 0x6d, - 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4b, - 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2f, 0x4d, 0x55, 0x52, - 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6b, - 0x77, 0x37, 0x2b, 0x55, 0x30, 0x62, 0x36, 0x4c, 0x4a, 0x33, 0x2f, 0x69, - 0x79, 0x4b, 0x35, 0x53, 0x39, 0x6b, 0x4a, 0x52, 0x61, 0x54, 0x65, 0x0a, - 0x70, 0x4c, 0x69, 0x61, 0x57, 0x4e, 0x30, 0x62, 0x66, 0x56, 0x4b, 0x66, - 0x6a, 0x6c, 0x6c, 0x44, 0x69, 0x49, 0x47, 0x6b, 0x6e, 0x69, 0x62, 0x56, - 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, - 0x6b, 0x68, 0x76, 0x6c, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6a, 0x79, 0x4e, - 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4c, 0x5a, 0x5a, 0x6d, 0x36, 0x7a, - 0x4e, 0x54, 0x66, 0x6c, 0x0a, 0x4d, 0x72, 0x59, 0x3d, 0x0a, 0x2d, 0x2d, - 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, - 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, - 0x00 -}; -unsigned int ca_pem_len = 2557; +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +unsigned char ca_pem[] = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x64, 0x7a, 0x43, 0x43, + 0x41, 0x6c, 0x2b, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, + 0x41, 0x67, 0x41, 0x41, 0x75, 0x54, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, + 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, + 0x41, 0x44, 0x42, 0x61, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x4a, 0x0a, 0x52, 0x54, 0x45, + 0x53, 0x4d, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, + 0x4a, 0x51, 0x6d, 0x46, 0x73, 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, + 0x6c, 0x4d, 0x52, 0x4d, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x4c, 0x45, 0x77, 0x70, 0x44, 0x65, 0x57, 0x4a, 0x6c, 0x63, 0x6c, 0x52, + 0x79, 0x64, 0x58, 0x4e, 0x30, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, + 0x44, 0x0a, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6c, 0x43, 0x59, 0x57, + 0x78, 0x30, 0x61, 0x57, 0x31, 0x76, 0x63, 0x6d, 0x55, 0x67, 0x51, 0x33, + 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x43, + 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, + 0x41, 0x77, 0x4d, 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x45, 0x34, 0x4e, 0x44, + 0x59, 0x77, 0x4d, 0x46, 0x6f, 0x58, 0x0a, 0x44, 0x54, 0x49, 0x31, 0x4d, + 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x49, 0x7a, 0x4e, 0x54, 0x6b, 0x77, 0x4d, + 0x46, 0x6f, 0x77, 0x57, 0x6a, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x53, 0x55, 0x55, 0x78, 0x45, + 0x6a, 0x41, 0x51, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x6f, 0x54, 0x43, + 0x55, 0x4a, 0x68, 0x62, 0x48, 0x52, 0x70, 0x62, 0x57, 0x39, 0x79, 0x0a, + 0x5a, 0x54, 0x45, 0x54, 0x4d, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x43, 0x78, 0x4d, 0x4b, 0x51, 0x33, 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, + 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4d, 0x5a, 0x51, 0x6d, 0x46, 0x73, + 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, 0x6c, 0x49, 0x45, 0x4e, 0x35, + 0x59, 0x6d, 0x56, 0x79, 0x0a, 0x56, 0x48, 0x4a, 0x31, 0x63, 0x33, 0x51, + 0x67, 0x55, 0x6d, 0x39, 0x76, 0x64, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, + 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, + 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, + 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6f, 0x43, 0x67, 0x67, 0x45, + 0x42, 0x41, 0x4b, 0x4d, 0x45, 0x75, 0x79, 0x4b, 0x72, 0x0a, 0x6d, 0x44, + 0x31, 0x58, 0x36, 0x43, 0x5a, 0x79, 0x6d, 0x72, 0x56, 0x35, 0x31, 0x43, + 0x6e, 0x69, 0x34, 0x65, 0x69, 0x56, 0x67, 0x4c, 0x47, 0x77, 0x34, 0x31, + 0x75, 0x4f, 0x4b, 0x79, 0x6d, 0x61, 0x5a, 0x4e, 0x2b, 0x68, 0x58, 0x65, + 0x32, 0x77, 0x43, 0x51, 0x56, 0x74, 0x32, 0x79, 0x67, 0x75, 0x7a, 0x6d, + 0x4b, 0x69, 0x59, 0x76, 0x36, 0x30, 0x69, 0x4e, 0x6f, 0x53, 0x36, 0x7a, + 0x6a, 0x72, 0x0a, 0x49, 0x5a, 0x33, 0x41, 0x51, 0x53, 0x73, 0x42, 0x55, + 0x6e, 0x75, 0x49, 0x64, 0x39, 0x4d, 0x63, 0x6a, 0x38, 0x65, 0x36, 0x75, + 0x59, 0x69, 0x31, 0x61, 0x67, 0x6e, 0x6e, 0x63, 0x2b, 0x67, 0x52, 0x51, + 0x4b, 0x66, 0x52, 0x7a, 0x4d, 0x70, 0x69, 0x6a, 0x53, 0x33, 0x6c, 0x6a, + 0x77, 0x75, 0x6d, 0x55, 0x4e, 0x4b, 0x6f, 0x55, 0x4d, 0x4d, 0x6f, 0x36, + 0x76, 0x57, 0x72, 0x4a, 0x59, 0x65, 0x4b, 0x0a, 0x6d, 0x70, 0x59, 0x63, + 0x71, 0x57, 0x65, 0x34, 0x50, 0x77, 0x7a, 0x56, 0x39, 0x2f, 0x6c, 0x53, + 0x45, 0x79, 0x2f, 0x43, 0x47, 0x39, 0x56, 0x77, 0x63, 0x50, 0x43, 0x50, + 0x77, 0x42, 0x4c, 0x4b, 0x42, 0x73, 0x75, 0x61, 0x34, 0x64, 0x6e, 0x4b, + 0x4d, 0x33, 0x70, 0x33, 0x31, 0x76, 0x6a, 0x73, 0x75, 0x66, 0x46, 0x6f, + 0x52, 0x45, 0x4a, 0x49, 0x45, 0x39, 0x4c, 0x41, 0x77, 0x71, 0x53, 0x75, + 0x0a, 0x58, 0x6d, 0x44, 0x2b, 0x74, 0x71, 0x59, 0x46, 0x2f, 0x4c, 0x54, + 0x64, 0x42, 0x31, 0x6b, 0x43, 0x31, 0x46, 0x6b, 0x59, 0x6d, 0x47, 0x50, + 0x31, 0x70, 0x57, 0x50, 0x67, 0x6b, 0x41, 0x78, 0x39, 0x58, 0x62, 0x49, + 0x47, 0x65, 0x76, 0x4f, 0x46, 0x36, 0x75, 0x76, 0x55, 0x41, 0x36, 0x35, + 0x65, 0x68, 0x44, 0x35, 0x66, 0x2f, 0x78, 0x58, 0x74, 0x61, 0x62, 0x7a, + 0x35, 0x4f, 0x54, 0x5a, 0x79, 0x0a, 0x64, 0x63, 0x39, 0x33, 0x55, 0x6b, + 0x33, 0x7a, 0x79, 0x5a, 0x41, 0x73, 0x75, 0x54, 0x33, 0x6c, 0x79, 0x53, + 0x4e, 0x54, 0x50, 0x78, 0x38, 0x6b, 0x6d, 0x43, 0x46, 0x63, 0x42, 0x35, + 0x6b, 0x70, 0x76, 0x63, 0x59, 0x36, 0x37, 0x4f, 0x64, 0x75, 0x68, 0x6a, + 0x70, 0x72, 0x6c, 0x33, 0x52, 0x6a, 0x4d, 0x37, 0x31, 0x6f, 0x47, 0x44, + 0x48, 0x77, 0x65, 0x49, 0x31, 0x32, 0x76, 0x2f, 0x79, 0x65, 0x0a, 0x6a, + 0x6c, 0x30, 0x71, 0x68, 0x71, 0x64, 0x4e, 0x6b, 0x4e, 0x77, 0x6e, 0x47, + 0x6a, 0x6b, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4e, 0x46, 0x4d, + 0x45, 0x4d, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, + 0x42, 0x59, 0x45, 0x46, 0x4f, 0x57, 0x64, 0x57, 0x54, 0x43, 0x43, 0x52, + 0x31, 0x6a, 0x4d, 0x72, 0x50, 0x6f, 0x49, 0x56, 0x44, 0x61, 0x47, 0x65, + 0x7a, 0x71, 0x31, 0x0a, 0x42, 0x45, 0x33, 0x77, 0x4d, 0x42, 0x49, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, + 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x4d, 0x77, + 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x41, 0x30, 0x47, + 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0a, 0x44, 0x51, 0x45, + 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, + 0x46, 0x44, 0x46, 0x32, 0x4f, 0x35, 0x47, 0x39, 0x52, 0x61, 0x45, 0x49, + 0x46, 0x6f, 0x4e, 0x32, 0x37, 0x54, 0x79, 0x63, 0x6c, 0x68, 0x41, 0x4f, + 0x39, 0x39, 0x32, 0x54, 0x39, 0x4c, 0x64, 0x63, 0x77, 0x34, 0x36, 0x51, + 0x51, 0x46, 0x2b, 0x76, 0x61, 0x4b, 0x53, 0x6d, 0x32, 0x65, 0x54, 0x39, + 0x32, 0x0a, 0x39, 0x68, 0x6b, 0x54, 0x49, 0x37, 0x67, 0x51, 0x43, 0x76, + 0x6c, 0x59, 0x70, 0x4e, 0x52, 0x68, 0x63, 0x4c, 0x30, 0x45, 0x59, 0x57, + 0x6f, 0x53, 0x69, 0x68, 0x66, 0x56, 0x43, 0x72, 0x33, 0x46, 0x76, 0x44, + 0x42, 0x38, 0x31, 0x75, 0x6b, 0x4d, 0x4a, 0x59, 0x32, 0x47, 0x51, 0x45, + 0x2f, 0x73, 0x7a, 0x4b, 0x4e, 0x2b, 0x4f, 0x4d, 0x59, 0x33, 0x45, 0x55, + 0x2f, 0x74, 0x33, 0x57, 0x67, 0x78, 0x0a, 0x6a, 0x6b, 0x7a, 0x53, 0x73, + 0x77, 0x46, 0x30, 0x37, 0x72, 0x35, 0x31, 0x58, 0x67, 0x64, 0x49, 0x47, + 0x6e, 0x39, 0x77, 0x2f, 0x78, 0x5a, 0x63, 0x68, 0x4d, 0x42, 0x35, 0x68, + 0x62, 0x67, 0x46, 0x2f, 0x58, 0x2b, 0x2b, 0x5a, 0x52, 0x47, 0x6a, 0x44, + 0x38, 0x41, 0x43, 0x74, 0x50, 0x68, 0x53, 0x4e, 0x7a, 0x6b, 0x45, 0x31, + 0x61, 0x6b, 0x78, 0x65, 0x68, 0x69, 0x2f, 0x6f, 0x43, 0x72, 0x30, 0x0a, + 0x45, 0x70, 0x6e, 0x33, 0x6f, 0x30, 0x57, 0x43, 0x34, 0x7a, 0x78, 0x65, + 0x39, 0x5a, 0x32, 0x65, 0x74, 0x63, 0x69, 0x65, 0x66, 0x43, 0x37, 0x49, + 0x70, 0x4a, 0x35, 0x4f, 0x43, 0x42, 0x52, 0x4c, 0x62, 0x66, 0x31, 0x77, + 0x62, 0x57, 0x73, 0x61, 0x59, 0x37, 0x31, 0x6b, 0x35, 0x68, 0x2b, 0x33, + 0x7a, 0x76, 0x44, 0x79, 0x6e, 0x79, 0x36, 0x37, 0x47, 0x37, 0x66, 0x79, + 0x55, 0x49, 0x68, 0x7a, 0x0a, 0x6b, 0x73, 0x4c, 0x69, 0x34, 0x78, 0x61, + 0x4e, 0x6d, 0x6a, 0x49, 0x43, 0x71, 0x34, 0x34, 0x59, 0x33, 0x65, 0x6b, + 0x51, 0x45, 0x65, 0x35, 0x2b, 0x4e, 0x61, 0x75, 0x51, 0x72, 0x7a, 0x34, + 0x77, 0x6c, 0x48, 0x72, 0x51, 0x4d, 0x7a, 0x32, 0x6e, 0x5a, 0x51, 0x2f, + 0x31, 0x2f, 0x49, 0x36, 0x65, 0x59, 0x73, 0x39, 0x48, 0x52, 0x43, 0x77, + 0x42, 0x58, 0x62, 0x73, 0x64, 0x74, 0x54, 0x4c, 0x53, 0x0a, 0x52, 0x39, + 0x49, 0x34, 0x4c, 0x74, 0x44, 0x2b, 0x67, 0x64, 0x77, 0x79, 0x61, 0x68, + 0x36, 0x31, 0x37, 0x6a, 0x7a, 0x56, 0x2f, 0x4f, 0x65, 0x42, 0x48, 0x52, + 0x6e, 0x44, 0x4a, 0x45, 0x4c, 0x71, 0x59, 0x7a, 0x6d, 0x70, 0x0a, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x6a, 0x6a, + 0x43, 0x43, 0x41, 0x6e, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x51, 0x41, 0x7a, 0x72, 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, + 0x43, 0x37, 0x4b, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6e, 0x36, 0x35, 0x54, + 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x68, 0x0a, 0x4d, + 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, + 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x45, 0x56, 0x4d, 0x42, 0x4d, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, 0x4d, 0x52, 0x47, 0x6c, 0x6e, 0x61, + 0x55, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6a, 0x4d, + 0x52, 0x6b, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4c, 0x45, + 0x78, 0x42, 0x33, 0x0a, 0x64, 0x33, 0x63, 0x75, 0x5a, 0x47, 0x6c, 0x6e, + 0x61, 0x57, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, + 0x4d, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, + 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, + 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, + 0x55, 0x6d, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x0a, 0x4d, 0x6a, 0x41, + 0x65, 0x46, 0x77, 0x30, 0x78, 0x4d, 0x7a, 0x41, 0x34, 0x4d, 0x44, 0x45, + 0x78, 0x4d, 0x6a, 0x41, 0x77, 0x4d, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, + 0x7a, 0x4f, 0x44, 0x41, 0x78, 0x4d, 0x54, 0x55, 0x78, 0x4d, 0x6a, 0x41, + 0x77, 0x4d, 0x44, 0x42, 0x61, 0x4d, 0x47, 0x45, 0x78, 0x43, 0x7a, 0x41, + 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, + 0x54, 0x0a, 0x4d, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x4b, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x42, 0x4a, 0x62, 0x6d, 0x4d, 0x78, 0x47, 0x54, + 0x41, 0x58, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, + 0x64, 0x33, 0x64, 0x79, 0x35, 0x6b, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x35, 0x6a, 0x0a, 0x62, 0x32, 0x30, 0x78, 0x49, + 0x44, 0x41, 0x65, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, 0x54, 0x46, + 0x30, 0x52, 0x70, 0x5a, 0x32, 0x6c, 0x44, 0x5a, 0x58, 0x4a, 0x30, 0x49, + 0x45, 0x64, 0x73, 0x62, 0x32, 0x4a, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, + 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4d, 0x49, 0x49, 0x42, 0x49, + 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x0a, + 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, + 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4b, 0x43, + 0x41, 0x51, 0x45, 0x41, 0x75, 0x7a, 0x66, 0x4e, 0x4e, 0x4e, 0x78, 0x37, + 0x61, 0x38, 0x6d, 0x79, 0x61, 0x4a, 0x43, 0x74, 0x53, 0x6e, 0x58, 0x2f, + 0x52, 0x72, 0x6f, 0x68, 0x43, 0x67, 0x69, 0x4e, 0x39, 0x52, 0x6c, 0x55, + 0x79, 0x66, 0x75, 0x49, 0x0a, 0x32, 0x2f, 0x4f, 0x75, 0x38, 0x6a, 0x71, + 0x4a, 0x6b, 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6d, 0x76, + 0x50, 0x72, 0x43, 0x33, 0x6f, 0x58, 0x67, 0x6b, 0x6b, 0x52, 0x4c, 0x70, + 0x69, 0x6d, 0x6e, 0x37, 0x57, 0x6f, 0x36, 0x68, 0x2b, 0x34, 0x46, 0x52, + 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, 0x4c, 0x65, 0x63, 0x59, 0x78, 0x70, + 0x73, 0x4d, 0x4e, 0x7a, 0x61, 0x48, 0x78, 0x6d, 0x78, 0x0a, 0x31, 0x78, + 0x37, 0x65, 0x2f, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4e, 0x36, + 0x37, 0x73, 0x48, 0x30, 0x4e, 0x4f, 0x33, 0x58, 0x73, 0x73, 0x30, 0x72, + 0x30, 0x75, 0x70, 0x53, 0x2f, 0x6b, 0x71, 0x62, 0x69, 0x74, 0x4f, 0x74, + 0x53, 0x5a, 0x70, 0x4c, 0x59, 0x6c, 0x36, 0x5a, 0x74, 0x72, 0x41, 0x47, + 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6b, 0x59, 0x39, 0x32, + 0x65, 0x51, 0x0a, 0x71, 0x32, 0x45, 0x47, 0x6e, 0x49, 0x2f, 0x79, 0x75, + 0x75, 0x6d, 0x30, 0x36, 0x5a, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7a, 0x56, + 0x2b, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4d, 0x48, 0x61, 0x75, 0x56, 0x42, + 0x4a, 0x56, 0x4a, 0x38, 0x7a, 0x55, 0x74, 0x6c, 0x75, 0x4e, 0x4a, 0x62, + 0x64, 0x31, 0x33, 0x34, 0x2f, 0x74, 0x4a, 0x53, 0x37, 0x53, 0x73, 0x56, + 0x51, 0x65, 0x70, 0x6a, 0x35, 0x57, 0x7a, 0x0a, 0x74, 0x43, 0x4f, 0x37, + 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, 0x77, + 0x74, 0x50, 0x31, 0x4d, 0x56, 0x59, 0x77, 0x6e, 0x53, 0x6c, 0x63, 0x55, + 0x66, 0x49, 0x4b, 0x64, 0x7a, 0x58, 0x4f, 0x53, 0x30, 0x78, 0x5a, 0x4b, + 0x42, 0x67, 0x79, 0x4d, 0x55, 0x4e, 0x47, 0x50, 0x48, 0x67, 0x6d, 0x2b, + 0x46, 0x36, 0x48, 0x6d, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2b, 0x55, 0x51, + 0x0a, 0x76, 0x49, 0x4f, 0x6c, 0x43, 0x73, 0x52, 0x6e, 0x4b, 0x50, 0x5a, + 0x7a, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6e, 0x62, 0x44, 0x68, 0x78, 0x53, + 0x4a, 0x49, 0x54, 0x52, 0x4e, 0x72, 0x77, 0x39, 0x46, 0x44, 0x4b, 0x5a, + 0x4a, 0x6f, 0x62, 0x71, 0x37, 0x6e, 0x4d, 0x57, 0x78, 0x4d, 0x34, 0x4d, + 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6f, 0x30, 0x49, + 0x77, 0x51, 0x44, 0x41, 0x50, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, + 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, + 0x48, 0x2f, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, + 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6a, + 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, + 0x51, 0x55, 0x54, 0x69, 0x4a, 0x55, 0x49, 0x42, 0x69, 0x56, 0x0a, 0x35, + 0x75, 0x4e, 0x75, 0x35, 0x67, 0x2f, 0x36, 0x2b, 0x72, 0x6b, 0x53, 0x37, + 0x51, 0x59, 0x58, 0x6a, 0x7a, 0x6b, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, + 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x4c, 0x42, + 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6e, 0x4b, + 0x4a, 0x52, 0x76, 0x44, 0x6b, 0x68, 0x6a, 0x36, 0x7a, 0x48, 0x64, 0x36, + 0x6d, 0x63, 0x59, 0x0a, 0x31, 0x59, 0x6c, 0x39, 0x50, 0x4d, 0x57, 0x4c, + 0x53, 0x6e, 0x2f, 0x70, 0x76, 0x74, 0x73, 0x72, 0x46, 0x39, 0x2b, 0x77, + 0x58, 0x33, 0x4e, 0x33, 0x4b, 0x6a, 0x49, 0x54, 0x4f, 0x59, 0x46, 0x6e, + 0x51, 0x6f, 0x51, 0x6a, 0x38, 0x6b, 0x56, 0x6e, 0x4e, 0x65, 0x79, 0x49, + 0x76, 0x2f, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4d, 0x4e, 0x4b, 0x53, 0x75, + 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x0a, 0x4e, 0x65, 0x46, + 0x32, 0x32, 0x64, 0x2b, 0x6d, 0x51, 0x72, 0x76, 0x48, 0x52, 0x41, 0x69, + 0x47, 0x66, 0x7a, 0x5a, 0x30, 0x4a, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, + 0x55, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6b, 0x6e, 0x64, 0x74, 0x68, 0x2f, + 0x4a, 0x73, 0x77, 0x31, 0x48, 0x4b, 0x6a, 0x32, 0x5a, 0x4c, 0x37, 0x74, + 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4f, 0x47, 0x5a, 0x58, 0x31, 0x4e, + 0x47, 0x0a, 0x46, 0x64, 0x74, 0x6f, 0x6d, 0x2f, 0x44, 0x7a, 0x4d, 0x4e, + 0x55, 0x2b, 0x4d, 0x65, 0x4b, 0x4e, 0x68, 0x4a, 0x37, 0x6a, 0x69, 0x74, + 0x72, 0x61, 0x6c, 0x6a, 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, + 0x6c, 0x77, 0x55, 0x48, 0x42, 0x48, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, + 0x37, 0x41, 0x6a, 0x36, 0x34, 0x47, 0x78, 0x4a, 0x55, 0x54, 0x46, 0x79, + 0x38, 0x62, 0x4a, 0x5a, 0x39, 0x31, 0x0a, 0x38, 0x72, 0x47, 0x4f, 0x6d, + 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4b, + 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2f, 0x4d, 0x55, 0x52, + 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6b, + 0x77, 0x37, 0x2b, 0x55, 0x30, 0x62, 0x36, 0x4c, 0x4a, 0x33, 0x2f, 0x69, + 0x79, 0x4b, 0x35, 0x53, 0x39, 0x6b, 0x4a, 0x52, 0x61, 0x54, 0x65, 0x0a, + 0x70, 0x4c, 0x69, 0x61, 0x57, 0x4e, 0x30, 0x62, 0x66, 0x56, 0x4b, 0x66, + 0x6a, 0x6c, 0x6c, 0x44, 0x69, 0x49, 0x47, 0x6b, 0x6e, 0x69, 0x62, 0x56, + 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, + 0x6b, 0x68, 0x76, 0x6c, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6a, 0x79, 0x4e, + 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4c, 0x5a, 0x5a, 0x6d, 0x36, 0x7a, + 0x4e, 0x54, 0x66, 0x6c, 0x0a, 0x4d, 0x72, 0x59, 0x3d, 0x0a, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, + 0x00 +}; +unsigned int ca_pem_len = 2557; diff --git a/tools/Update-Library.ps1 b/tools/Update-Library.ps1 index d95edc78..35f7401d 100644 --- a/tools/Update-Library.ps1 +++ b/tools/Update-Library.ps1 @@ -2,8 +2,9 @@ # SPDX-License-Identifier: MIT param( - $SdkVersion = $(throw "Azure SDK for C Version (git tag) not provided"), - $NewLibraryVersion = $(throw "New Azure SDK for C Arduino Library version not provided") + $SdkBranch = $(throw "SdkBranch not provided"), + $SdkVersion = $(throw "SdkVersion not provided"), + $NewLibraryVersion = $(throw "NewLibraryVersion not provided") ) $SrcFolder = "..\src" @@ -12,7 +13,7 @@ $LibConfigFile = "..\library.properties" Write-Host "Cloning azure-sdk-for-c repository." git config --local core.autocrlf false -git clone -b $SdkVersion https://github.com/Azure/azure-sdk-for-c sdkrepo +git clone -b $SdkBranch https://github.com/Azure/azure-sdk-for-c sdkrepo Write-Host "Flattening the azure-sdk-for-c file structure and updating src/." From 137a313bba83a5b2d62b41b0610037729e157dfd Mon Sep 17 00:00:00 2001 From: Dane Walton Date: Wed, 26 Oct 2022 11:20:12 -0700 Subject: [PATCH 2/3] few updates --- README.md | 2 +- examples/Azure_IoT_Adu_ESP32/docs/lib.png | Bin 41495 -> 0 bytes library.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 examples/Azure_IoT_Adu_ESP32/docs/lib.png diff --git a/README.md b/README.md index 37e1de63..ae37ca44 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This library package contains the following samples. Please refer to their docum What is the difference between **IoT Hub** and **IoT Central** samples? 1. IoT Hub samples will get devices connected directly to [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-concepts-and-iot-hub) -2. IoT Central samples will leverage DPS ([Device Provisioning Service](https://docs.microsoft.com/azure/iot-dps/about-iot-dps)) to provision the device and then connect it to [Azure IoT Central](https://docs.microsoft.com/azure/iot-central/core/overview-iot-central). +1. IoT Central samples will leverage DPS ([Device Provisioning Service](https://docs.microsoft.com/azure/iot-dps/about-iot-dps)) to provision the device and then connect it to [Azure IoT Central](https://docs.microsoft.com/azure/iot-central/core/overview-iot-central). Please note that provisioning through DPS is mandatory for IoT Central scenarios, but DPS can also be used for IoT Hub devices as well. diff --git a/examples/Azure_IoT_Adu_ESP32/docs/lib.png b/examples/Azure_IoT_Adu_ESP32/docs/lib.png deleted file mode 100644 index ee91ec4b25eedbb63fadf34a3fbc01fc1410ba71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41495 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 diff --git a/library.properties b/library.properties index 0b8dcf25..8353e19d 100644 --- a/library.properties +++ b/library.properties @@ -1,4 +1,4 @@ -name=Azure SDK for C ADU +name=Azure SDK for C version=1.1.0-beta.1 author=Microsoft Corporation maintainer=Microsoft Corporation From 989cc627d448a807bc21b5db6491267b43f492ab Mon Sep 17 00:00:00 2001 From: Dane Walton Date: Thu, 27 Oct 2022 12:10:51 -0700 Subject: [PATCH 3/3] pr comments --- examples/Azure_IoT_Adu_ESP32/iot_configs.h | 2 +- examples/Azure_IoT_Adu_ESP32/readme.md | 6 +- src/azure_ca.h | 440 ++++++++++----------- 3 files changed, 225 insertions(+), 223 deletions(-) diff --git a/examples/Azure_IoT_Adu_ESP32/iot_configs.h b/examples/Azure_IoT_Adu_ESP32/iot_configs.h index ad3949ab..cf7afef5 100644 --- a/examples/Azure_IoT_Adu_ESP32/iot_configs.h +++ b/examples/Azure_IoT_Adu_ESP32/iot_configs.h @@ -62,7 +62,7 @@ #define IOT_CONFIG_DEVICE_KEY "Device Key" #endif // IOT_CONFIG_USE_X509_CERT -// Azure IoT ADU values +// Azure IoT ADU parameters #define ADU_DEVICE_MANUFACTURER "ESPRESSIF" #define ADU_DEVICE_MODEL "ESP32-Embedded" #define ADU_UPDATE_PROVIDER "ESPRESSIF" diff --git a/examples/Azure_IoT_Adu_ESP32/readme.md b/examples/Azure_IoT_Adu_ESP32/readme.md index aa684e2b..e5b34c84 100644 --- a/examples/Azure_IoT_Adu_ESP32/readme.md +++ b/examples/Azure_IoT_Adu_ESP32/readme.md @@ -97,7 +97,9 @@ _The following was run on Windows 11 and Ubuntu Desktop 20.04 environments, with ## New Image Instructions -In order to update our device, we have to build the image which our device will update to. We will have to direct the Arduino IDE to specify an output directory so that we can easily find the binary. Open the `preferences.txt` located at `C:\Users\\AppData\Local\Arduino15\` and add `build.path=C:\Arduino-output` (or whichever directory you prefer). +In order to update our device, we have to build the image which our device will update to. We will have to direct the Arduino IDE to specify an output directory so that we can easily find the binary. Open the `preferences.txt` (usually located at `C:\Users\\AppData\Local\Arduino15\`) and add `build.path=C:\Arduino-output` (or whichever directory you prefer). + +Once you are done with the ADU sample, you may remove the added configuration to restore the build output to its original location. 1. Connect the ESP32 microcontroller to your USB port. @@ -143,7 +145,7 @@ To import the update (`Azure_IoT_Adu_ESP32_1.1.bin`) and manifest (`ESPRESSIF.ES ### Tag Your Device -Add the `"ADUGroup"` tag to the device's top-level twin document. This is used to group device together, and you may choose whichever title you prefer. +Add the `"ADUGroup"` tag to the device's top-level twin document. This is used to group devices together, and you may choose whichever tag you prefer (e.g., "embeddedSDK"). ```json "tags": { diff --git a/src/azure_ca.h b/src/azure_ca.h index a17adc39..6e65d145 100644 --- a/src/azure_ca.h +++ b/src/azure_ca.h @@ -1,220 +1,220 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// SPDX-License-Identifier: MIT - -unsigned char ca_pem[] = { - 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, - 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, - 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x64, 0x7a, 0x43, 0x43, - 0x41, 0x6c, 0x2b, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, - 0x41, 0x67, 0x41, 0x41, 0x75, 0x54, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, - 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, - 0x41, 0x44, 0x42, 0x61, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, - 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x4a, 0x0a, 0x52, 0x54, 0x45, - 0x53, 0x4d, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, - 0x4a, 0x51, 0x6d, 0x46, 0x73, 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, - 0x6c, 0x4d, 0x52, 0x4d, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, - 0x4c, 0x45, 0x77, 0x70, 0x44, 0x65, 0x57, 0x4a, 0x6c, 0x63, 0x6c, 0x52, - 0x79, 0x64, 0x58, 0x4e, 0x30, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, - 0x44, 0x0a, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6c, 0x43, 0x59, 0x57, - 0x78, 0x30, 0x61, 0x57, 0x31, 0x76, 0x63, 0x6d, 0x55, 0x67, 0x51, 0x33, - 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x43, - 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, - 0x41, 0x77, 0x4d, 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x45, 0x34, 0x4e, 0x44, - 0x59, 0x77, 0x4d, 0x46, 0x6f, 0x58, 0x0a, 0x44, 0x54, 0x49, 0x31, 0x4d, - 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x49, 0x7a, 0x4e, 0x54, 0x6b, 0x77, 0x4d, - 0x46, 0x6f, 0x77, 0x57, 0x6a, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, - 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x53, 0x55, 0x55, 0x78, 0x45, - 0x6a, 0x41, 0x51, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x6f, 0x54, 0x43, - 0x55, 0x4a, 0x68, 0x62, 0x48, 0x52, 0x70, 0x62, 0x57, 0x39, 0x79, 0x0a, - 0x5a, 0x54, 0x45, 0x54, 0x4d, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, - 0x43, 0x78, 0x4d, 0x4b, 0x51, 0x33, 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, - 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, - 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4d, 0x5a, 0x51, 0x6d, 0x46, 0x73, - 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, 0x6c, 0x49, 0x45, 0x4e, 0x35, - 0x59, 0x6d, 0x56, 0x79, 0x0a, 0x56, 0x48, 0x4a, 0x31, 0x63, 0x33, 0x51, - 0x67, 0x55, 0x6d, 0x39, 0x76, 0x64, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, - 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, - 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, - 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6f, 0x43, 0x67, 0x67, 0x45, - 0x42, 0x41, 0x4b, 0x4d, 0x45, 0x75, 0x79, 0x4b, 0x72, 0x0a, 0x6d, 0x44, - 0x31, 0x58, 0x36, 0x43, 0x5a, 0x79, 0x6d, 0x72, 0x56, 0x35, 0x31, 0x43, - 0x6e, 0x69, 0x34, 0x65, 0x69, 0x56, 0x67, 0x4c, 0x47, 0x77, 0x34, 0x31, - 0x75, 0x4f, 0x4b, 0x79, 0x6d, 0x61, 0x5a, 0x4e, 0x2b, 0x68, 0x58, 0x65, - 0x32, 0x77, 0x43, 0x51, 0x56, 0x74, 0x32, 0x79, 0x67, 0x75, 0x7a, 0x6d, - 0x4b, 0x69, 0x59, 0x76, 0x36, 0x30, 0x69, 0x4e, 0x6f, 0x53, 0x36, 0x7a, - 0x6a, 0x72, 0x0a, 0x49, 0x5a, 0x33, 0x41, 0x51, 0x53, 0x73, 0x42, 0x55, - 0x6e, 0x75, 0x49, 0x64, 0x39, 0x4d, 0x63, 0x6a, 0x38, 0x65, 0x36, 0x75, - 0x59, 0x69, 0x31, 0x61, 0x67, 0x6e, 0x6e, 0x63, 0x2b, 0x67, 0x52, 0x51, - 0x4b, 0x66, 0x52, 0x7a, 0x4d, 0x70, 0x69, 0x6a, 0x53, 0x33, 0x6c, 0x6a, - 0x77, 0x75, 0x6d, 0x55, 0x4e, 0x4b, 0x6f, 0x55, 0x4d, 0x4d, 0x6f, 0x36, - 0x76, 0x57, 0x72, 0x4a, 0x59, 0x65, 0x4b, 0x0a, 0x6d, 0x70, 0x59, 0x63, - 0x71, 0x57, 0x65, 0x34, 0x50, 0x77, 0x7a, 0x56, 0x39, 0x2f, 0x6c, 0x53, - 0x45, 0x79, 0x2f, 0x43, 0x47, 0x39, 0x56, 0x77, 0x63, 0x50, 0x43, 0x50, - 0x77, 0x42, 0x4c, 0x4b, 0x42, 0x73, 0x75, 0x61, 0x34, 0x64, 0x6e, 0x4b, - 0x4d, 0x33, 0x70, 0x33, 0x31, 0x76, 0x6a, 0x73, 0x75, 0x66, 0x46, 0x6f, - 0x52, 0x45, 0x4a, 0x49, 0x45, 0x39, 0x4c, 0x41, 0x77, 0x71, 0x53, 0x75, - 0x0a, 0x58, 0x6d, 0x44, 0x2b, 0x74, 0x71, 0x59, 0x46, 0x2f, 0x4c, 0x54, - 0x64, 0x42, 0x31, 0x6b, 0x43, 0x31, 0x46, 0x6b, 0x59, 0x6d, 0x47, 0x50, - 0x31, 0x70, 0x57, 0x50, 0x67, 0x6b, 0x41, 0x78, 0x39, 0x58, 0x62, 0x49, - 0x47, 0x65, 0x76, 0x4f, 0x46, 0x36, 0x75, 0x76, 0x55, 0x41, 0x36, 0x35, - 0x65, 0x68, 0x44, 0x35, 0x66, 0x2f, 0x78, 0x58, 0x74, 0x61, 0x62, 0x7a, - 0x35, 0x4f, 0x54, 0x5a, 0x79, 0x0a, 0x64, 0x63, 0x39, 0x33, 0x55, 0x6b, - 0x33, 0x7a, 0x79, 0x5a, 0x41, 0x73, 0x75, 0x54, 0x33, 0x6c, 0x79, 0x53, - 0x4e, 0x54, 0x50, 0x78, 0x38, 0x6b, 0x6d, 0x43, 0x46, 0x63, 0x42, 0x35, - 0x6b, 0x70, 0x76, 0x63, 0x59, 0x36, 0x37, 0x4f, 0x64, 0x75, 0x68, 0x6a, - 0x70, 0x72, 0x6c, 0x33, 0x52, 0x6a, 0x4d, 0x37, 0x31, 0x6f, 0x47, 0x44, - 0x48, 0x77, 0x65, 0x49, 0x31, 0x32, 0x76, 0x2f, 0x79, 0x65, 0x0a, 0x6a, - 0x6c, 0x30, 0x71, 0x68, 0x71, 0x64, 0x4e, 0x6b, 0x4e, 0x77, 0x6e, 0x47, - 0x6a, 0x6b, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4e, 0x46, 0x4d, - 0x45, 0x4d, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, - 0x42, 0x59, 0x45, 0x46, 0x4f, 0x57, 0x64, 0x57, 0x54, 0x43, 0x43, 0x52, - 0x31, 0x6a, 0x4d, 0x72, 0x50, 0x6f, 0x49, 0x56, 0x44, 0x61, 0x47, 0x65, - 0x7a, 0x71, 0x31, 0x0a, 0x42, 0x45, 0x33, 0x77, 0x4d, 0x42, 0x49, 0x47, - 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, - 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x4d, 0x77, - 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, - 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x41, 0x30, 0x47, - 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0a, 0x44, 0x51, 0x45, - 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, - 0x46, 0x44, 0x46, 0x32, 0x4f, 0x35, 0x47, 0x39, 0x52, 0x61, 0x45, 0x49, - 0x46, 0x6f, 0x4e, 0x32, 0x37, 0x54, 0x79, 0x63, 0x6c, 0x68, 0x41, 0x4f, - 0x39, 0x39, 0x32, 0x54, 0x39, 0x4c, 0x64, 0x63, 0x77, 0x34, 0x36, 0x51, - 0x51, 0x46, 0x2b, 0x76, 0x61, 0x4b, 0x53, 0x6d, 0x32, 0x65, 0x54, 0x39, - 0x32, 0x0a, 0x39, 0x68, 0x6b, 0x54, 0x49, 0x37, 0x67, 0x51, 0x43, 0x76, - 0x6c, 0x59, 0x70, 0x4e, 0x52, 0x68, 0x63, 0x4c, 0x30, 0x45, 0x59, 0x57, - 0x6f, 0x53, 0x69, 0x68, 0x66, 0x56, 0x43, 0x72, 0x33, 0x46, 0x76, 0x44, - 0x42, 0x38, 0x31, 0x75, 0x6b, 0x4d, 0x4a, 0x59, 0x32, 0x47, 0x51, 0x45, - 0x2f, 0x73, 0x7a, 0x4b, 0x4e, 0x2b, 0x4f, 0x4d, 0x59, 0x33, 0x45, 0x55, - 0x2f, 0x74, 0x33, 0x57, 0x67, 0x78, 0x0a, 0x6a, 0x6b, 0x7a, 0x53, 0x73, - 0x77, 0x46, 0x30, 0x37, 0x72, 0x35, 0x31, 0x58, 0x67, 0x64, 0x49, 0x47, - 0x6e, 0x39, 0x77, 0x2f, 0x78, 0x5a, 0x63, 0x68, 0x4d, 0x42, 0x35, 0x68, - 0x62, 0x67, 0x46, 0x2f, 0x58, 0x2b, 0x2b, 0x5a, 0x52, 0x47, 0x6a, 0x44, - 0x38, 0x41, 0x43, 0x74, 0x50, 0x68, 0x53, 0x4e, 0x7a, 0x6b, 0x45, 0x31, - 0x61, 0x6b, 0x78, 0x65, 0x68, 0x69, 0x2f, 0x6f, 0x43, 0x72, 0x30, 0x0a, - 0x45, 0x70, 0x6e, 0x33, 0x6f, 0x30, 0x57, 0x43, 0x34, 0x7a, 0x78, 0x65, - 0x39, 0x5a, 0x32, 0x65, 0x74, 0x63, 0x69, 0x65, 0x66, 0x43, 0x37, 0x49, - 0x70, 0x4a, 0x35, 0x4f, 0x43, 0x42, 0x52, 0x4c, 0x62, 0x66, 0x31, 0x77, - 0x62, 0x57, 0x73, 0x61, 0x59, 0x37, 0x31, 0x6b, 0x35, 0x68, 0x2b, 0x33, - 0x7a, 0x76, 0x44, 0x79, 0x6e, 0x79, 0x36, 0x37, 0x47, 0x37, 0x66, 0x79, - 0x55, 0x49, 0x68, 0x7a, 0x0a, 0x6b, 0x73, 0x4c, 0x69, 0x34, 0x78, 0x61, - 0x4e, 0x6d, 0x6a, 0x49, 0x43, 0x71, 0x34, 0x34, 0x59, 0x33, 0x65, 0x6b, - 0x51, 0x45, 0x65, 0x35, 0x2b, 0x4e, 0x61, 0x75, 0x51, 0x72, 0x7a, 0x34, - 0x77, 0x6c, 0x48, 0x72, 0x51, 0x4d, 0x7a, 0x32, 0x6e, 0x5a, 0x51, 0x2f, - 0x31, 0x2f, 0x49, 0x36, 0x65, 0x59, 0x73, 0x39, 0x48, 0x52, 0x43, 0x77, - 0x42, 0x58, 0x62, 0x73, 0x64, 0x74, 0x54, 0x4c, 0x53, 0x0a, 0x52, 0x39, - 0x49, 0x34, 0x4c, 0x74, 0x44, 0x2b, 0x67, 0x64, 0x77, 0x79, 0x61, 0x68, - 0x36, 0x31, 0x37, 0x6a, 0x7a, 0x56, 0x2f, 0x4f, 0x65, 0x42, 0x48, 0x52, - 0x6e, 0x44, 0x4a, 0x45, 0x4c, 0x71, 0x59, 0x7a, 0x6d, 0x70, 0x0a, 0x2d, - 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, - 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, - 0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, - 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, - 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x6a, 0x6a, - 0x43, 0x43, 0x41, 0x6e, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, - 0x49, 0x51, 0x41, 0x7a, 0x72, 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, - 0x43, 0x37, 0x4b, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6e, 0x36, 0x35, 0x54, - 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, - 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x68, 0x0a, 0x4d, - 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, - 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x45, 0x56, 0x4d, 0x42, 0x4d, 0x47, 0x41, - 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, 0x4d, 0x52, 0x47, 0x6c, 0x6e, 0x61, - 0x55, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6a, 0x4d, - 0x52, 0x6b, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4c, 0x45, - 0x78, 0x42, 0x33, 0x0a, 0x64, 0x33, 0x63, 0x75, 0x5a, 0x47, 0x6c, 0x6e, - 0x61, 0x57, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, - 0x4d, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, - 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, - 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, - 0x55, 0x6d, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x0a, 0x4d, 0x6a, 0x41, - 0x65, 0x46, 0x77, 0x30, 0x78, 0x4d, 0x7a, 0x41, 0x34, 0x4d, 0x44, 0x45, - 0x78, 0x4d, 0x6a, 0x41, 0x77, 0x4d, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, - 0x7a, 0x4f, 0x44, 0x41, 0x78, 0x4d, 0x54, 0x55, 0x78, 0x4d, 0x6a, 0x41, - 0x77, 0x4d, 0x44, 0x42, 0x61, 0x4d, 0x47, 0x45, 0x78, 0x43, 0x7a, 0x41, - 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, - 0x54, 0x0a, 0x4d, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, - 0x51, 0x4b, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, - 0x56, 0x79, 0x64, 0x43, 0x42, 0x4a, 0x62, 0x6d, 0x4d, 0x78, 0x47, 0x54, - 0x41, 0x58, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, - 0x64, 0x33, 0x64, 0x79, 0x35, 0x6b, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, - 0x56, 0x79, 0x64, 0x43, 0x35, 0x6a, 0x0a, 0x62, 0x32, 0x30, 0x78, 0x49, - 0x44, 0x41, 0x65, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, 0x54, 0x46, - 0x30, 0x52, 0x70, 0x5a, 0x32, 0x6c, 0x44, 0x5a, 0x58, 0x4a, 0x30, 0x49, - 0x45, 0x64, 0x73, 0x62, 0x32, 0x4a, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, - 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4d, 0x49, 0x49, 0x42, 0x49, - 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x0a, - 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, - 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4b, 0x43, - 0x41, 0x51, 0x45, 0x41, 0x75, 0x7a, 0x66, 0x4e, 0x4e, 0x4e, 0x78, 0x37, - 0x61, 0x38, 0x6d, 0x79, 0x61, 0x4a, 0x43, 0x74, 0x53, 0x6e, 0x58, 0x2f, - 0x52, 0x72, 0x6f, 0x68, 0x43, 0x67, 0x69, 0x4e, 0x39, 0x52, 0x6c, 0x55, - 0x79, 0x66, 0x75, 0x49, 0x0a, 0x32, 0x2f, 0x4f, 0x75, 0x38, 0x6a, 0x71, - 0x4a, 0x6b, 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6d, 0x76, - 0x50, 0x72, 0x43, 0x33, 0x6f, 0x58, 0x67, 0x6b, 0x6b, 0x52, 0x4c, 0x70, - 0x69, 0x6d, 0x6e, 0x37, 0x57, 0x6f, 0x36, 0x68, 0x2b, 0x34, 0x46, 0x52, - 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, 0x4c, 0x65, 0x63, 0x59, 0x78, 0x70, - 0x73, 0x4d, 0x4e, 0x7a, 0x61, 0x48, 0x78, 0x6d, 0x78, 0x0a, 0x31, 0x78, - 0x37, 0x65, 0x2f, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4e, 0x36, - 0x37, 0x73, 0x48, 0x30, 0x4e, 0x4f, 0x33, 0x58, 0x73, 0x73, 0x30, 0x72, - 0x30, 0x75, 0x70, 0x53, 0x2f, 0x6b, 0x71, 0x62, 0x69, 0x74, 0x4f, 0x74, - 0x53, 0x5a, 0x70, 0x4c, 0x59, 0x6c, 0x36, 0x5a, 0x74, 0x72, 0x41, 0x47, - 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6b, 0x59, 0x39, 0x32, - 0x65, 0x51, 0x0a, 0x71, 0x32, 0x45, 0x47, 0x6e, 0x49, 0x2f, 0x79, 0x75, - 0x75, 0x6d, 0x30, 0x36, 0x5a, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7a, 0x56, - 0x2b, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4d, 0x48, 0x61, 0x75, 0x56, 0x42, - 0x4a, 0x56, 0x4a, 0x38, 0x7a, 0x55, 0x74, 0x6c, 0x75, 0x4e, 0x4a, 0x62, - 0x64, 0x31, 0x33, 0x34, 0x2f, 0x74, 0x4a, 0x53, 0x37, 0x53, 0x73, 0x56, - 0x51, 0x65, 0x70, 0x6a, 0x35, 0x57, 0x7a, 0x0a, 0x74, 0x43, 0x4f, 0x37, - 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, 0x77, - 0x74, 0x50, 0x31, 0x4d, 0x56, 0x59, 0x77, 0x6e, 0x53, 0x6c, 0x63, 0x55, - 0x66, 0x49, 0x4b, 0x64, 0x7a, 0x58, 0x4f, 0x53, 0x30, 0x78, 0x5a, 0x4b, - 0x42, 0x67, 0x79, 0x4d, 0x55, 0x4e, 0x47, 0x50, 0x48, 0x67, 0x6d, 0x2b, - 0x46, 0x36, 0x48, 0x6d, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2b, 0x55, 0x51, - 0x0a, 0x76, 0x49, 0x4f, 0x6c, 0x43, 0x73, 0x52, 0x6e, 0x4b, 0x50, 0x5a, - 0x7a, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6e, 0x62, 0x44, 0x68, 0x78, 0x53, - 0x4a, 0x49, 0x54, 0x52, 0x4e, 0x72, 0x77, 0x39, 0x46, 0x44, 0x4b, 0x5a, - 0x4a, 0x6f, 0x62, 0x71, 0x37, 0x6e, 0x4d, 0x57, 0x78, 0x4d, 0x34, 0x4d, - 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6f, 0x30, 0x49, - 0x77, 0x51, 0x44, 0x41, 0x50, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, - 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, - 0x48, 0x2f, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, - 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6a, - 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, - 0x51, 0x55, 0x54, 0x69, 0x4a, 0x55, 0x49, 0x42, 0x69, 0x56, 0x0a, 0x35, - 0x75, 0x4e, 0x75, 0x35, 0x67, 0x2f, 0x36, 0x2b, 0x72, 0x6b, 0x53, 0x37, - 0x51, 0x59, 0x58, 0x6a, 0x7a, 0x6b, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, - 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x4c, 0x42, - 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6e, 0x4b, - 0x4a, 0x52, 0x76, 0x44, 0x6b, 0x68, 0x6a, 0x36, 0x7a, 0x48, 0x64, 0x36, - 0x6d, 0x63, 0x59, 0x0a, 0x31, 0x59, 0x6c, 0x39, 0x50, 0x4d, 0x57, 0x4c, - 0x53, 0x6e, 0x2f, 0x70, 0x76, 0x74, 0x73, 0x72, 0x46, 0x39, 0x2b, 0x77, - 0x58, 0x33, 0x4e, 0x33, 0x4b, 0x6a, 0x49, 0x54, 0x4f, 0x59, 0x46, 0x6e, - 0x51, 0x6f, 0x51, 0x6a, 0x38, 0x6b, 0x56, 0x6e, 0x4e, 0x65, 0x79, 0x49, - 0x76, 0x2f, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4d, 0x4e, 0x4b, 0x53, 0x75, - 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x0a, 0x4e, 0x65, 0x46, - 0x32, 0x32, 0x64, 0x2b, 0x6d, 0x51, 0x72, 0x76, 0x48, 0x52, 0x41, 0x69, - 0x47, 0x66, 0x7a, 0x5a, 0x30, 0x4a, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, - 0x55, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6b, 0x6e, 0x64, 0x74, 0x68, 0x2f, - 0x4a, 0x73, 0x77, 0x31, 0x48, 0x4b, 0x6a, 0x32, 0x5a, 0x4c, 0x37, 0x74, - 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4f, 0x47, 0x5a, 0x58, 0x31, 0x4e, - 0x47, 0x0a, 0x46, 0x64, 0x74, 0x6f, 0x6d, 0x2f, 0x44, 0x7a, 0x4d, 0x4e, - 0x55, 0x2b, 0x4d, 0x65, 0x4b, 0x4e, 0x68, 0x4a, 0x37, 0x6a, 0x69, 0x74, - 0x72, 0x61, 0x6c, 0x6a, 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, - 0x6c, 0x77, 0x55, 0x48, 0x42, 0x48, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, - 0x37, 0x41, 0x6a, 0x36, 0x34, 0x47, 0x78, 0x4a, 0x55, 0x54, 0x46, 0x79, - 0x38, 0x62, 0x4a, 0x5a, 0x39, 0x31, 0x0a, 0x38, 0x72, 0x47, 0x4f, 0x6d, - 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4b, - 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2f, 0x4d, 0x55, 0x52, - 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6b, - 0x77, 0x37, 0x2b, 0x55, 0x30, 0x62, 0x36, 0x4c, 0x4a, 0x33, 0x2f, 0x69, - 0x79, 0x4b, 0x35, 0x53, 0x39, 0x6b, 0x4a, 0x52, 0x61, 0x54, 0x65, 0x0a, - 0x70, 0x4c, 0x69, 0x61, 0x57, 0x4e, 0x30, 0x62, 0x66, 0x56, 0x4b, 0x66, - 0x6a, 0x6c, 0x6c, 0x44, 0x69, 0x49, 0x47, 0x6b, 0x6e, 0x69, 0x62, 0x56, - 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, - 0x6b, 0x68, 0x76, 0x6c, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6a, 0x79, 0x4e, - 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4c, 0x5a, 0x5a, 0x6d, 0x36, 0x7a, - 0x4e, 0x54, 0x66, 0x6c, 0x0a, 0x4d, 0x72, 0x59, 0x3d, 0x0a, 0x2d, 0x2d, - 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, - 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, - 0x00 -}; -unsigned int ca_pem_len = 2557; +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +unsigned char ca_pem[] = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x64, 0x7a, 0x43, 0x43, + 0x41, 0x6c, 0x2b, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, + 0x41, 0x67, 0x41, 0x41, 0x75, 0x54, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, + 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, + 0x41, 0x44, 0x42, 0x61, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x4a, 0x0a, 0x52, 0x54, 0x45, + 0x53, 0x4d, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, + 0x4a, 0x51, 0x6d, 0x46, 0x73, 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, + 0x6c, 0x4d, 0x52, 0x4d, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x4c, 0x45, 0x77, 0x70, 0x44, 0x65, 0x57, 0x4a, 0x6c, 0x63, 0x6c, 0x52, + 0x79, 0x64, 0x58, 0x4e, 0x30, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, + 0x44, 0x0a, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6c, 0x43, 0x59, 0x57, + 0x78, 0x30, 0x61, 0x57, 0x31, 0x76, 0x63, 0x6d, 0x55, 0x67, 0x51, 0x33, + 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x43, + 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, + 0x41, 0x77, 0x4d, 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x45, 0x34, 0x4e, 0x44, + 0x59, 0x77, 0x4d, 0x46, 0x6f, 0x58, 0x0a, 0x44, 0x54, 0x49, 0x31, 0x4d, + 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x49, 0x7a, 0x4e, 0x54, 0x6b, 0x77, 0x4d, + 0x46, 0x6f, 0x77, 0x57, 0x6a, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x53, 0x55, 0x55, 0x78, 0x45, + 0x6a, 0x41, 0x51, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x6f, 0x54, 0x43, + 0x55, 0x4a, 0x68, 0x62, 0x48, 0x52, 0x70, 0x62, 0x57, 0x39, 0x79, 0x0a, + 0x5a, 0x54, 0x45, 0x54, 0x4d, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x43, 0x78, 0x4d, 0x4b, 0x51, 0x33, 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, + 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4d, 0x5a, 0x51, 0x6d, 0x46, 0x73, + 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, 0x6c, 0x49, 0x45, 0x4e, 0x35, + 0x59, 0x6d, 0x56, 0x79, 0x0a, 0x56, 0x48, 0x4a, 0x31, 0x63, 0x33, 0x51, + 0x67, 0x55, 0x6d, 0x39, 0x76, 0x64, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, + 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, + 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, + 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6f, 0x43, 0x67, 0x67, 0x45, + 0x42, 0x41, 0x4b, 0x4d, 0x45, 0x75, 0x79, 0x4b, 0x72, 0x0a, 0x6d, 0x44, + 0x31, 0x58, 0x36, 0x43, 0x5a, 0x79, 0x6d, 0x72, 0x56, 0x35, 0x31, 0x43, + 0x6e, 0x69, 0x34, 0x65, 0x69, 0x56, 0x67, 0x4c, 0x47, 0x77, 0x34, 0x31, + 0x75, 0x4f, 0x4b, 0x79, 0x6d, 0x61, 0x5a, 0x4e, 0x2b, 0x68, 0x58, 0x65, + 0x32, 0x77, 0x43, 0x51, 0x56, 0x74, 0x32, 0x79, 0x67, 0x75, 0x7a, 0x6d, + 0x4b, 0x69, 0x59, 0x76, 0x36, 0x30, 0x69, 0x4e, 0x6f, 0x53, 0x36, 0x7a, + 0x6a, 0x72, 0x0a, 0x49, 0x5a, 0x33, 0x41, 0x51, 0x53, 0x73, 0x42, 0x55, + 0x6e, 0x75, 0x49, 0x64, 0x39, 0x4d, 0x63, 0x6a, 0x38, 0x65, 0x36, 0x75, + 0x59, 0x69, 0x31, 0x61, 0x67, 0x6e, 0x6e, 0x63, 0x2b, 0x67, 0x52, 0x51, + 0x4b, 0x66, 0x52, 0x7a, 0x4d, 0x70, 0x69, 0x6a, 0x53, 0x33, 0x6c, 0x6a, + 0x77, 0x75, 0x6d, 0x55, 0x4e, 0x4b, 0x6f, 0x55, 0x4d, 0x4d, 0x6f, 0x36, + 0x76, 0x57, 0x72, 0x4a, 0x59, 0x65, 0x4b, 0x0a, 0x6d, 0x70, 0x59, 0x63, + 0x71, 0x57, 0x65, 0x34, 0x50, 0x77, 0x7a, 0x56, 0x39, 0x2f, 0x6c, 0x53, + 0x45, 0x79, 0x2f, 0x43, 0x47, 0x39, 0x56, 0x77, 0x63, 0x50, 0x43, 0x50, + 0x77, 0x42, 0x4c, 0x4b, 0x42, 0x73, 0x75, 0x61, 0x34, 0x64, 0x6e, 0x4b, + 0x4d, 0x33, 0x70, 0x33, 0x31, 0x76, 0x6a, 0x73, 0x75, 0x66, 0x46, 0x6f, + 0x52, 0x45, 0x4a, 0x49, 0x45, 0x39, 0x4c, 0x41, 0x77, 0x71, 0x53, 0x75, + 0x0a, 0x58, 0x6d, 0x44, 0x2b, 0x74, 0x71, 0x59, 0x46, 0x2f, 0x4c, 0x54, + 0x64, 0x42, 0x31, 0x6b, 0x43, 0x31, 0x46, 0x6b, 0x59, 0x6d, 0x47, 0x50, + 0x31, 0x70, 0x57, 0x50, 0x67, 0x6b, 0x41, 0x78, 0x39, 0x58, 0x62, 0x49, + 0x47, 0x65, 0x76, 0x4f, 0x46, 0x36, 0x75, 0x76, 0x55, 0x41, 0x36, 0x35, + 0x65, 0x68, 0x44, 0x35, 0x66, 0x2f, 0x78, 0x58, 0x74, 0x61, 0x62, 0x7a, + 0x35, 0x4f, 0x54, 0x5a, 0x79, 0x0a, 0x64, 0x63, 0x39, 0x33, 0x55, 0x6b, + 0x33, 0x7a, 0x79, 0x5a, 0x41, 0x73, 0x75, 0x54, 0x33, 0x6c, 0x79, 0x53, + 0x4e, 0x54, 0x50, 0x78, 0x38, 0x6b, 0x6d, 0x43, 0x46, 0x63, 0x42, 0x35, + 0x6b, 0x70, 0x76, 0x63, 0x59, 0x36, 0x37, 0x4f, 0x64, 0x75, 0x68, 0x6a, + 0x70, 0x72, 0x6c, 0x33, 0x52, 0x6a, 0x4d, 0x37, 0x31, 0x6f, 0x47, 0x44, + 0x48, 0x77, 0x65, 0x49, 0x31, 0x32, 0x76, 0x2f, 0x79, 0x65, 0x0a, 0x6a, + 0x6c, 0x30, 0x71, 0x68, 0x71, 0x64, 0x4e, 0x6b, 0x4e, 0x77, 0x6e, 0x47, + 0x6a, 0x6b, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4e, 0x46, 0x4d, + 0x45, 0x4d, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, + 0x42, 0x59, 0x45, 0x46, 0x4f, 0x57, 0x64, 0x57, 0x54, 0x43, 0x43, 0x52, + 0x31, 0x6a, 0x4d, 0x72, 0x50, 0x6f, 0x49, 0x56, 0x44, 0x61, 0x47, 0x65, + 0x7a, 0x71, 0x31, 0x0a, 0x42, 0x45, 0x33, 0x77, 0x4d, 0x42, 0x49, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, + 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x4d, 0x77, + 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x41, 0x30, 0x47, + 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0a, 0x44, 0x51, 0x45, + 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, + 0x46, 0x44, 0x46, 0x32, 0x4f, 0x35, 0x47, 0x39, 0x52, 0x61, 0x45, 0x49, + 0x46, 0x6f, 0x4e, 0x32, 0x37, 0x54, 0x79, 0x63, 0x6c, 0x68, 0x41, 0x4f, + 0x39, 0x39, 0x32, 0x54, 0x39, 0x4c, 0x64, 0x63, 0x77, 0x34, 0x36, 0x51, + 0x51, 0x46, 0x2b, 0x76, 0x61, 0x4b, 0x53, 0x6d, 0x32, 0x65, 0x54, 0x39, + 0x32, 0x0a, 0x39, 0x68, 0x6b, 0x54, 0x49, 0x37, 0x67, 0x51, 0x43, 0x76, + 0x6c, 0x59, 0x70, 0x4e, 0x52, 0x68, 0x63, 0x4c, 0x30, 0x45, 0x59, 0x57, + 0x6f, 0x53, 0x69, 0x68, 0x66, 0x56, 0x43, 0x72, 0x33, 0x46, 0x76, 0x44, + 0x42, 0x38, 0x31, 0x75, 0x6b, 0x4d, 0x4a, 0x59, 0x32, 0x47, 0x51, 0x45, + 0x2f, 0x73, 0x7a, 0x4b, 0x4e, 0x2b, 0x4f, 0x4d, 0x59, 0x33, 0x45, 0x55, + 0x2f, 0x74, 0x33, 0x57, 0x67, 0x78, 0x0a, 0x6a, 0x6b, 0x7a, 0x53, 0x73, + 0x77, 0x46, 0x30, 0x37, 0x72, 0x35, 0x31, 0x58, 0x67, 0x64, 0x49, 0x47, + 0x6e, 0x39, 0x77, 0x2f, 0x78, 0x5a, 0x63, 0x68, 0x4d, 0x42, 0x35, 0x68, + 0x62, 0x67, 0x46, 0x2f, 0x58, 0x2b, 0x2b, 0x5a, 0x52, 0x47, 0x6a, 0x44, + 0x38, 0x41, 0x43, 0x74, 0x50, 0x68, 0x53, 0x4e, 0x7a, 0x6b, 0x45, 0x31, + 0x61, 0x6b, 0x78, 0x65, 0x68, 0x69, 0x2f, 0x6f, 0x43, 0x72, 0x30, 0x0a, + 0x45, 0x70, 0x6e, 0x33, 0x6f, 0x30, 0x57, 0x43, 0x34, 0x7a, 0x78, 0x65, + 0x39, 0x5a, 0x32, 0x65, 0x74, 0x63, 0x69, 0x65, 0x66, 0x43, 0x37, 0x49, + 0x70, 0x4a, 0x35, 0x4f, 0x43, 0x42, 0x52, 0x4c, 0x62, 0x66, 0x31, 0x77, + 0x62, 0x57, 0x73, 0x61, 0x59, 0x37, 0x31, 0x6b, 0x35, 0x68, 0x2b, 0x33, + 0x7a, 0x76, 0x44, 0x79, 0x6e, 0x79, 0x36, 0x37, 0x47, 0x37, 0x66, 0x79, + 0x55, 0x49, 0x68, 0x7a, 0x0a, 0x6b, 0x73, 0x4c, 0x69, 0x34, 0x78, 0x61, + 0x4e, 0x6d, 0x6a, 0x49, 0x43, 0x71, 0x34, 0x34, 0x59, 0x33, 0x65, 0x6b, + 0x51, 0x45, 0x65, 0x35, 0x2b, 0x4e, 0x61, 0x75, 0x51, 0x72, 0x7a, 0x34, + 0x77, 0x6c, 0x48, 0x72, 0x51, 0x4d, 0x7a, 0x32, 0x6e, 0x5a, 0x51, 0x2f, + 0x31, 0x2f, 0x49, 0x36, 0x65, 0x59, 0x73, 0x39, 0x48, 0x52, 0x43, 0x77, + 0x42, 0x58, 0x62, 0x73, 0x64, 0x74, 0x54, 0x4c, 0x53, 0x0a, 0x52, 0x39, + 0x49, 0x34, 0x4c, 0x74, 0x44, 0x2b, 0x67, 0x64, 0x77, 0x79, 0x61, 0x68, + 0x36, 0x31, 0x37, 0x6a, 0x7a, 0x56, 0x2f, 0x4f, 0x65, 0x42, 0x48, 0x52, + 0x6e, 0x44, 0x4a, 0x45, 0x4c, 0x71, 0x59, 0x7a, 0x6d, 0x70, 0x0a, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x6a, 0x6a, + 0x43, 0x43, 0x41, 0x6e, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x51, 0x41, 0x7a, 0x72, 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, + 0x43, 0x37, 0x4b, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6e, 0x36, 0x35, 0x54, + 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x68, 0x0a, 0x4d, + 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, + 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x45, 0x56, 0x4d, 0x42, 0x4d, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, 0x4d, 0x52, 0x47, 0x6c, 0x6e, 0x61, + 0x55, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6a, 0x4d, + 0x52, 0x6b, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4c, 0x45, + 0x78, 0x42, 0x33, 0x0a, 0x64, 0x33, 0x63, 0x75, 0x5a, 0x47, 0x6c, 0x6e, + 0x61, 0x57, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, + 0x4d, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, + 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, + 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, + 0x55, 0x6d, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x0a, 0x4d, 0x6a, 0x41, + 0x65, 0x46, 0x77, 0x30, 0x78, 0x4d, 0x7a, 0x41, 0x34, 0x4d, 0x44, 0x45, + 0x78, 0x4d, 0x6a, 0x41, 0x77, 0x4d, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, + 0x7a, 0x4f, 0x44, 0x41, 0x78, 0x4d, 0x54, 0x55, 0x78, 0x4d, 0x6a, 0x41, + 0x77, 0x4d, 0x44, 0x42, 0x61, 0x4d, 0x47, 0x45, 0x78, 0x43, 0x7a, 0x41, + 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, + 0x54, 0x0a, 0x4d, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x4b, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x42, 0x4a, 0x62, 0x6d, 0x4d, 0x78, 0x47, 0x54, + 0x41, 0x58, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, + 0x64, 0x33, 0x64, 0x79, 0x35, 0x6b, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x35, 0x6a, 0x0a, 0x62, 0x32, 0x30, 0x78, 0x49, + 0x44, 0x41, 0x65, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, 0x54, 0x46, + 0x30, 0x52, 0x70, 0x5a, 0x32, 0x6c, 0x44, 0x5a, 0x58, 0x4a, 0x30, 0x49, + 0x45, 0x64, 0x73, 0x62, 0x32, 0x4a, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, + 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4d, 0x49, 0x49, 0x42, 0x49, + 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x0a, + 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, + 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4b, 0x43, + 0x41, 0x51, 0x45, 0x41, 0x75, 0x7a, 0x66, 0x4e, 0x4e, 0x4e, 0x78, 0x37, + 0x61, 0x38, 0x6d, 0x79, 0x61, 0x4a, 0x43, 0x74, 0x53, 0x6e, 0x58, 0x2f, + 0x52, 0x72, 0x6f, 0x68, 0x43, 0x67, 0x69, 0x4e, 0x39, 0x52, 0x6c, 0x55, + 0x79, 0x66, 0x75, 0x49, 0x0a, 0x32, 0x2f, 0x4f, 0x75, 0x38, 0x6a, 0x71, + 0x4a, 0x6b, 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6d, 0x76, + 0x50, 0x72, 0x43, 0x33, 0x6f, 0x58, 0x67, 0x6b, 0x6b, 0x52, 0x4c, 0x70, + 0x69, 0x6d, 0x6e, 0x37, 0x57, 0x6f, 0x36, 0x68, 0x2b, 0x34, 0x46, 0x52, + 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, 0x4c, 0x65, 0x63, 0x59, 0x78, 0x70, + 0x73, 0x4d, 0x4e, 0x7a, 0x61, 0x48, 0x78, 0x6d, 0x78, 0x0a, 0x31, 0x78, + 0x37, 0x65, 0x2f, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4e, 0x36, + 0x37, 0x73, 0x48, 0x30, 0x4e, 0x4f, 0x33, 0x58, 0x73, 0x73, 0x30, 0x72, + 0x30, 0x75, 0x70, 0x53, 0x2f, 0x6b, 0x71, 0x62, 0x69, 0x74, 0x4f, 0x74, + 0x53, 0x5a, 0x70, 0x4c, 0x59, 0x6c, 0x36, 0x5a, 0x74, 0x72, 0x41, 0x47, + 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6b, 0x59, 0x39, 0x32, + 0x65, 0x51, 0x0a, 0x71, 0x32, 0x45, 0x47, 0x6e, 0x49, 0x2f, 0x79, 0x75, + 0x75, 0x6d, 0x30, 0x36, 0x5a, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7a, 0x56, + 0x2b, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4d, 0x48, 0x61, 0x75, 0x56, 0x42, + 0x4a, 0x56, 0x4a, 0x38, 0x7a, 0x55, 0x74, 0x6c, 0x75, 0x4e, 0x4a, 0x62, + 0x64, 0x31, 0x33, 0x34, 0x2f, 0x74, 0x4a, 0x53, 0x37, 0x53, 0x73, 0x56, + 0x51, 0x65, 0x70, 0x6a, 0x35, 0x57, 0x7a, 0x0a, 0x74, 0x43, 0x4f, 0x37, + 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, 0x77, + 0x74, 0x50, 0x31, 0x4d, 0x56, 0x59, 0x77, 0x6e, 0x53, 0x6c, 0x63, 0x55, + 0x66, 0x49, 0x4b, 0x64, 0x7a, 0x58, 0x4f, 0x53, 0x30, 0x78, 0x5a, 0x4b, + 0x42, 0x67, 0x79, 0x4d, 0x55, 0x4e, 0x47, 0x50, 0x48, 0x67, 0x6d, 0x2b, + 0x46, 0x36, 0x48, 0x6d, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2b, 0x55, 0x51, + 0x0a, 0x76, 0x49, 0x4f, 0x6c, 0x43, 0x73, 0x52, 0x6e, 0x4b, 0x50, 0x5a, + 0x7a, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6e, 0x62, 0x44, 0x68, 0x78, 0x53, + 0x4a, 0x49, 0x54, 0x52, 0x4e, 0x72, 0x77, 0x39, 0x46, 0x44, 0x4b, 0x5a, + 0x4a, 0x6f, 0x62, 0x71, 0x37, 0x6e, 0x4d, 0x57, 0x78, 0x4d, 0x34, 0x4d, + 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6f, 0x30, 0x49, + 0x77, 0x51, 0x44, 0x41, 0x50, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, + 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, + 0x48, 0x2f, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, + 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6a, + 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, + 0x51, 0x55, 0x54, 0x69, 0x4a, 0x55, 0x49, 0x42, 0x69, 0x56, 0x0a, 0x35, + 0x75, 0x4e, 0x75, 0x35, 0x67, 0x2f, 0x36, 0x2b, 0x72, 0x6b, 0x53, 0x37, + 0x51, 0x59, 0x58, 0x6a, 0x7a, 0x6b, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, + 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x4c, 0x42, + 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6e, 0x4b, + 0x4a, 0x52, 0x76, 0x44, 0x6b, 0x68, 0x6a, 0x36, 0x7a, 0x48, 0x64, 0x36, + 0x6d, 0x63, 0x59, 0x0a, 0x31, 0x59, 0x6c, 0x39, 0x50, 0x4d, 0x57, 0x4c, + 0x53, 0x6e, 0x2f, 0x70, 0x76, 0x74, 0x73, 0x72, 0x46, 0x39, 0x2b, 0x77, + 0x58, 0x33, 0x4e, 0x33, 0x4b, 0x6a, 0x49, 0x54, 0x4f, 0x59, 0x46, 0x6e, + 0x51, 0x6f, 0x51, 0x6a, 0x38, 0x6b, 0x56, 0x6e, 0x4e, 0x65, 0x79, 0x49, + 0x76, 0x2f, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4d, 0x4e, 0x4b, 0x53, 0x75, + 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x0a, 0x4e, 0x65, 0x46, + 0x32, 0x32, 0x64, 0x2b, 0x6d, 0x51, 0x72, 0x76, 0x48, 0x52, 0x41, 0x69, + 0x47, 0x66, 0x7a, 0x5a, 0x30, 0x4a, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, + 0x55, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6b, 0x6e, 0x64, 0x74, 0x68, 0x2f, + 0x4a, 0x73, 0x77, 0x31, 0x48, 0x4b, 0x6a, 0x32, 0x5a, 0x4c, 0x37, 0x74, + 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4f, 0x47, 0x5a, 0x58, 0x31, 0x4e, + 0x47, 0x0a, 0x46, 0x64, 0x74, 0x6f, 0x6d, 0x2f, 0x44, 0x7a, 0x4d, 0x4e, + 0x55, 0x2b, 0x4d, 0x65, 0x4b, 0x4e, 0x68, 0x4a, 0x37, 0x6a, 0x69, 0x74, + 0x72, 0x61, 0x6c, 0x6a, 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, + 0x6c, 0x77, 0x55, 0x48, 0x42, 0x48, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, + 0x37, 0x41, 0x6a, 0x36, 0x34, 0x47, 0x78, 0x4a, 0x55, 0x54, 0x46, 0x79, + 0x38, 0x62, 0x4a, 0x5a, 0x39, 0x31, 0x0a, 0x38, 0x72, 0x47, 0x4f, 0x6d, + 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4b, + 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2f, 0x4d, 0x55, 0x52, + 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6b, + 0x77, 0x37, 0x2b, 0x55, 0x30, 0x62, 0x36, 0x4c, 0x4a, 0x33, 0x2f, 0x69, + 0x79, 0x4b, 0x35, 0x53, 0x39, 0x6b, 0x4a, 0x52, 0x61, 0x54, 0x65, 0x0a, + 0x70, 0x4c, 0x69, 0x61, 0x57, 0x4e, 0x30, 0x62, 0x66, 0x56, 0x4b, 0x66, + 0x6a, 0x6c, 0x6c, 0x44, 0x69, 0x49, 0x47, 0x6b, 0x6e, 0x69, 0x62, 0x56, + 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, + 0x6b, 0x68, 0x76, 0x6c, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6a, 0x79, 0x4e, + 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4c, 0x5a, 0x5a, 0x6d, 0x36, 0x7a, + 0x4e, 0x54, 0x66, 0x6c, 0x0a, 0x4d, 0x72, 0x59, 0x3d, 0x0a, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, + 0x00 +}; +unsigned int ca_pem_len = 2557;