AWS IoT Fleet Provisioning: The Complete Story

Before reading this, understand CSR, TLS Handshake, and Claim vs Device Credentials.

Fleet Provisioning is like a fully automated employee onboarding system — devices go from visitor pass to full employee badge without human intervention.


The Complete Flow Overview

sequenceDiagram
    participant Factory as Factory
    participant Device as ESP32 Device
    participant AWS as AWS IoT Core
    participant Thing as Thing Registry
    
    Factory->>Device: 1. Install claim credentials
    Device->>Device: 2. First boot - generate device keys
    Device->>Device: 3. Create CSR with device public key
    Device->>AWS: 4. TLS connect with claim credentials
    Device->>AWS: 5. MQTT: CreateCertificateFromCsr
    AWS->>AWS: 6. Verify claim signature
    AWS->>AWS: 7. Generate device certificate
    AWS->>Device: 8. MQTT: Return certificate + token
    Device->>AWS: 9. MQTT: RegisterThing with token
    AWS->>Thing: 10. Create Thing in registry
    AWS->>Device: 11. MQTT: Return Thing name
    Device->>Device: 12. Store device certificate
    Device->>Device: 13. Switch to device credentials
    Note over Device,AWS: Ready for production use

Step-by-Step Breakdown

Step 1: Factory Preparation

// Same credentials installed on ALL devices
typedef struct {
    const char* claim_cert_path;        // "/spiffs/claim-certificate.pem"
    const char* claim_key_path;         // "/spiffs/claim-private-key.pem"
    const char* template_name;          // "SmartLockTemplate"
    const char* device_serial;          // "SL001234567" (unique per device)
} factory_config_t;

esp_err_t factory_install_credentials(factory_config_t* config) {
    // Install same claim cert on thousands of devices
    copy_file_to_spiffs(CLAIM_CERT_SOURCE, config->claim_cert_path);
    copy_file_to_spiffs(CLAIM_KEY_SOURCE, config->claim_key_path);
    
    // But unique serial number per device
    write_device_serial(config->device_serial);
    
    return ESP_OK;
}

Step 2: Device Key Generation

esp_err_t generate_device_keypair() {
    CK_SESSION_HANDLE session;
    CK_OBJECT_HANDLE private_key, public_key;
    
    // Generate P-256 ECDSA key pair in secure storage
    CK_ATTRIBUTE public_template[] = {
        {CKA_CLASS, &public_key_class, sizeof(public_key_class)},
        {CKA_KEY_TYPE, &ecdsa_type, sizeof(ecdsa_type)},
        {CKA_LABEL, DEVICE_PUBLIC_KEY_LABEL, strlen(DEVICE_PUBLIC_KEY_LABEL)},
        {CKA_VERIFY, &true_val, sizeof(true_val)}
    };
    
    CK_ATTRIBUTE private_template[] = {
        {CKA_CLASS, &private_key_class, sizeof(private_key_class)},
        {CKA_KEY_TYPE, &ecdsa_type, sizeof(ecdsa_type)},
        {CKA_LABEL, DEVICE_PRIVATE_KEY_LABEL, strlen(DEVICE_PRIVATE_KEY_LABEL)},
        {CKA_SIGN, &true_val, sizeof(true_val)},
        {CKA_PRIVATE, &true_val, sizeof(true_val)}
    };
    
    CK_MECHANISM mechanism = {CKM_EC_KEY_PAIR_GEN, ec_params, sizeof(ec_params)};
    
    return C_GenerateKeyPair(session, &mechanism,
                            public_template, 4,
                            private_template, 5,
                            &public_key, &private_key);
}

Step 3: CSR Creation

