En la parte 3 creamos un certificado a mano y lo pegamos en el código del ESP32. Para un dispositivo está bien. Para diez, o para un producto, es inviable: un firmware distinto por placa, llaves privadas en archivos y en el historial de Git.
AWS IoT tiene una solución oficial: Fleet Provisioning. Todos los dispositivos salen con el mismo firmware, y cada uno obtiene su propio certificado la primera vez que se conecta. En esta parte vemos cómo funciona y cómo configurarlo.
Las opciones para dar identidad a un dispositivo
| Método | Cómo funciona | Cuándo usarlo |
|---|---|---|
| Manual | Creas el certificado y lo cargas en cada placa | Pocos dispositivos, pruebas |
| Fleet Provisioning por claim | El firmware trae un certificado compartido muy limitado; con él, cada dispositivo pide uno propio | Fabricar muchos dispositivos iguales |
| Fleet Provisioning por usuario de confianza | Una app autenticada pide un certificado temporal y se lo pasa al dispositivo durante la instalación | El usuario instala el dispositivo con una app |
| JITP / JITR con tu propia CA | Firmas los certificados en fábrica; AWS registra el dispositivo en su primera conexión | Tienes tu propia autoridad de certificación |
Fleet Provisioning por claim, paso a paso
- Creas un certificado de claim y una política que solo le permite pedir un certificado nuevo y registrarse.
- Ese certificado va en el firmware (o en NVS) de todos los dispositivos.
- Al arrancar sin identidad propia, el dispositivo se conecta con el certificado de claim y pide un certificado nuevo por MQTT.
- AWS le devuelve un certificado y su llave privada.
- El dispositivo llama a la plantilla de aprovisionamiento con ese certificado y sus datos (por ejemplo, su número de serie).
- AWS crea el Thing, activa el certificado y le adjunta la política definitiva.
- El dispositivo guarda su certificado en NVS (parte 6), se desconecta y vuelve a conectarse con su identidad propia.
La plantilla de aprovisionamiento
Describe qué crear para cada dispositivo. Un ejemplo:
{
"Parameters": {
"NumeroSerie": { "Type": "String" },
"AWS::IoT::Certificate::Id": { "Type": "String" }
},
"Resources": {
"certificate": {
"Type": "AWS::IoT::Certificate",
"Properties": {
"CertificateId": { "Ref": "AWS::IoT::Certificate::Id" },
"Status": "Active"
}
},
"thing": {
"Type": "AWS::IoT::Thing",
"Properties": {
"ThingName": { "Fn::Join": ["", ["casa-demo-", { "Ref": "NumeroSerie" }]] }
}
},
"policy": {
"Type": "AWS::IoT::Policy",
"Properties": { "PolicyName": "casa-demo-dispositivo" }
}
}
}
La política casa-demo-dispositivo es la de la parte 13, con ${iot:Connection.Thing.ThingName}: una sola política sirve para todos los dispositivos que se registren.
Para crear la plantilla necesitas un rol de aprovisionamiento que AWS IoT asume para crear los recursos (la política administrada AWSIoTThingsRegistration cubre lo necesario):
aws iot create-provisioning-template \
--template-name casa-demo \
--provisioning-role-arn arn:aws:iam::CUENTA:role/aprovisionamiento-iot \
--template-body file://plantilla.json \
--enabled
La política del certificado de claim
Es la pieza más delicada: ese certificado está en todos los dispositivos. Solo debe permitir lo mínimo para aprovisionarse:
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "iot:Connect", "Resource": "*" },
{
"Effect": "Allow",
"Action": ["iot:Publish", "iot:Receive"],
"Resource": [
"arn:aws:iot:REGION:CUENTA:topic/$aws/certificates/create/*",
"arn:aws:iot:REGION:CUENTA:topic/$aws/provisioning-templates/casa-demo/provision/*"
]
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": [
"arn:aws:iot:REGION:CUENTA:topicfilter/$aws/certificates/create/*",
"arn:aws:iot:REGION:CUENTA:topicfilter/$aws/provisioning-templates/casa-demo/provision/*"
]
}
]
}
Con este certificado no se puede leer ni controlar ningún dispositivo: solo pedir un certificado y registrarse con esa plantilla.
Lo que hace el dispositivo
Los tópicos del proceso (todas las respuestas llegan en .../accepted o .../rejected):
| Paso | Publica en | Recibe en |
|---|---|---|
| Pedir certificado y llave | $aws/certificates/create/json | $aws/certificates/create/json/accepted |
| Registrarse | $aws/provisioning-templates/casa-demo/provision/json | .../provision/json/accepted |
Un esquema del código con la librería MQTT de la parte 3:
void onMessage(String& topic, String& payload) {
JsonDocument doc;
if (deserializeJson(doc, payload)) return;
if (topic.endsWith("create/json/accepted")) {
// Guardar en memoria (aún no en NVS) el certificado y la llave
certNuevo = doc["certificatePem"].as<String>();
llaveNueva = doc["privateKey"].as<String>();
String token = doc["certificateOwnershipToken"].as<String>();
JsonDocument registro;
registro["certificateOwnershipToken"] = token;
registro["parameters"]["NumeroSerie"] = numeroSerie();
String cuerpo;
serializeJson(registro, cuerpo);
client.publish("$aws/provisioning-templates/casa-demo/provision/json", cuerpo);
} else if (topic.endsWith("provision/json/accepted")) {
guardarIdentidad(doc["thingName"].as<String>(), certNuevo, llaveNueva); // ahora sí, a NVS
ESP.restart(); // vuelve a conectar con su identidad propia
} else if (topic.endsWith("rejected")) {
Serial.printf("Rechazado: %s\n", payload.c_str());
}
}
void aprovisionar() {
client.subscribe("$aws/certificates/create/json/accepted");
client.subscribe("$aws/certificates/create/json/rejected");
client.subscribe("$aws/provisioning-templates/casa-demo/provision/json/accepted");
client.subscribe("$aws/provisioning-templates/casa-demo/provision/json/rejected");
client.publish("$aws/certificates/create/json", "{}");
}
Detalles importantes:
- El buffer MQTT debe ser grande: la respuesta con el certificado y la llave ocupa varios KB.
- Guarda en NVS solo después del registro aceptado. Si el registro falla, el certificado nuevo queda inactivo y no sirve.
- Conéctate con el client ID que exija la política del claim. Con
Resource: "*"eniot:Connect, cualquiera vale; puedes restringirlo a un prefijo. - La variante con CSR (
$aws/certificates/create-from-csr/json) es más segura: el dispositivo genera su propia llave y solo envía la solicitud de firma, así que la llave privada nunca sale del chip. Requiere generar la CSR en el ESP32 (por ejemplo, con mbedTLS).
Validar quién se registra: el pre-provisioning hook
Como el certificado de claim está en todos los dispositivos, alguien podría extraerlo y registrar dispositivos falsos. La plantilla admite un pre-provisioning hook: una Lambda que AWS llama antes de crear nada, con los parámetros que envió el dispositivo.
export const handler = async (event) => {
const serie = event.parameters?.NumeroSerie;
const valido = await numeroDeSerieFabricado(serie); // tu lista de números de serie fabricados
return { allowProvisioning: Boolean(valido) };
};
Si devuelve false, el registro se rechaza. Con una lista de números de serie fabricados (y que no se puedan usar dos veces), un certificado de claim robado no sirve para mucho.
Fleet Provisioning por usuario de confianza
Si el usuario instala el dispositivo con una app, esta puede llamar a CreateProvisioningClaim (con sus credenciales de AWS) y obtener un certificado de claim temporal, válido solo 5 minutos. La app se lo pasa al dispositivo (por ejemplo, por el portal de la parte 16 o por BLE), y el resto del proceso es el mismo. No hay ningún certificado compartido en el firmware.
Errores comunes
create/json/rejectedsin más. La política del claim no permite el tópico, o falta suscribirse arejectedpara ver el motivo.- El registro falla con un error de plantilla. Un parámetro obligatorio no llegó, o el rol de aprovisionamiento no tiene permisos.
- El dispositivo se aprovisiona en cada arranque. No está guardando la identidad en NVS, o no la lee antes de decidir.
- Respuesta truncada. El buffer de la librería MQTT es pequeño para el certificado y la llave.
Preguntas frecuentes
¿Es seguro poner el certificado de claim en el firmware?
Es un riesgo aceptado por el diseño, que se reduce con una política mínima, el pre-provisioning hook y, si puedes, con el cifrado de flash. Si no puedes asumirlo, usa la variante por usuario de confianza.
¿Cuánto cuesta?
Fleet Provisioning no tiene costo propio: pagas los mensajes MQTT del proceso y, si la usas, la Lambda del hook. Unos pocos mensajes por dispositivo, una sola vez.
¿Puedo cambiar la plantilla después?
Sí, las plantillas tienen versiones. Los dispositivos ya registrados no cambian; los nuevos usan la versión por defecto.