Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-c.yml
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ jobs:
# actually had -- coverage computed over fewer tests than anyone thought.
# Now that no suite decides at run time whether to participate, the count
# is deterministic, so assert it exactly. Bump this when adding a suite.
EXPECTED_TESTS: '51'
EXPECTED_TESTS: '52'
services:
mosquitto:
image: eclipse-mosquitto:2
Expand Down
8 changes: 8 additions & 0 deletions c/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@
server's Maximum Packet Size, is the new `AZ_IOT_ERR_PUBLISH_REFUSED`, quota exceeded (`0x97`) is `AZ_IOT_ERR_BUSY`, other failures stay
`AZ_IOT_ERR_MQTT`. The az_mqtt adapter uses it; publish callbacks receive it unchanged, and the
connection client logs the code of a failed PUBACK.
- Software updates: `AZ_IOT_SU_REQUEST_BUFFER_SIZE` defaults to 16384 (was 4096), so offers above
4 KiB are no longer refused; the client struct grows by about 24 KiB. The manifest signature is
decoded into the client's own scratch, mostly in place, rather than fixed stack buffers: a
signature that fits the request buffer verifies unless its SJWK header alone exceeds a quarter
of it, and the verifier's stack drops from about 6.5 KiB to under 1 KiB.
`az_iot_su_parse_update_request()` uses `AZ_IOT_SU_VERIFY_SCRATCH_SIZE` (8192) bytes of stack
instead. A part that does not fit is logged as too large, not as invalid. The ESP32 sample sets
the request buffer to 8192 and its NVS partition to 36 KB (erase the flash to move to it).
3 changes: 2 additions & 1 deletion c/docs/client-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ Defaults used when the matching option is left at 0:
| Macro | Default | Bounds |
| --- | --- | --- |
| `AZ_IOT_SU_MAX_ROOT_KEYS` | 4 | Root keys in the trust store. |
| `AZ_IOT_SU_REQUEST_BUFFER_SIZE` | 4096 | Copy of the update metadata (manifest and signature) for the current deployment. A larger deployment is refused: `AZ_IOT_SU_EVENT_UPDATE_REFUSED` is raised with `AZ_IOT_ERR_NOT_ENOUGH_SPACE`. |
| `AZ_IOT_SU_REQUEST_BUFFER_SIZE` | 16384 | Copy of the update metadata (manifest, signature and file URLs) for the current deployment; a one-file offer is about 5 KiB. A larger deployment is refused: `AZ_IOT_SU_EVENT_UPDATE_REFUSED` is raised with `AZ_IOT_ERR_NOT_ENOUGH_SPACE`. Also sizes `AZ_IOT_SU_STATE_BLOB_MAX_SIZE` and the scratch the client verifies the signature in, so the client holds about twice this value. |
| `AZ_IOT_SU_VERIFY_SCRATCH_SIZE` | 8192 | Stack used by `az_iot_su_parse_update_request()` for the decoded signature (JWS header with the signing key, and the signatures). A signature that does not fit fails with `AZ_IOT_ERR_AUTH` and an `ERROR` log naming the part that is too large. |
| `AZ_IOT_SU_WORKFLOW_ID_SIZE` | 64 | Workflow ID kept for reporting, duplicate detection and persistence. A deployment with a longer ID is refused: nothing is processed or reported, and `AZ_IOT_SU_EVENT_UPDATE_REFUSED` is raised with `AZ_IOT_ERR_NOT_ENOUGH_SPACE`. |
| `AZ_IOT_SU_PERSIST_MAX_ATTEMPTS` | 5 | Consecutive failed state writes before the client stops retrying (1 to `0xFFFFFFFF`). |
| `AZ_IOT_SU_DEVICE_PROPERTIES_BUFFER_SIZE` | 512 | Default size of the device-properties buffer type. Application-side only; `az_iot_su_device_properties_buffer_size()` gives the exact size. |
Expand Down
3 changes: 2 additions & 1 deletion c/docs/eng/software-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ updates core owns all orchestration**:

