Skip to content

[C] Hub renewal stores provider-made certificates; harden managed provider storage - #287

Closed
Ewerton Scaboro da Silva (ewertons) wants to merge 19 commits into
mainfrom
feature/c-use-issued-certificates
Closed

Ewerton Scaboro da Silva (ewertons) wants to merge 19 commits into
mainfrom
feature/c-use-issued-certificates

Conversation

@ewertons

@ewertons Ewerton Scaboro da Silva (ewertons) commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Hub renewal stores issued certificates in the certificate provider when the provider made the CSR. The DPS operational-cert callback always gets the chain and the store result. The managed provider accepts only chains for its own key and writes files atomically with owner-only permissions.

Connection client

  • send_csr(): the key owner owns the chain.
    • csr NULL: the client gets the CSR from the provider's get_csr() (subject = device id). On ISSUED it stores the chain with store_issued_certificate() before the callback; the result is in the new az_iot_csr_event.store_status. az_iot_csr_event gains _internal_size (SDK-stamped, like the state event) so fields can be appended safely. Needs a v2 provider with both hooks, else AZ_IOT_ERR_NOT_SUPPORTED before anything is published.
    • App csr: the chain is delivered, never stored (store_status = AZ_IOT_ERR_NOT_SUPPORTED).
    • The live session is kept. Subsequent connects use the stored chain; if the store fails, the previous credential stays in use.
  • az_iot_operational_cert_callback gains store_result. It fires with the chain even when the store fails; the DPS registration then fails, as before. Breaking: callers must add the parameter. Breaking: az_iot_csr_event gains _internal_size as its first member, which shifts its layout; callers must rebuild.
  • DPS enrollment (dps.request_operational_certificate) requires a provider with get_csr, release_csr and store_issued_certificate; open() returns AZ_IOT_ERR_NOT_SUPPORTED otherwise. The issued chain is always stored in the provider, never kept by the client. Breaking: providers without store_issued_certificate can no longer be used for DPS enrollment.
  • The provider contract now says store_issued_certificate() must be all-or-nothing.
  • The provider vtable has no version field (and no AZ_IOT_CERTIFICATE_PROVIDER_VTABLE_VERSION); optional hooks are gated on NULL only. Breaking: custom providers must drop .version.

Managed provider

  • A store is refused unless every certificate in the chain parses (count must match) and the leaf certifies the operational key. At init, a stored chain that fails either check is ignored.
  • The key and chain are written to a uniquely named, exclusively created file next to the destination (0600; owner-only DACL on Windows), flushed, then renamed over the destination. A failed write leaves the old file untouched. A symlink at the destination is replaced, not followed.
  • The mode of an existing key file is not changed.

Tests: unit tests for each behavior above. The permission and symlink tests are POSIX only. On Windows, a test checks the key and chain DACLs.

Validation (Linux, gcc): unit suites pass, also under ASan/UBSan. Samples and the e2e CSR test compile with Paho. The Windows code path was syntax-checked with MinGW. clang-format 18 and the repo lint scripts pass on changed files. The hub-renewal e2e test has not been run.

Copilot AI added 4 commits September 28, 2026 22:36
- Add opts.use_issued_certificates (default true via options_default()):
  on AZ_IOT_CSR_ISSUED the client stores the chain in the certificate
  provider before the CSR callback, reports it in evt->store_status, and
  keeps the live session. A failed store keeps the previous credential.
- Operational-cert callback (DPS) now always fires with the chain plus
  the store result, including when the store fails.
- Managed provider stores atomically (temp file, validate, replace), so a
  failed store keeps the previous certificate.
- Provider contract: store_issued_certificate() is all-or-nothing.
- Update samples, e2e test, docs; add unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- store_issued_certificate() refuses a chain whose leaf does not certify
  the operational key; init ignores a stored chain that does not.
- Key and chain are written to a uniquely named, exclusively created file
  next to the destination (0600 / owner-only DACL), flushed, then renamed
  over it. A link at the destination is replaced, not followed.