esp_err_t create_device_csr(char* csr_buffer, size_t buffer_size, size_t* csr_length) {
    // Subject information for the CSR
    mbedtls_x509write_csr csr_ctx;
    mbedtls_x509write_csr_init(&csr_ctx);
    
    char subject_name[256];
    snprintf(subject_name, sizeof(subject_name),
             "CN=%s,O=CongruentTech,C=US", get_device_serial());
    
    // Set CSR subject
    mbedtls_x509write_csr_set_subject_name(&csr_ctx, subject_name);
    
    // Load device key from PKCS#11
    mbedtls_pk_context device_key;
    load_pkcs11_key(&device_key, DEVICE_PRIVATE_KEY_LABEL);
    mbedtls_x509write_csr_set_key(&csr_ctx, &device_key);
    
    // Set signature algorithm
    mbedtls_x509write_csr_set_md_alg(&csr_ctx, MBEDTLS_MD_SHA256);
    
    // Generate CSR in PEM format
    return mbedtls_x509write_csr_pem(&csr_ctx, 
                                    (uint8_t*)csr_buffer, 
                                    buffer_size, 
                                    mbedtls_ctr_drbg_random, &ctr_drbg);
}

MQTT API Calls (CBOR Format)

CreateCertificateFromCsr Request

esp_err_t send_create_certificate_request(const char* csr) {
    // CBOR message to AWS IoT
    uint8_t cbor_buffer[2048];
    size_t cbor_length;
    
    // Create CBOR payload: {"certificateSigningRequest": "<CSR>"}
    CborEncoder encoder, mapEncoder;
    cbor_encoder_init(&encoder, cbor_buffer, sizeof(cbor_buffer), 0);
    cbor_encoder_create_map(&encoder, &mapEncoder, 1);
    cbor_encode_text_stringz(&mapEncoder, "certificateSigningRequest");
    cbor_encode_text_string(&mapEncoder, csr, strlen(csr));
    cbor_encoder_close_container(&encoder, &mapEncoder);
    
    cbor_length = cbor_encoder_get_buffer_size(&encoder, cbor_buffer);
    
    // Publish to AWS IoT Fleet Provisioning topic
    return esp_mqtt_client_publish(mqtt_client,
                                  "$aws/certificates/create/cbor",
                                  (char*)cbor_buffer, cbor_length, 1, 0);
}

CreateCertificateFromCsr Response

void on_certificate_response(const char* topic, const uint8_t* payload, size_t length) {
    // Parse CBOR response from AWS
    CborParser parser;
    CborValue map;
    cbor_parser_init(payload, length, 0, &parser, &map);
    
    // Extract certificate, certificateId, and certificateOwnershipToken
    char certificate[2048];
    char certificate_id[64];  
    char ownership_token[512];
    
    extract_cbor_field(&map, "certificatePem", certificate, sizeof(certificate));
    extract_cbor_field(&map, "certificateId", certificate_id, sizeof(certificate_id));
    extract_cbor_field(&map, "certificateOwnershipToken", ownership_token, sizeof(ownership_token));
    
    // Store certificate in PKCS#11
    pkcs11_store_certificate(certificate, DEVICE_CERTIFICATE_LABEL);
    
    // Use ownership token for Thing registration
    register_thing_with_token(ownership_token);
}

RegisterThing Request

esp_err_t register_thing(const char* ownership_token) {
    // CBOR message for Thing creation
    uint8_t cbor_buffer[1024];
    size_t cbor_length;
    
    CborEncoder encoder, mapEncoder, paramEncoder;
    cbor_encoder_init(&encoder, cbor_buffer, sizeof(cbor_buffer), 0);
    cbor_encoder_create_map(&encoder, &mapEncoder, 2);
    
    // Add ownership token
    cbor_encode_text_stringz(&mapEncoder, "certificateOwnershipToken");
    cbor_encode_text_string(&mapEncoder, ownership_token, strlen(ownership_token));
    
    // Add parameters
    cbor_encode_text_stringz(&mapEncoder, "parameters");
    cbor_encoder_create_map(&mapEncoder, &paramEncoder, 1);
    cbor_encode_text_stringz(&paramEncoder, "SerialNumber");
    cbor_encode_text_stringz(&paramEncoder, get_device_serial());
    cbor_encoder_close_container(&mapEncoder, &paramEncoder);
    
    cbor_encoder_close_container(&encoder, &mapEncoder);
    cbor_length = cbor_encoder_get_buffer_size(&encoder, cbor_buffer);
    
    // Publish to Thing registration topic
    return esp_mqtt_client_publish(mqtt_client,
                                  "$aws/provisioning-templates/SmartLockTemplate/provision/cbor",
                                  (char*)cbor_buffer, cbor_length, 1, 0);
}