| Responsibility | Owner |
|----------------|-------|
| JWS compact parsing, base64url decode | **core** (uses `az::core` base64/JSON it already links) |
| JWS compact parsing, base64url decode | **core** (uses `az::core` base64/JSON it already links; base64 through `az_iot_base64_decode()`) |
| SJWK extraction, JWK `n`/`e` parsing | **core** |
| Root-key store, `kid` resolution | **core** (see §7) |
| Revocation enforcement (disabled kids) | **core** |
Expand Down Expand Up @@ -1463,6 +1463,7 @@ This avoids duplicating the well-tested JSON parsing logic and keeps us aligned
| Workflow struct with `action`, `id`, `retry_timestamp` | Only `id` is used (from `workflowId`) |
| File hash parsing (`hash_type` + `hash_value` as `az_span`) | ✅ Sufficient |
| `az_json_string_unescape()` for manifest string unescaping | Not used: it stops at `\u` escapes. The SDK decodes JSON strings with `az_iot_json_string_decode()` (`src/core/json_string.c`) |
| `az_base64_url_decode()` / `az_base64_decode()` for JWS segments | Used through `az_iot_base64_decode()` (`src/core/base64.c`), which first rejects characters outside the alphabet (az_core left-shifts a negative value on them), misplaced padding and nonzero trailing bits, and decodes in blocks so the output may overwrite the input |
| `az_iot_hub_client_properties_writer_*` for PnP component wrapping | Not used (device twin only) |