- Tests for each; POSIX-only for permissions and links.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mp-file count

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace opts.use_issued_certificates with ownership from the CSR source:
- send_csr(csr = NULL): the client takes the CSR from the provider's
  get_csr() (subject = device id) and stores the issued chain with
  store_issued_certificate(). Requires a v2 provider with both hooks,
  else AZ_IOT_ERR_NOT_SUPPORTED before anything is published.
- send_csr(csr): the application owns the key; the chain is delivered,
  never stored (store_status AZ_IOT_ERR_NOT_SUPPORTED).

hub_renew sample uses send_csr(NULL). Tests and docs updated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ewertons Ewerton Scaboro da Silva (ewertons) changed the title [C] Store hub-issued certificates; harden managed provider storage [C] Hub renewal stores provider-made certificates; harden managed provider storage Sep 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Public ABI compatibility and incomplete full-chain validation can cause unsafe reads or persist unusable certificate chains.

Review effort: Balanced
Findings: 4 High severity

Open (4)

Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.c Outdated
Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.c Outdated
Comment thread c/inc/azure/iot/az_iot_connection_client.h
Comment thread c/inc/azure/iot/az_iot_connection_client.h
- Managed provider: a stored or persisted chain is used only if every
  certificate parses (not just the leaf) and, on store, the parsed count
  matches the issued count.
- az_iot_csr_event gains _internal_size, stamped by the SDK, so fields
  like store_status can be appended safely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The CSR event change breaks existing callback ABI, and v1 provider access can read beyond its declared vtable.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
Resolved since last review (4)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Make sample provider credential replacement atomic

c/​inc/​azure/​iot/​az_iot_certificate_provider.h:123

This new mandatory contract is not met by the repository's complete sample provider: c/samples/common/sample_cert_provider.c:135-160 opens the destination with "wb" before writing, so a write or close failure returns an error after destroying the previous credential. Update that provider to use atomic replacement too; otherwise the public example teaches an implementation that violates this contract and can strand the next connection.

Low severity Add Windows DACL permission coverage

c/​adapters/​cert_openssl/​az_iot_certificate_provider_managed.c:133

The new owner-only Windows DACL is security-critical behavior, but the added permission tests are excluded under _WIN32; the coverage table also explicitly says this path is untested. Add a Windows unit test that creates both files and inspects their resulting DACLs, verifying access is limited to the owner and SYSTEM rather than relying only on a MinGW syntax check.

Comment thread c/src/core/connection_client.c Outdated
Comment thread c/docs/test-coverage.md Outdated
…age links

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The event layout remains ABI-incompatible, and certificate validation still accepts empty entries and some trailing garbage.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Low severity Align proposed API sketch with documented event fields

c/​docs/​eng/​certificate-management.md:360

This prose now references evt->store_status, but the immediately preceding “Proposed C surface” still defines az_iot_csr_event without either store_status or _internal_size. Update that API sketch so the design document is internally consistent with the public header.

Comment thread c/inc/azure/iot/az_iot_connection_client.h

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The new event layout breaks ABI compatibility, and certificate-storage validation and contract consistency remain incomplete.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)

Comment thread c/inc/azure/iot/az_iot_certificate_provider.h
Comment thread c/docs/eng/certificate-management.md
- sample_cert_provider writes the chain to "<path>.tmp", flushes it, then
  replaces the destination, so a failed store keeps the previous chain.
- certificate-management.md: az_iot_csr_event and send_csr() match the
  header (_internal_size, store_status, csr NULL).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread c/src/core/connection_client.c Outdated
- open() with dps.request_operational_certificate now requires a
  provider with get_csr, release_csr and store_issued_certificate. The
  issued chain is always stored in the provider (failure fails the
  registration); the callback-only path is removed.
- send_csr() docs list the exact hooks behind AZ_IOT_ERR_NOT_SUPPORTED.
- Sample provider writes through a uniquely named, exclusively created
  temp file (0600 on POSIX) instead of a fixed "<path>.tmp".
- Windows unit test checks the key and chain DACLs (protected; Owner
  Rights and SYSTEM only).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Persisted-chain validation can consume unbounded memory during initialization, and the documented existing-key permission behavior lacks regression coverage.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (2)

Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.c
Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.h
- Remove az_iot_certificate_provider_vtable::version and
  AZ_IOT_CERTIFICATE_PROVIDER_VTABLE_VERSION; the client gates optional
  hooks on NULL only. Providers, samples, tests and docs updated; the
  v1-provider tests are removed.
