Skip to content

[C] Renewal CSR from the certificate provider; issued chains always stored - #411

Open
Ewerton Scaboro da Silva (ewertons) wants to merge 8 commits into
mainfrom
feature/c-renewal-from-provider
Open

Ewerton Scaboro da Silva (ewertons) wants to merge 8 commits into
mainfrom
feature/c-renewal-from-provider

Conversation

@ewertons

@ewertons Ewerton Scaboro da Silva (ewertons) commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The certificate provider is the only source of CSRs, and every issued chain is stored in it.

Hub renewal (az_iot_connection_client_send_csr)

  • Breaking: the csr parameter is removed. The client gets the CSR from the provider's get_csr() (subject: device id) and releases it once the request is built.
  • The provider needs get_csr, release_csr and store_issued_certificate; otherwise AZ_IOT_ERR_NOT_SUPPORTED before anything is published. A get_csr() error is returned as is.
  • On 200, the chain is stored with store_issued_certificate() before the callback. Breaking: az_iot_csr_event gains store_status.
  • A failed store keeps the live session; the next connect uses the previous credential.

DPS enrollment (dps.request_operational_certificate)

  • Breaking: requires the same three hooks (open() returns AZ_IOT_ERR_NOT_SUPPORTED; also checked when a DPS session is started without open()). The chain is always stored; there is no callback-only path.
  • Breaking: az_iot_operational_cert_callback gains store_result. It fires with the chain even when the store fails; the registration then fails.

Both

  • A truncated, unbalanced or trailing-text response (hub 200, DPS ASSIGNED) is never stored: hub renewal fails with AZ_IOT_ERR_PROTOCOL, the DPS registration fails. A chain array must close.
  • Provider contract: store_issued_certificate() must be all-or-nothing.
  • A credentials response is matched only for a 3-digit status followed by /? with $rid as one of the properties.
  • The sample provider (samples/common/sample_cert_provider.c) stores all-or-nothing: unique exclusive temporary file, then rename; an empty entry is refused.

Samples (hub_renew, dps_csr_managed, dps_sas_key_issued_cert, custom_certificate_provider), the e2e CSR test and the docs are updated.

Tests: unit tests for each behavior.

Validation (Linux, gcc): all unit suites pass, also under ASan/UBSan; e2e suite compiles; clang-tidy 18.1.8 clean on changed sources and samples; clang-format 18 and repo lint scripts pass; MinGW syntax check of changed C files. The hub-renewal and DPS e2e tests were not run.

…tored

- send_csr() drops its csr parameter: the CSR comes from the provider's
  get_csr() (subject: device id). The provider needs get_csr, release_csr
  and store_issued_certificate, else AZ_IOT_ERR_NOT_SUPPORTED before
  anything is published.
- On a 200, the chain is stored with store_issued_certificate() before
  the callback; az_iot_csr_event gains store_status. A failed store keeps
  the session and the previous credential.
- DPS enrollment requires the same three hooks; the chain is always
  stored, and az_iot_operational_cert_callback gains store_result. A
  failed store fails the registration.
- A truncated or malformed hub 200 or DPS ASSIGNED payload is never
  stored; a chain array must close.
- store_issued_certificate() must be all-or-nothing.

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.

🟡 Changes recommended

Bundled providers violate the new atomic-storage guarantee, and malformed prefix-matched topics can trigger certificate persistence.

3 open findings
What changed in this PR

Centralizes CSR generation and issued-certificate persistence in certificate providers for DPS enrollment and Hub renewal.

Changes:

  • Requires provider CSR, release, and certificate-storage hooks.
  • Reports storage results through callbacks.
  • Adds strict JSON validation, tests, samples, and documentation.
File Description
c/​tests/​unit/​connection_client_test.c Expands CSR and storage tests.
c/​tests/​e2e/​tests/​e2e_csr_test.c Handles storage results in E2E coverage.
c/​src/​core/​internal/​cert_util.h Declares JSON completeness validation.
c/​src/​core/​connection_client.c Implements provider-owned CSR and storage flows.
c/​src/​core/​cert_util.c Validates complete JSON and closed chains.
c/​samples/​authentication/​hub_renew/​README.md Updates renewal workflow documentation.
c/​samples/​authentication/​hub_renew/​main.c Uses SDK-managed CSR storage.
c/​samples/​authentication/​dps_sas_key_issued_cert/​main.c Reports storage outcomes.
c/​samples/​authentication/​dps_csr_managed/​main.c Handles failed certificate storage.
c/​samples/​authentication/​custom_certificate_provider/​main.c Updates callback signature.
c/​inc/​azure/​iot/​az_iot_connection_client.h Revises public CSR APIs and callbacks.
c/​inc/​azure/​iot/​az_iot_certificate_provider.h Adds atomic storage contract.
c/​docs/​eng/​test-coverage.md Records added test coverage.
c/​docs/​eng/​certificate-management.md Documents provider-owned certificate lifecycle.
c/​docs/​connecting.md Updates operational-certificate guidance.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread c/inc/azure/iot/az_iot_certificate_provider.h
Comment thread c/src/core/connection_client.c
Comment thread c/inc/azure/iot/az_iot_connection_client.h Outdated
Copilot AI balanced review requested due to automatic review settings October 11, 2026 07:29
…back docs