### Configurable Limits (No Source Change Needed)
Expand Down
9 changes: 9 additions & 0 deletions c/docs/eng/test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,9 @@ RS256 signature verification) through the crypto backends in `c/adapters/crypto_
| | Standalone entry points check the backend they need | `az_iot_su_parse_update_request()`: no backend is INVALID_ARG, no `verify_rs256` is NOT_SUPPORTED, and non-NULL outputs are zeroed on both. `az_iot_su_verify_file_hash()`: no backend is INVALID_ARG; a SHA-256-only backend verifies. | unit | Done | [standalone_entry_points_check_the_backend_they_need](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c) |
| | Manifest signed by an unknown root key is rejected | End-to-end through `az_iot_su_parse_update_request()`. | unit | Done | [manifest_signed_by_an_unknown_root_key_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1428) |
| | Malformed jws is rejected | Wrong segment count, bad base64url, missing header. | unit | Done | [malformed_jws_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1449) |
| | Near-limit nested signing key is verified | An offer near `AZ_IOT_SU_REQUEST_BUFFER_SIZE`, almost all signing JWK, verifies in the managed client. | unit | Done | [near_limit_nested_signing_key_is_verified](../../tests/unit/su_client_test.c) |
| | Large signature is verified | JWS header and signing key over 2 KiB in an offer over 4 KiB; modulus in standard base64 with escaped slashes, asserted as decoded with the exponent and signature (managed and public parser). | unit | Done | [large_signature_is_verified](../../tests/unit/su_client_test.c) |
| | Oversized signature is reported as too large | A header that does not fit `AZ_IOT_SU_VERIFY_SCRATCH_SIZE` is logged as too large; a malformed one as not valid. | unit | Done | [oversized_signature_is_reported_as_too_large](../../tests/unit/su_client_test.c) |
| | Malformed manifest JSON is rejected | — | unit | Done | [malformed_manifest_json_is_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1481) |
| | Verify file hash rejects an unsupported algorithm | — | unit | Done | [verify_file_hash_rejects_an_unsupported_algorithm](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/su_client_test.c#L1534) |
| End-to-end | Real update is downloaded, verified, installed and reported | A real offer through the whole client: real roots, real download and hash, SUCCEEDED acknowledged. Passes live; not yet in CI. | e2e | Pending | [real_update_is_downloaded_verified_installed_and_reported](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/e2e/tests/e2e_su_offer_test.c) |
Expand Down Expand Up @@ -1157,6 +1160,12 @@ own, so they are unit-tested only.
| | A short buffer fails and is wiped | One byte short, or too short for the string to sign: `AZ_IOT_ERR_NOT_ENOUGH_SPACE`, buffer zeroed. | unit | Done | [a_short_buffer_fails_and_is_wiped](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/sas_token_test.c) |
| | Invalid arguments are rejected | NULLs, empty URI, 0 expiry, unusable crypto, empty key; `&`, space, newline, `=`, `/` in the URI or key name. | unit | Done | [invalid_arguments_are_rejected](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/sas_token_test.c) |
| | Derive rejects bad arguments and zeroes its output | Empty or NULL ID or key, no crypto: `AZ_IOT_ERR_INVALID_ARG`, output zeroed. | unit | Done | [derive_rejects_bad_arguments_and_zeroes_its_output](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/sas_token_test.c) |
| Base64 decode | base64url decodes every tail length | 0, 2 and 3 trailing characters; `-`/`_`. | unit | Done | [base64url_decodes_every_tail_length](../../tests/unit/base64_test.c) |
| | Standard base64 is accepted only when allowed | Padded `+`/`/` text. | unit | Done | [standard_base64_is_accepted_only_when_allowed](../../tests/unit/base64_test.c) |
| | Malformed input is rejected | Empty, bad length, out-of-alphabet, mixed alphabets, misplaced or excess padding. | unit | Done | [malformed_input_is_rejected](../../tests/unit/base64_test.c) |
| | Nonzero trailing bits are rejected | Non-canonical text that az_core would decode to the same bytes. | unit | Done | [nonzero_trailing_bits_are_rejected](../../tests/unit/base64_test.c) |
| | Destination bounds are checked | One byte short fails; exact fits; NULL output rejected. | unit | Done | [destination_bounds_are_checked](../../tests/unit/base64_test.c) |
| | Block boundaries decode in place | Lengths around the 64-character block, separate and in place. | unit | Done | [block_boundaries_decode_in_place](../../tests/unit/base64_test.c) |
| JSON string decode | Plain text is copied | — | unit | Done | [plain_text_is_copied](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/json_string_test.c) |
| | Every two-character escape decodes | — | unit | Done | [every_two_character_escape_decodes](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/json_string_test.c) |
| | Unicode escapes cover each UTF-8 width | 1-, 2- and 3-byte bounds. | unit | Done | [unicode_escapes_cover_each_utf8_width](https://github.com/Azure/azure-iot-sdk/blob/main/c/tests/unit/json_string_test.c) |
Expand Down
23 changes: 18 additions & 5 deletions c/inc/azure/iot/az_iot_su.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,23 @@ extern "C"
* valid during the subscriber callback, but the workflow is processed
* asynchronously across many do_work() calls; the upstream parser stores spans
* that point INTO this payload (and unescapes the manifest in place), so it must
* outlive the callback. Sized for a v5 manifest with the upstream's bounded
* step/file counts plus the JWS signature. Override if your deployments are
* larger. No heap is used. */
* outlive the callback. Holds the escaped manifest, its JWS signature and the
* file URLs; a one-file offer is about 5 KiB. Override if your deployments are
* larger. It also bounds the scratch the client verifies the signature in (see
* AZ_IOT_SU_PERSIST_BLOB_SIZE). No heap is used. */
#ifndef AZ_IOT_SU_REQUEST_BUFFER_SIZE
#define AZ_IOT_SU_REQUEST_BUFFER_SIZE 4096
#define AZ_IOT_SU_REQUEST_BUFFER_SIZE 16384
Comment thread
Copilot marked this conversation as resolved.
#endif

/**
* @brief Stack scratch az_iot_su_parse_update_request() decodes the manifest signature into.
*
* Holds the decoded JWS header (which embeds the signing key), the signing key and the
* signatures. A signature that does not fit is rejected with AZ_IOT_ERR_AUTH and an error
* log naming the part that is too large. The managed client does not use it.
*/
#ifndef AZ_IOT_SU_VERIFY_SCRATCH_SIZE
#define AZ_IOT_SU_VERIFY_SCRATCH_SIZE 8192
#endif

/** @brief Capacity of the packed applied update id (provider, name, version). */
Expand Down Expand Up @@ -1141,7 +1153,8 @@ extern "C"
*
* Verifies the manifest trust chain (JWS/SJWK, root-key `kid`, RS256, both
* RSA checks, SHA-256 binding), then parses the manifest. Fail-closed:
* non-NULL outputs are zeroed on every error.
* non-NULL outputs are zeroed on every error. Uses
* AZ_IOT_SU_VERIFY_SCRATCH_SIZE bytes of stack for the decoded signature.
*
* @param request_json `{ workflowId, updateManifest, updateManifestSignature,
* fileUrls }` as the service sends it. The
Expand Down
7 changes: 7 additions & 0 deletions c/samples/software_update/esp32/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@ cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/components")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# Applies to the SDK component and the app alike, as their struct layouts must
# match. Halves the 16 KiB default: a one-file offer is about 5 KiB, and NVS
# (partitions*.csv) must hold two generations of the largest checkpoint
# (AZ_IOT_SU_STATE_BLOB_MAX_SIZE, about 8.6 KiB) while one replaces the other.
idf_build_set_property(COMPILE_DEFINITIONS "AZ_IOT_SU_REQUEST_BUFFER_SIZE=8192" APPEND)

project(su_esp32)
2 changes: 1 addition & 1 deletion c/samples/software_update/esp32/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ sequenceDiagram
| `install` | `esp_ota_end` + `esp_ota_set_boot_partition`; returns `REBOOT_REQUIRED` |
| `apply` | No-op; the new image is already running |
| `restore` | Rollback to the running partition |
| `persist` / `load` state | NVS namespace `su_sample`, key `wf_state` (resume across the reboot) |
| `persist` / `load` state | NVS namespace `su_sample`, key `wf_state` (resume across the reboot). The 36 KB `nvs` partition holds two generations of the largest checkpoint; [CMakeLists.txt](CMakeLists.txt) sets `AZ_IOT_SU_REQUEST_BUFFER_SIZE` to 8192 to bound it. Erase the flash when moving from the earlier 24 KB layout. |

Once connected to its hub, the new image calls `su_esp32_ota_mark_valid()`
(`esp_ota_mark_app_valid_cancel_rollback`). If it crashes or never connects, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ idf_component_register(
"${AZ_IOT_ROOT}/src/core/proto3.c"
"${AZ_IOT_ROOT}/src/core/span_writer.c"
"${AZ_IOT_ROOT}/src/core/json_string.c"
"${AZ_IOT_ROOT}/src/core/base64.c"
"${AZ_IOT_ROOT}/src/core/message.c"
"${AZ_IOT_ROOT}/src/core/certificate_provider_pem.c"
"${AZ_IOT_ROOT}/src/core/crypto.c"
Expand Down
2 changes: 1 addition & 1 deletion c/samples/software_update/esp32/main/app_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ void app_main(void)
dp.installed_update_id.version = SU_UPDATE_VERSION;

static AZ_IOT_SU_DEVICE_PROPERTIES_STORAGE(dp_buffer);
/* ~14 KB: too large for the 12 KB main task stack. */
/* ~22 KB: too large for the 12 KB main task stack. */
static az_iot_su_client su;
az_iot_su_client_config_options su_opts = az_iot_su_client_config_options_default();
su_opts.hooks = &hooks;
Expand Down
6 changes: 3 additions & 3 deletions c/samples/software_update/esp32/partitions.csv
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# Sized for a 4 MB flash (the original ESP32-WROOM-32 module).
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
nvs, data, nvs, 0x9000, 0x9000,
otadata, data, ota, 0x12000, 0x2000,
phy_init, data, phy, 0x14000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x1D0000,
ota_1, app, ota_1, 0x1F0000, 0x1D0000,
2 changes: 1 addition & 1 deletion c/samples/software_update/esp32/partitions_secure.csv
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# efuse_em is used only with sdkconfig.secure_virtual_efuse.
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
nvs, data, nvs, , 0x9000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
nvs_keys, data, nvs_keys, , 0x1000, encrypted
Expand Down
1 change: 1 addition & 0 deletions c/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ add_library(az_iot_core
core/proto3.c
core/span_writer.c
core/json_string.c
core/base64.c
core/message.c
core/certificate_provider_pem.c
core/crypto.c
Expand Down
118 changes: 118 additions & 0 deletions c/src/core/base64.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license
// information.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding this? Don't we have base64 functionality in azure-sdk-for-c?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does use azure-sdk-for-c: the decoding itself is az_base64_url_decode()/az_base64_decode(). The wrapper adds what those lack for untrusted signature input:

  • Validation first. On a character outside the alphabet, az_core left-shifts a negative value (UBSan: az_base64.c:249, hit by the standard-base64 modulus the service sends when given to the url decoder); it also accepts nonzero trailing bits (AB decodes like AA).
  • In-place decoding. Verification decodes the SJWK signature, payload and signing key in place within the decoded header so a near-limit offer fits the scratch; az_core does not document overlapping input/output, so the wrapper decodes in 64-character blocks through a small buffer.
  • One call that picks url or standard alphabet (the modulus may be either).
    It replaced a local decoder in su_client.c after review asked for az_core plus a reusable, tested az_iot_* helper. No change made.


/* SPDX-License-Identifier: MIT */
#include <stdint.h>
#include <string.h>

#include <azure/core/az_base64.h>

#include "internal/base64.h"

/** @brief Characters decoded per az_core call; a multiple of 4. */
#define AZ_IOT_BASE64_BLOCK 64

/**
* @brief 6-bit value of a base64 character.
*
* @param c The character.
* @param std Standard alphabet ('+', '/') rather than base64url ('-', '_').
* @return The value; -1 outside the alphabet, including '='.
*/
static int32_t b64_value(uint8_t c, bool std)
{
if (c >= 'A' && c <= 'Z')
{
return c - 'A';
}
if (c >= 'a' && c <= 'z')
{
return c - 'a' + 26;
}
if (c >= '0' && c <= '9')
{
return c - '0' + 52;
}
if (c == (std ? '+' : '-'))
{
return 62;
}
if (c == (std ? '/' : '_'))
{
return 63;
}
return -1;
}

az_iot_result az_iot_base64_decode(az_span in, bool allow_std, az_span out, az_span* out_decoded)
{
const uint8_t* p = az_span_ptr(in);
int32_t len = az_span_size(in);
if (out_decoded == NULL || len <= 0)
{
return AZ_IOT_ERR_INVALID_ARG;
}

bool std = false;
for (int32_t i = 0; allow_std && i < len; ++i)
{
std = std || p[i] == '+' || p[i] == '/' || p[i] == '=';
}
int32_t data_len = len;
if (std)
{
if (len % 4 != 0)
{
return AZ_IOT_ERR_INVALID_ARG;
}
while (data_len > len - 2 && p[data_len - 1] == '=')
{
--data_len;
}
}
int32_t rem = data_len % 4;
if (data_len <= 0 || rem == 1 || (std && len - data_len != (4 - rem) % 4))
{
return AZ_IOT_ERR_INVALID_ARG;
}
for (int32_t i = 0; i < data_len; ++i)
{
if (b64_value(p[i], std) < 0)
{
return AZ_IOT_ERR_INVALID_ARG;
}
}
/* Canonical form: the bits past the last whole byte are zero. */
int32_t last = b64_value(p[data_len - 1], std);
if ((rem == 2 && (last & 0x0F) != 0) || (rem == 3 && (last & 0x03) != 0))
{
return AZ_IOT_ERR_INVALID_ARG;
}
if ((data_len / 4) * 3 + (rem * 3) / 4 > az_span_size(out))
{
return AZ_IOT_ERR_NOT_ENOUGH_SPACE;
}

/* Each block is read before its bytes are written, and output never passes
* input, so decoding in place is safe. */
uint8_t block[AZ_IOT_BASE64_BLOCK / 4 * 3];
uint8_t* dst = az_span_ptr(out);
int32_t w = 0;
for (int32_t i = 0; i < len; i += AZ_IOT_BASE64_BLOCK)
{
int32_t end = (len - i < AZ_IOT_BASE64_BLOCK) ? len : i + AZ_IOT_BASE64_BLOCK;
az_span src = az_span_slice(in, i, end);
int32_t written = 0;
az_result r = std ? az_base64_decode(AZ_SPAN_FROM_BUFFER(block), src, &written)
: az_base64_url_decode(AZ_SPAN_FROM_BUFFER(block), src, &written);
if (az_result_failed(r))
{
return AZ_IOT_ERR_INVALID_ARG;
}
memmove(dst + w, block, (size_t)written);
w += written;
}
*out_decoded = az_span_create(dst, w);
return AZ_IOT_OK;
}
51 changes: 51 additions & 0 deletions c/src/core/internal/base64.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license
// information.

/* SPDX-License-Identifier: MIT */
/**
* @file base64.h
* @brief Canonical base64 decoding, in place or not.
*
* Validates the input before handing it to az_core, which left-shifts a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding this? Don't we have base64 functionality in azure-sdk-for-c?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does use azure-sdk-for-c: the decoding itself is az_base64_url_decode()/az_base64_decode(). The wrapper adds what those lack for untrusted signature input:

  • Validation first. On a character outside the alphabet, az_core left-shifts a negative value (UBSan: az_base64.c:249, hit by the standard-base64 modulus the service sends when given to the url decoder); it also accepts nonzero trailing bits (AB decodes like AA).
  • In-place decoding. Verification decodes the SJWK signature, payload and signing key in place within the decoded header so a near-limit offer fits the scratch; az_core does not document overlapping input/output, so the wrapper decodes in 64-character blocks through a small buffer.
  • One call that picks url or standard alphabet (the modulus may be either).
    It replaced a local decoder in su_client.c after review asked for az_core plus a reusable, tested az_iot_* helper. No change made.

* negative value on a character outside the alphabet and accepts nonzero
* trailing bits. Decodes in blocks through a small buffer, so the output may
* overwrite the input.
*/
#ifndef AZ_IOT_BASE64_H
#define AZ_IOT_BASE64_H

#include <stdbool.h>

#include <azure/core/az_span.h>

#include "azure/iot/az_iot_result.h"

#ifdef __cplusplus
extern "C"
{
#endif

/**
* @brief Decode canonical unpadded base64url or, if @p allow_std, padded standard base64.
*
* Standard base64 is chosen when @p in has '+', '/' or '='. Its length must be a multiple
* of 4, with at most two trailing '='. Unused trailing bits must be zero.
*
* @param in Encoded text.
* @param allow_std Also accept standard base64.
* @param out Destination. May start at az_span_ptr(@p in) (in place); must not
* otherwise overlap @p in.
* @param out_decoded The decoded slice of @p out. Untouched on error; @p out may then hold
* a partial result.
* @return AZ_IOT_OK; AZ_IOT_ERR_INVALID_ARG if @p in is empty, has a character outside the
* alphabet, misplaced padding, a bad length or nonzero trailing bits;
* AZ_IOT_ERR_NOT_ENOUGH_SPACE when @p out is too small.
*/
az_iot_result az_iot_base64_decode(az_span in, bool allow_std, az_span out, az_span* out_decoded);

#ifdef __cplusplus
}
#endif

#endif /* AZ_IOT_BASE64_H */
Loading
Loading