Real CBOR Message Examples

CreateCertificateFromCsr CBOR (hex)

A1                          # map(1)
   78 1A                    # text(26)
      636572746966696361746553696E696E6752657175657374  # "certificateSigningRequest"
   79 05 9C                 # text(1436) 
      2D2D2D2D2D424547494E20434552544946494341544520524551554553542D2D2D2D2D0A...
      # "-----BEGIN CERTIFICATE REQUEST-----\nMIIBVTCBvwIBADBTMQsw..."

RegisterThing CBOR (hex)

A2                          # map(2)
   78 1A                    # text(26)
      636572746966696361746553696E696E6752657175657374  # "certificateOwnershipToken" 
   78 80                    # text(128)
      61626364656667686969...  # ownership token value
   6A                       # text(10)  
      706172616D6574657273    # "parameters"
   A1                       # map(1)
      6C                    # text(12)
         53657269616C4E756D626572  # "SerialNumber"
      6B                    # text(11)
         534C303031323334353637     # "SL001234567"

AWS IoT Provisioning Template

{
  "templateName": "SmartLockTemplate",
  "description": "Template for smart lock devices",
  "enabled": true,
  "provisioningRoleArn": "arn:aws:iam::123456789:role/IoTProvisioningRole",
  "templateBody": {
    "Parameters": {
      "SerialNumber": {
        "Type": "String"
      }
    },
    "Resources": {
      "thing": {
        "Type": "AWS::IoT::Thing",
        "Properties": {
          "thingName": {"Fn::Join": ["-", ["SmartLock", {"Ref": "SerialNumber"}]]},
          "thingTypeName": "SmartLock",
          "attributePayload": {
            "SerialNumber": {"Ref": "SerialNumber"},
            "DeviceType": "SmartLock"
          }
        }
      },
      "certificate": {
        "Type": "AWS::IoT::Certificate",
        "Properties": {
          "certificatePem": {"Fn::GetAtt": ["CSR", "certificatePem"]},
          "status": "ACTIVE"
        }
      },
      "policy": {
        "Type": "AWS::IoT::Policy",
        "Properties": {
          "policyName": "SmartLockPolicy"
        }
      }
    }
  }
}

Memory and Timing Analysis

ESP32 Memory Usage During Provisioning

typedef struct {
    size_t claim_credentials;      // ~2.3KB (cert + key)
    size_t device_keypair;         // ~2.3KB (generated pair)
    size_t csr_buffer;            // ~1.5KB (temporary)
    size_t cbor_buffers;          // ~3KB (request/response)
    size_t tls_session;           // ~45KB (during handshake)
    size_t mqtt_client;           // ~8KB (persistent)
} provisioning_memory_t;

// Peak usage: ~62KB
// Steady state after provisioning: ~10KB

Timing Analysis

typedef struct {
    uint32_t key_generation_ms;    // 200-500ms (ECDSA P-256)
    uint32_t csr_creation_ms;      // 50-150ms
    uint32_t tls_handshake_ms;     // 800-2000ms
    uint32_t mqtt_connect_ms;      // 200-500ms
    uint32_t cert_request_ms;      // 1000-3000ms (network)
    uint32_t thing_register_ms;    // 500-1500ms (network)
} provisioning_timing_t;

// Total provisioning time: 2.75-7.65 seconds

Error Handling

Common Failure Points