- A credentials response is matched only for a 3-digit status followed by
  '/?' with $rid as one of the properties.
- The sample provider stores a chain all-or-nothing: unique exclusive temp
  file, then rename; an empty entry is refused.
- The operational-cert callback fires after the store attempt.

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.

🔵 Needs a closer look

Bundled providers violate the new atomic-storage guarantee, and malformed DPS responses can leave a stale connection profile for retries.

1 open finding
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Reset cached profile after rejected assignment payload

c/​src/​core/​connection_client.c:2989

This completeness check runs after the ASSIGNED handler has already called dps_read_connection_profile(), which mutates c->connection_profile. If a trailing-text response carries (for example) mqttV5, this check fails the registration and schedules a retry, but the next valid assignment with no profile (which should default to classic) does not reset that field, so the client can connect using the profile from the rejected payload. Validate the full payload before reading/caching any assignment fields, or reset the profile at the start of each assignment attempt.

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 11, 2026 07:32

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.

🟡 Changes recommended

The sample provider is rejected due to its missing vtable version, and malformed query separators can hang MQTT processing.

2 open findings
1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Log effective error status when CSR output is missing

c/​src/​core/​connection_client.c:9095

When get_csr() returns AZ_IOT_OK but leaves csr_base64 null, this logs the failure as AZ_IOT_OK even though the function correctly returns AZ_IOT_ERR_INTERNAL. Log the effective returned status so provider contract violations are diagnosable.

🧠 Review effort: Balanced

Comment thread c/src/core/connection_client.c Outdated
Comment thread c/samples/common/sample_cert_provider.c
…ment fields

A rejected payload no longer leaves its hub or connection profile behind
for the next registration.
@ewertons

Copy link
Copy Markdown
Contributor Author

Re the previously-missed finding at connection_client.c:2989 (review on 5fab8b6): fixed in 18cf577. With CSR enrollment, the ASSIGNED payload is checked for completeness before any assignment field (hub, device id, connection profile) is read or cached. Test: dps_a_rejected_assignment_leaves_the_profile_alone.

Copilot AI balanced review requested due to automatic review settings October 11, 2026 07:38

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.

🔵 Needs a closer look

The sample provider omits its required vtable version, causing the custom-provider enrollment sample to fail as unsupported.

2 open findings

🧠 Review effort: Balanced

… the rid search

- The sample certificate provider sets its vtable version again; without
  it, CSR enrollment refused the provider.
- The $rid search advances past each separator in one place.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 07:43

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.

🟡 Changes recommended

The DPS failure callback is not cancellation-safe, and several tests do not assert the DPS lifecycle they claim to verify.

1 open finding
2 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Assert DPS lifecycle faults when registration fails

c/​tests/​unit/​connection_client_test.c:1556

This assertion checks the HUB lifecycle, which is already IDLE before provisioning starts, so it does not prove the stated “registration fails” behavior. With retries disabled, the failed DPS store should settle the DPS lifecycle in AZ_IOT_CONN_STATE_FAULTED; assert that scope/state so a regression that leaves provisioning connected or pending is caught.

This issue also appears on line 1578 of the same file.

Low severity Align ownership documentation and architecture diagrams

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

The revised ownership decision still contradicts item 4 earlier in this same document (c/docs/eng/certificate-management.md:264-266), which says the app callback is needed when the app owns persistence and is decoupled from provider storage. Under the new required-provider model the callback only observes the provider's storage result. Update that item (and the corresponding architecture diagrams that still show a missing chain continuing with bootstrap credentials) so the documented model is consistent.

Low severity Add failure-path tests for atomic certificate replacement

c/​samples/​common/​sample_cert_provider.c:47

This new cross-platform file-creation helper and the all-or-nothing replacement path have no automated coverage under c/tests (the only test reference to sample_cert_provider is absent). Add tests for successful replacement, an empty entry, write/close/rename failure preserving the previous file, and temporary-file cleanup; the C-library convention requires boundary/failure tests for new helpers.

🧠 Review effort: Balanced

Comment thread c/src/core/connection_client.c
…orage; tests check DPS state

- The operational-cert callback runs inside message processing; it must
  not call close() or deinit().
- Design doc and diagrams: the provider always stores the chain; a missing
  chain, malformed payload or failed store fails the registration.
- DPS store-failure and truncated-payload tests assert DPS ends FAULTED.
Copilot AI balanced review requested due to automatic review settings October 11, 2026 07:58
@ewertons

Copy link
Copy Markdown
Contributor Author

Re the previously-missed findings (review on 7b82fae), fixed in ff45a8b:

  • connection_client_test.c:1556/1578: the tests now assert DPS ends FAULTED.
  • certificate-management.md item 4 and the connection.md/connection-c.md diagrams: the callback only observes; the provider always stores; a missing chain, malformed payload or failed store fails the registration.
  • sample_cert_provider.c tests: not added. Samples have no unit-test harness; the same atomic-write logic is tested in the managed provider ([C] Managed provider: validate stored chains; atomic owner-only writes #410). Checked by running the custom_certificate_provider sample.

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.

🔵 Needs a closer look

It introduces broad breaking certificate-lifecycle changes, while the affected hub-renewal and DPS end-to-end paths were not executed.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

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.

2 participants