- Windows DACL test: rename a local that shadowed system() (MSVC C4459
  under /W4 /WX) and initialize the ACL size info.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The sample provider can replace a valid credential with an incomplete chain while reporting success.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)

Comment thread c/samples/common/sample_cert_provider.c
- Managed provider: a persisted chain over 64 KiB is refused at init
  rather than buffered.
- Sample provider: an empty issued entry fails the store, like the
  managed provider, so the previous chain is kept.
- Tests for both, and for init keeping an existing key file's mode.
- Restore AZ_IOT_HUB_PROFILE_INIT's formatting to main's.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Managed storage can accept a chain that its restart path subsequently rejects due to the 64 KiB limit.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (2)

Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.c Outdated
Copilot AI added 2 commits September 29, 2026 04:53
A chain that init() would not read back is refused at store, so a
successful store is always usable after a restart.
One test checks trailing whitespace is accepted; the other checks a chain
over 64 KiB is not used. The combined test did not build with MSVC.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The managed store can allocate an arbitrarily oversized chain before enforcing its 64 KiB limit.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
Resolved since last review (1)

Comment thread c/adapters/cert_openssl/az_iot_certificate_provider_managed.c
An entry that cannot fit in what is left of the 64 KiB chain limit is
refused before it is decoded or buffered.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Malformed or truncated Hub responses can still trigger automatic certificate persistence.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
Resolved since last review (1)

Comment thread c/src/core/connection_client.c
A truncated, unbalanced or trailing-text credentials response (hub) or
ASSIGNED payload (DPS) no longer reaches the provider: the hub renewal
fails with AZ_IOT_ERR_PROTOCOL and the DPS registration fails. The chain
array must also close.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Security-sensitive cross-platform persistence and the unrun Hub-renewal end-to-end path require final human validation.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Ewerton Scaboro da Silva (ewertons) added a commit that referenced this pull request Sep 30, 2026
`c/docs/` now holds customer documentation only; engineering docs are in
`c/docs/eng/`.

**Customer docs (`c/docs/`)**
- New: `architecture.md` (layers, mqttv3/mqttv5, feature availability,
adapters), `connecting.md` (states, provisioning, reconnection,
proxy/WebSockets, certificates, common results).
- Kept: `how_to_byo_mqtt_client.md` (conformance coverage now accurate),
`struct_versioning.md`.
- Deleted: `TODO.md`, `devnotes.md`, `dps-integration.md`, `design.md`
(replaced by `architecture.md`), `telemetry_api_comparison.html`.

**Engineering docs (`c/docs/eng/`)**
- Moved in: `connection.md`, `test-coverage.md`.
- `software-updates.md` = `su-client-design.md` + `su-spec.md` (device
contract, §2). Removed implementation-phase history and descriptions of
unimplemented items; gaps listed in §11.
- `connection-c.md` absorbs the C gaps from `connection-impl-status.md`
(§11).
- Rewritten as-built: `connection-state-and-error-propagation.md`,
`code-coverage.md`.
- Curated: `certificate-management.md`, `end-to-end-tests.md`.
- Deleted: `client-separation.md`, `split-client.md`,
`connection-impl-status.md`.

**Other**
- Doc references in code comments, `check-layering.sh` and `.gitignore`
updated; comment-only changes in C sources.
- Relative links and anchors under `c/`: 0 broken (30 on `main`).

**Validation**
- `linux-gcc-debug` with unit tests: build clean (`-Werror`); 39/40
suites pass, the remaining one needs an MQTT broker.
- `code-style.sh check` (clang-format 18.1.8),
`check-banned-constructs.sh`, `check-layering.sh`: pass.

Open PRs touching these docs (#265, #278, #287, #288) will need a
rebase.

---------

Co-authored-by: Ewerton Scaboro da Silva <12401690+ewertons@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ewertons

Copy link
Copy Markdown
Contributor Author

Superseded by smaller PRs: #410 (managed provider storage) and #411 (renewal CSR from the provider, chains always stored). The rest follows in separate PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants