Phase 2: Operational Certificate via MQTT Fleet Provisioning
Who should read these docs?
Firmware engineers implementing the fleet provisioning sequence on the device itself. This page describes the exact behavior expected by the CPP platform.
After the bootstrap certificate is installed during manufacturing (Phase 1), the device runs this sequence once on first boot to obtain its operational certificate and register itself with AWS IoT Core.
How It Works
- Device connects to AWS IoT Core using the bootstrap certificate (mTLS, port 8883)
- Device subscribes to all response topics before publishing anything
- Device generates a fresh RSA key pair and CSR on-device
- Device publishes the CSR and receives back a signed operational certificate and an ownership token
- Device publishes a RegisterThing request using the ownership token
- AWS IoT creates the device's Thing record and attaches IoT policies
- Device disconnects and reconnects using the operational certificate. Provisioning is complete.
Connection Requirements
Connect to the MQTT endpoint for the device's region of sale. See Prerequisites for the full endpoint table.
| Parameter | Value |
|---|---|
| Port | 8883 |
| Authentication | mTLS (bootstrap certificate + private key) |
| Client ID | Device MPBID (uppercase) |
| Keep Alive | 30 seconds |
| Clean Session | false. The broker stores QoS 1 subscriptions across reconnects for the same client ID, so if the device disconnects between subscribing and receiving a response, the broker will redeliver rather than drop the message. |
| TLS SNI | Required. The endpoint uses SNI-only mode. |
| Server CA | AmazonRootCA1; see Prerequisites |
The bootstrap certificate can only connect and publish/subscribe to Fleet Provisioning topics. It cannot update shadows or publish asset scan data. After this phase completes, the device must reconnect with the operational certificate for all normal operations.
Provisioning Template
The provisioning template name is device-type-specific — contact the CPP team for your product's value. Currently defined templates:
| Device Type | Provisioning Template |
|---|---|
generic-device | generic-provisioning-template |
bridge | bridge-provisioning-template |
Throughout the rest of this page, {provisioning-template-name} stands in for your product's actual template name — substitute it directly into the topic strings and the templateName field.
MQTT Topics
All response topics (the accepted and rejected topics) must be subscribed to before publishing the CSR request. AWS IoT Core may deliver the response before the publish call returns, so subscribing first is not optional.
| Topic | Direction | QoS | When |
|---|---|---|---|
$aws/certificates/create-from-csr/json | Publish | 1 | Send the CSR to request a certificate |
$aws/certificates/create-from-csr/json/accepted | Subscribe | 1 | Receive the signed certificate and ownership token |
$aws/certificates/create-from-csr/json/rejected | Subscribe | 1 | Receive error if certificate request fails |
$aws/provisioning-templates/{provisioning-template-name}/provision/json | Publish | 1 | Register as an IoT Thing |
$aws/provisioning-templates/{provisioning-template-name}/provision/json/accepted | Subscribe | 1 | Receive confirmation; Thing created and policies attached |
$aws/provisioning-templates/{provisioning-template-name}/provision/json/rejected | Subscribe | 1 | Receive error if registration fails |
Certificate Ownership Token
When AWS IoT Core responds to the CSR request it includes a certificateOwnershipToken, a short-lived JWT cryptographically bound to the new certificate. This token must be included in the RegisterThing request.
The token is valid for approximately 5 minutes. The firmware must publish RegisterThing within that window after receiving the accepted response. If it expires, discard the issued certificate and restart from Step 3 (generate a new CSR).
This is why the device is responsible for calling RegisterThing rather than a backend service doing it. The token is only delivered to whoever published the CSR: the device. It proves that the entity attempting to register is the same entity that generated the certificate, not a third party that intercepted the certificate on the accepted topic. If a backend service called RegisterThing on the device's behalf, the token would have to travel from the device to the backend first, creating a new attack surface.
The pre-hook Lambda is the backend's control point; it validates the MPBID and can reject the registration, but the registration itself is intentionally device-driven so the ownership proof never has to leave the device.
What RegisterThing Does
When the device calls RegisterThing, the appropriate provisioning-template is invoked within AWS which causes the following to happen:
- Validate the MPBID format via a pre-hook Lambda (rejects if lowercase, wrong length, or whitespace)
- Create an IoT Thing named after the MPBID (e.g.,
FFFF000001) - Assign the Thing to the appropriate group
- Activate the operational certificate and attaches relevant security policies to the certificate. (without the policies attached, the certificate is effectively useless)
CSR Subject Fields
The device must generate a CSR containing these X.509 subject fields exactly. The Common Name and Given Name fields are device-specific; all others are fixed values.
These requirements are the same for all CSRs regardless of type of certificate being requested.
| Field | OID | Value |
|---|---|---|
| Common Name (CN) | 2.5.4.3 | Device type string (e.g., bridge). Contact the CPP team for your product's value. |
| Given Name (GN) | 2.5.4.42 | MPBID (10-character hex string) unique for each unit |
| Organization (O) | 2.5.4.10 | Milwaukee Tool |
| Organizational Unit (OU) | 2.5.4.11 | Connected Products |
| Country (C) | 2.5.4.6 | US |
| State (ST) | 2.5.4.8 | WI |
| Locality (L) | 2.5.4.7 | Brookfield |
MQTT Payload Reference
If you are implementing Phase 2 without the AWS IoT Device SDK (e.g., a bare-metal C/C++ stack), use the raw JSON payloads below. The SDK abstracts these structures, but the topics and JSON fields are what AWS IoT Core actually processes.
CreateCertificateFromCSR
Publish to $aws/certificates/create-from-csr/json:
{
"certificateSigningRequest": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...base64...\n-----END CERTIFICATE REQUEST-----\n"
}
The CSR PEM string must preserve the \n newlines within the Base64 body (not just at the header/footer boundaries). Flatten the PEM to a single-line JSON string value with literal \n escape sequences.
Received on $aws/certificates/create-from-csr/json/accepted:
{
"certificateId": "64-character-hex-id",
"certificatePem": "-----BEGIN CERTIFICATE-----\nMIIC...base64...\n-----END CERTIFICATE-----\n",
"certificateOwnershipToken": "eyJhbGci..."
}
Store certificatePem as the operational certificate. Pass certificateOwnershipToken directly to RegisterThing; do not decode or inspect it. certificateId does not need to be stored by the firmware.
Received on $aws/certificates/create-from-csr/json/rejected:
{
"statusCode": 400,
"errorCode": "InvalidCsr",
"errorMessage": "..."
}
RegisterThing
Publish to $aws/provisioning-templates/{provisioning-template-name}/provision/json:
{
"templateName": "{provisioning-template-name}",
"certificateOwnershipToken": "eyJhbGci...",
"parameters": {
"MPBID": "FFFF000001"
}
}
MPBID must be uppercase. templateName must be your product's exact template name (see Provisioning Template); any typo returns ResourceNotFound.
Received on $aws/provisioning-templates/{provisioning-template-name}/provision/json/accepted:
{
"deviceConfiguration": {},
"thingName": "FFFF000001"
}
thingName is always the MPBID. deviceConfiguration is empty for CPP devices.
Received on $aws/provisioning-templates/{provisioning-template-name}/provision/json/rejected:
{
"statusCode": 400,
"errorCode": "InvalidRequest",
"errorMessage": "..."
}
Reference Implementation
The following Python script shows the complete Phase 2 flow using the AWS IoT Device SDK for Python. Adapt the MQTT calls to your platform's client library; the topic names and payload structures are what matters, not the SDK.
import time
from awscrt import mqtt
from awsiot import iotidentity, mqtt_connection_builder
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes, serialization
from cryptography import x509
from cryptography.x509.oid import NameOID
# --- Configuration ---
DEVICE_MPBID = "FFFF000001" # 10-char uppercase hex MPBID
MQTT_ENDPOINT = "mqtt.prod.iot.digital.milwaukeetool.com" # Match device region of sale
BOOTSTRAP_CERT_PEM = "..." # PEM string (received from Phase 1)
BOOTSTRAP_KEY_PEM = "..." # PEM string (generated on-device in Phase 1)
DEVICE_TYPE = "bridge" # Contact CPP team for your product's value
PROVISIONING_TEMPLATE = "generic-provisioning-template" # device-type-specific; see Provisioning Template section above
RESPONSE_TIMEOUT_SECS = 15
def wait_for(response: dict, operation: str):
"""Poll until a callback populates response['data'] or response['error']."""
for _ in range(RESPONSE_TIMEOUT_SECS):
if response["error"]:
raise response["error"]
if response["data"]:
return response["data"]
time.sleep(1)
raise TimeoutError(f"Timed out waiting for {operation} response")
# --- Step 1: Connect with bootstrap certificate ---
mqtt_connection = mqtt_connection_builder.mtls_from_bytes(
endpoint=MQTT_ENDPOINT,
cert_bytes=BOOTSTRAP_CERT_PEM.encode(),
pri_key_bytes=BOOTSTRAP_KEY_PEM.encode(),
client_id=DEVICE_MPBID,
clean_session=False,
keep_alive_secs=30,
)
mqtt_connection.connect().result()
identity_client = iotidentity.IotIdentityClient(mqtt_connection)
# --- Step 2: Subscribe to all response topics before publishing anything ---
create_cert_response = {"data": None, "error": None}
register_thing_response = {"data": None, "error": None}
identity_client.subscribe_to_create_certificate_from_csr_accepted(
request=iotidentity.CreateCertificateFromCsrSubscriptionRequest(),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda r: create_cert_response.update({"data": r}),
)[0].result()
identity_client.subscribe_to_create_certificate_from_csr_rejected(
request=iotidentity.CreateCertificateFromCsrSubscriptionRequest(),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda e: create_cert_response.update({"error": Exception(f"CreateCertificateFromCSR rejected: {e.error_code} - {e.error_message}")}),
)[0].result()
identity_client.subscribe_to_register_thing_accepted(
request=iotidentity.RegisterThingSubscriptionRequest(template_name=PROVISIONING_TEMPLATE),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda r: register_thing_response.update({"data": r}),
)[0].result()
identity_client.subscribe_to_register_thing_rejected(
request=iotidentity.RegisterThingSubscriptionRequest(template_name=PROVISIONING_TEMPLATE),
qos=mqtt.QoS.AT_LEAST_ONCE,
callback=lambda e: register_thing_response.update({"error": Exception(f"RegisterThing rejected: {e.error_code} - {e.error_message}")}),
)[0].result()
# --- Step 3: Generate a new RSA key pair and CSR on-device ---
# In firmware this runs on the device. Use the subject fields from the CSR Subject Fields
# section above. CN is product-specific (contact CPP team); givenName is the MPBID.
operational_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
operational_private_key_pem = operational_private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
csr_pem = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, DEVICE_TYPE),
x509.NameAttribute(NameOID.GIVEN_NAME, DEVICE_MPBID),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Milwaukee Tool"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Connected Products"),
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "WI"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Brookfield"),
])).sign(operational_private_key, hashes.SHA256()).public_bytes(serialization.Encoding.PEM).decode()
# --- Step 4: Request the operational certificate ---
identity_client.publish_create_certificate_from_csr(
request=iotidentity.CreateCertificateFromCsrRequest(certificate_signing_request=csr_pem),
qos=mqtt.QoS.AT_LEAST_ONCE,
).result()
cert_data = wait_for(create_cert_response, "CreateCertificateFromCSR")
operational_cert_pem = cert_data.certificate_pem
ownership_token = cert_data.certificate_ownership_token
# Store operational_cert_pem and operational_private_key_pem to secure storage on the device.
# Never log the private key.
# IMPORTANT: Retain the bootstrap certificate and its private key permanently in secure storage.
# The bootstrap certificate is required for certificate rotation — it is the only credential
# that can authenticate a new CSR request when the operational certificate needs to be replaced.
# --- Step 5: Register the device as an IoT Thing ---
identity_client.publish_register_thing(
request=iotidentity.RegisterThingRequest(
template_name=PROVISIONING_TEMPLATE,
certificate_ownership_token=ownership_token,
parameters={"MPBID": DEVICE_MPBID},
),
qos=mqtt.QoS.AT_LEAST_ONCE,
).result()
thing_data = wait_for(register_thing_response, "RegisterThing")
# --- Step 6: Disconnect and reconnect with the operational certificate ---
# Unsubscribe from the four provisioning response topics before disconnecting.
# Because clean_session=False, the broker retains QoS 1 subscriptions across reconnects.
# If you skip this step, the broker may redeliver stale provisioning messages on the
# next connect (same client ID, same persistent session).
identity_client.unsubscribe("$aws/certificates/create-from-csr/json/accepted")[0].result()
identity_client.unsubscribe("$aws/certificates/create-from-csr/json/rejected")[0].result()
identity_client.unsubscribe(f"$aws/provisioning-templates/{PROVISIONING_TEMPLATE}/provision/json/accepted")[0].result()
identity_client.unsubscribe(f"$aws/provisioning-templates/{PROVISIONING_TEMPLATE}/provision/json/rejected")[0].result()
mqtt_connection.disconnect().result()
# From this point on the device uses the operational certificate for all MQTT communication.
mqtt_connection = mqtt_connection_builder.mtls_from_bytes(
endpoint=MQTT_ENDPOINT,
cert_bytes=operational_cert_pem.encode(),
pri_key_bytes=operational_private_key_pem,
client_id=DEVICE_MPBID,
clean_session=False,
keep_alive_secs=30,
)
mqtt_connection.connect().result()
print(f"Provisioning complete. Thing: {thing_data.thing_name}")
Error Reference
CreateCertificateFromCSR
| Error Code | Cause | Resolution |
|---|---|---|
InvalidCsr | CSR is malformed or missing required subject fields | Regenerate the CSR with the correct subject fields |
InvalidRequest | Request payload is malformed | Verify the JSON structure |
UnauthorizedAccess | Bootstrap certificate not authorized for Fleet Provisioning | Verify the bootstrap certificate was issued correctly in Phase 1 |
InternalError | AWS IoT Core internal error | Retry with exponential backoff |
RegisterThing
| Error Code | Cause | Resolution |
|---|---|---|
ResourceAlreadyExists | A Thing with this MPBID already exists | The device completed Phase 2 in a prior session. If the device still has a valid operational certificate, it is already fully provisioned — skip directly to reconnecting with the operational cert. If the operational cert was lost (e.g., after a factory reset of NVM), re-provisioning a device that already has a registered Thing requires platform-side intervention; contact the CPP team. |
InvalidToken | Ownership token expired | Submit a new CSR and complete RegisterThing before the token expires |
InvalidRequest | Payload malformed or MPBID not uppercase | Verify MPBID is uppercase and token is present |
UnauthorizedAccess | Certificate not authorized for this provisioning template | Verify bootstrap certificate was issued correctly in Phase 1 |
ResourceNotFound | Provisioning template not found | Template name must exactly match your product's provisioning template (see Provisioning Template) |
InternalError | AWS IoT Core internal error | Retry with exponential backoff |
Device State Machine
Ideally, firmware would track provisioning state in non-volatile memory so that a reboot during Phase 2 can resume from the correct step rather than starting over.
AWS Documentation
- Fleet Provisioning by Claim: AWS documentation for the overall Fleet Provisioning mechanism this page implements
- Fleet Provisioning MQTT API: full payload reference for
CreateCertificateFromCSRandRegisterThing - AWS IoT Device SDK for Python v2: the SDK used in the reference implementation above