esp_err_t handle_provisioning_errors(provisioning_error_t error) {
    switch (error) {
        case PROVISIONING_CLAIM_CERT_EXPIRED:
            ESP_LOGE(TAG, "Claim certificate expired - update factory image");
            return ESP_FAIL;
            
        case PROVISIONING_TEMPLATE_NOT_FOUND:
            ESP_LOGE(TAG, "Template 'SmartLockTemplate' not found in AWS");
            return ESP_FAIL;
            
        case PROVISIONING_CLAIM_NOT_AUTHORIZED:
            ESP_LOGE(TAG, "Claim certificate not registered with template");
            return ESP_FAIL;
            
        case PROVISIONING_NETWORK_TIMEOUT:
            ESP_LOGW(TAG, "Network timeout - retrying in 30 seconds");
            vTaskDelay(30000 / portTICK_PERIOD_MS);
            return ESP_ERR_TIMEOUT;
            
        case PROVISIONING_DUPLICATE_SERIAL:
            ESP_LOGE(TAG, "Device serial already registered");
            return ESP_ERR_DUPLICATE_OBJECT;
    }
    
    return ESP_OK;
}

Retry Logic

esp_err_t provision_with_retry() {
    int max_retries = 5;
    int retry_delay_ms = 1000;
    
    for (int attempt = 1; attempt <= max_retries; attempt++) {
        ESP_LOGI(TAG, "Provisioning attempt %d/%d", attempt, max_retries);
        
        esp_err_t ret = device_provisioning_start();
        
        if (ret == ESP_OK) {
            ESP_LOGI(TAG, "Provisioning successful on attempt %d", attempt);
            return ESP_OK;
        }
        
        if (ret == ESP_ERR_TIMEOUT && attempt < max_retries) {
            ESP_LOGW(TAG, "Attempt %d failed, retrying in %d seconds", 
                     attempt, retry_delay_ms / 1000);
            vTaskDelay(retry_delay_ms / portTICK_PERIOD_MS);
            retry_delay_ms *= 2;  // Exponential backoff
        } else {
            ESP_LOGE(TAG, "Provisioning failed permanently: %s", 
                     esp_err_to_name(ret));
            return ret;
        }
    }
    
    return ESP_FAIL;
}

Complete ESP32 Implementation

esp_err_t fleet_provisioning_complete_flow() {
    esp_err_t ret;
    
    // Step 1: Load claim credentials from SPIFFS
    ret = load_claim_credentials_from_spiffs();
    if (ret != ESP_OK) return ret;
    
    // Step 2: Generate unique device key pair
    ret = generate_device_keypair();
    if (ret != ESP_OK) return ret;
    
    // Step 3: Create CSR with device public key
    char csr[2048];
    size_t csr_length;
    ret = create_device_csr(csr, sizeof(csr), &csr_length);
    if (ret != ESP_OK) return ret;
    
    // Step 4: Connect to AWS IoT with claim credentials  
    ret = mqtt_connect_with_claim_credentials();
    if (ret != ESP_OK) return ret;
    
    // Step 5: Request device certificate
    ret = send_create_certificate_request(csr);
    if (ret != ESP_OK) return ret;
    
    // Step 6: Wait for certificate response (handled in callback)
    ret = wait_for_certificate_response(30000);  // 30 second timeout
    if (ret != ESP_OK) return ret;
    
    // Step 7: Register Thing (done automatically after cert received)
    ret = wait_for_thing_registration(10000);    // 10 second timeout  
    if (ret != ESP_OK) return ret;
    
    // Step 8: Switch to device credentials
    ret = mqtt_reconnect_with_device_credentials();
    if (ret != ESP_OK) return ret;
    
    // Step 9: Clean up (optional)
    #ifdef CONFIG_DELETE_CLAIM_CREDENTIALS
    delete_claim_credentials();
    #endif
    
    ESP_LOGI(TAG, "✅ Fleet provisioning completed successfully");
    return ESP_OK;
}

Next Steps

Fleet Provisioning transforms your device from factory visitor to registered employee — completely automated, secure, and scalable for millions of devices!