Skip to content

Chapter 6: Hub Onboarding & Fleet Provisioning

In Chapter 5, we saw how the MQTT Proxy gives sub-devices a shared connection to AWS IoT Core. But there's a prerequisite we skipped over: the hub itself needs to be registered with IoT Core before it can connect at all. That's what Hub Onboarding handles — it's the very first thing that runs when a hub powers on for the first time.

This chapter walks through how the SDK proves the hub's identity to AWS, obtains permanent credentials, and transitions from "unknown device" to "registered thing."

Why Does the Hub Need Onboarding?

A brand-new hub has no identity in AWS IoT Core. Without onboarding:

  • It can't establish an MQTT connection (no valid certificate).
  • It can't receive commands or send telemetry.
  • It can't proxy connections for sub-devices.

Onboarding solves this by exchanging temporary credentials for permanent ones and registering the hub as a "managed thing" in IoT Core.

Two Paths: Fleet Provisioning vs. JITR

The SDK supports two provisioning methods, configured in iotmi_config.json:

Method How It Works Certificate Outcome
Fleet Provisioning (FP) Uses a shared claim certificate to request a unique device certificate from IoT Core New cert + key generated by AWS
JITR (Just-In-Time Registration) Uses a pre-installed DHA certificate signed by a registered CA Existing DHA cert becomes the permanent cert

The method is read from config during initialization:

// hub_onboarding.cpp — InitProvisioner()
provisioning_method_ =
    config->GetStringValue(IOT_PROVISIONING_METHOD_CONF);
// Falls back to "JITR" if missing or invalid

📁 provisioning/IoTSmartHomeDevice-HubOnboarding/include/hub_onboarding.hpp

The Key Players

Three classes collaborate to make onboarding work:

Class File Role
HubOnboarding hub_onboarding.hpp Orchestrator — reads config, drives the flow
DeviceProvisioner device_provisioner.hpp Adapter — translates between HubOnboarding and the provisioning library
IotProvisioning Provisioning.hpp Engine — manages MQTT connections and delegates to FleetProvisioning

There's also FleetProvisioning (in FleetProvisioning.hpp), which handles the low-level MQTT pub/sub for the FP protocol. Let's see how they fit together.

The Fleet Provisioning Flow

Here's what happens when a hub provisions via FP — the more complex of the two paths:

sequenceDiagram
    participant Hub as HubOnboarding
    participant DP as DeviceProvisioner
    participant FP as FleetProvisioning
    participant MQTT as MQTT5 Client
    participant AWS as AWS IoT Core

    Hub->>DP: ProvisionFP(claimCert, params)
    DP->>MQTT: Connect with claim certificate
    MQTT->>AWS: TLS handshake (mTLS)
    FP->>AWS: CreateKeysAndCertificate
    AWS-->>FP: New cert + key + ownershipToken
    FP->>AWS: RegisterThing(template, token)
    AWS-->>FP: DeviceConfiguration (clientId, managedThingId)

ProcessDevice(): The Lifecycle Entry Point

ProcessDevice() is the single method that kicks off the entire onboarding flow. It branches based on the provisioning method:

// hub_onboarding.cpp — ProcessDevice()
void HubOnboarding::ProcessDevice() {
  if (provisioning_method_ == JITR) {
    // ... JITR path
  } else {
    // ... Fleet Provisioning path
  }
  provisioning_state_ = PROVISIONED;
  UpdateHubState(PROVISIONED);
}

📁 provisioning/IoTSmartHomeDevice-HubOnboarding/src/hub_onboarding.cpp

Fleet Provisioning Path (Step by Step)

1. Read claim credentials — The hub loads its claim certificate and private key using the CertHandler from Chapter 1:

// hub_onboarding.cpp — FP branch
cert_handler->read_cert_and_private_key(
    CERT_TYPE_T::CLAIM,
    claim_cert_value, claim_pk_value);

2. Build provisioning parameters — Serial number and UPC identify this specific hub:

fleet_provisioning_params.emplace(
    DEVICE_SERIAL_NUMBER_KEY, serial_num);
fleet_provisioning_params.emplace(
    UNIVERSAL_PRODUCT_CODE_KEY, upc);

3. Call DeviceProvisioner — This connects to IoT Core with the claim cert and runs the FP protocol:

device_provisioner_->ProvisionFP(
    client_id, claim_cert_value, claim_pk_value,
    fp_template_name, fleet_provisioning_params,
    device_configuration, permanent_cert, permanent_key);

📁 provisioning/IoTSmartHomeDevice-HubOnboarding/include/device_provisioner.hpp

4. Extract device configuration — IoT Core returns a clientId and managedThingId in the response:

final_client_id =
    device_configuration.find(
        DEVICE_CONFIGURATION_CLIENT_ID)->second;
managed_thing_id =
    device_configuration.find(
        DEVICE_CONFIGURATION_MANAGED_THING_ID)->second;

5. Store permanent credentials — The new certificate and key are written to disk via CertHandler:

cert_handler->write_permanent_cert_and_private_key(
    permanent_cert, permanent_key,
    permanent_cert_path, permanent_pk_path);

6. Update config — Certificate paths, client ID, managed thing ID, and provisioning state are all persisted:

UpdateConfigWithCertificatePaths();
UpdateConfigWithClientId(final_client_id);
UpdateConfigWithManagedThingId(managed_thing_id);
UpdateHubState(PROVISIONED);

JITR Path (Simpler)

JITR is more straightforward — the DHA certificate is already valid, so the hub just needs to connect once to trigger registration:

// hub_onboarding.cpp — JITR branch
device_provisioner_->ProvisionJITR(
    client_id, dha_cert_value, dha_pk_value);
final_cert_path_ = dha_cert_path_;
final_pk_path_ = dha_pk_path_;

Under the hood, ProvisionWithJitr() simply connects via MQTT. The first connection attempt may fail (IoT Core needs time to register the cert), but the retry logic handles this:

// Provisioning.cpp — ProvisionWithJitr()
// First connection may fail; retries handle
// the registration delay automatically.
mqtt5ClientHandler->Connect(mqttRetryTimeoutSeconds);

📁 provisioning/IoTSmartHomeDevice-IoTProvisioningLib/src/Provisioning.cpp

Inside FleetProvisioning: The Two-Phase Protocol

The FleetProvisioning class implements the AWS IoT Identity protocol using MQTT pub/sub. It runs two phases:

Phase 1: CreateCertificateAndKey

The hub subscribes to accepted/rejected topics, then publishes a request:

// FleetProvisioning.cpp
identityClient->SubscribeToCreateKeysAndCertificateAccepted(
    keySubRequest, QOS_AT_LEAST_ONCE,
    onKeysAccepted, onKeysAcceptedSubAck);
identityClient->PublishCreateKeysAndCertificate(
    createRequest, QOS_AT_LEAST_ONCE, onPublishAck);

On success, the callback captures the new certificate, private key, and an ownershipToken needed for Phase 2.

Phase 2: RegisterThing

Using the ownership token from Phase 1, the hub registers itself with a provisioning template:

// FleetProvisioning.cpp
RegisterThingRequest registerRequest;
registerRequest.TemplateName = fp_template_name;
registerRequest.CertificateOwnershipToken =
    certificateOwnershipToken;
registerRequest.Parameters = fleet_provisioning_params;
identityClient->PublishRegisterThing(
    registerRequest, QOS_AT_LEAST_ONCE, onPublishAck);

The response includes a DeviceConfiguration map containing the hub's clientId and managedThingId.

📁 provisioning/IoTSmartHomeDevice-IoTProvisioningLib/include/FleetProvisioning.hpp

The IsProvisioned() Guard

Before running ProcessDevice(), callers check whether onboarding already happened:

// hub_onboarding.cpp
bool HubOnboarding::IsProvisioned() {
  if (provisioning_state_ == PROVISIONED) {
    final_cert_path_ = config->GetStringValue(
        PERMANENT_CERT_PATH_CONF);
    return true;
  }
  return false;
}

This prevents re-provisioning on every reboot. The state is persisted in config, so it survives power cycles.

Error Handling

The provisioning library uses typed status codes to distinguish failure modes:

Status Code Meaning
FP_STATUS_OK Provisioning succeeded
FP_STATUS_MQTT_CONNECTION_ERROR Couldn't connect to IoT Core
FP_STATUS_IDENTITY_ERROR Certificate or registration rejected
FP_STATUS_SUBSCRIBE_TIMEOUT_ERROR MQTT subscription timed out
FP_STATUS_PUBLISH_TIMEOUT_ERROR MQTT publish timed out

DeviceProvisioner translates these into exceptions with descriptive messages:

// device_provisioner.cpp
if (fp_status == FP_STATUS_MQTT_CONNECTION_ERROR)
  throw std::runtime_error(
      "Failed due to MQTT Connection error");

📁 provisioning/IoTSmartHomeDevice-IoTProvisioningLib/include/Provisioning.hpp

Putting It All Together

Here's the complete onboarding lifecycle from power-on to "ready to connect":

  1. HubOnboarding constructor reads config → determines FP or JITR.
  2. IsProvisioned() checks if we already have permanent certs → skips if yes.
  3. ProcessDevice() runs the chosen provisioning path.
  4. Permanent credentials are stored via CertHandler (Chapter 1).
  5. Config is updated with clientId, managedThingId, and cert paths.
  6. State transitions to PROVISIONED.
  7. The MQTT Proxy (Chapter 5) can now connect using the permanent credentials.

Key Takeaways

  • Hub Onboarding is a one-time operation — once provisioned, the hub reuses its permanent credentials across reboots.
  • Fleet Provisioning is a two-phase MQTT protocol: create a certificate, then register the thing.
  • JITR is simpler — just connect with a pre-installed DHA cert and let IoT Core auto-register.
  • ProcessDevice() is the single entry point that handles both paths and updates all config state.
  • CertHandler from Chapter 1 handles all certificate I/O, whether reading claim certs or writing permanent ones.

What's Next?

The hub is now registered and has permanent credentials. But how does it discover and manage the sub-devices connected to it? In Chapter 7, we'll explore the Cloud Device Management Bridge (CDMB) — the component that synchronizes the hub's local device inventory with the cloud.