Chapter 237: Hardware Security Module Usage
Chapter Objectives
By the end of this chapter, you will be able to:
- Define the role and importance of a Hardware Security Module (HSM).
- Explain how the ESP32‘s Digital Signature (DS) peripheral functions as a hardware-protected key store.
- Understand the process of permanently provisioning a private key into eFuses.
- Use the
esp_ds
API to sign data without exposing the private key to software. - Implement a secure challenge-response authentication mechanism.
- Recognize the critical differences in HSM capabilities across ESP32 variants.
Introduction
Throughout the last few chapters, we have built a formidable fortress to protect our device’s firmware. Secure Boot validates its authenticity, Flash Encryption guards its confidentiality, and TLS certificates secure its communications. However, a skilled adversary might still seek out the weakest link. For many IoT applications, that link is the device identity private key—the secret key a device uses to authenticate itself to a cloud service using a client-side TLS certificate.
If this key is stored in flash memory, even encrypted flash, it is still potentially vulnerable to sophisticated software or side-channel attacks. What if we could store this key in a way that software could use it, but never, ever read it?
This is the exact function of a Hardware Security Module (HSM). An HSM is a specialized, tamper-resistant hardware component designed to safeguard cryptographic keys. While dedicated HSM chips are common in high-security applications, several ESP32 variants include a built-in peripheral that provides this core functionality: the Digital Signature (DS) peripheral. This chapter will teach you how to transform your ESP32 into its own hardware key vault, providing one of the strongest possible guarantees of device identity.
Theory
What is a Hardware Security Module (HSM)?
At its core, an HSM follows a simple but powerful principle: private keys never leave the hardware.
An HSM is a crypto-processor that provides a secure boundary around cryptographic keys. You can send it data and request a cryptographic operation (like creating a digital signature), but you cannot extract the key itself. It’s like having a trusted notary locked in a secure room. You can slide a document under the door and ask them to sign it with their unique seal, and they will slide the signed document back out. You get the benefit of their trusted signature, but you can never get your hands on the seal itself.
This protects the key from a wide range of attacks. If an attacker compromises the main application processor, they still cannot steal the private key because the CPU has no physical path to read it from the HSM.
The ESP32 Digital Signature (DS) Peripheral
On supported ESP32 variants, the DS peripheral is a hardware implementation of a basic HSM. It is designed to perform one primary task: create a digital signature using a private key that is permanently stored in its own dedicated eFuse block.
The process involves three distinct phases:
- Provisioning: A private key is generated on a secure host computer. Using the
espefuse.py
tool, this key is written into the special-purpose eFuses of the DS peripheral. This is a one-time, irreversible operation. Once the eFuses are write-protected, the key cannot be altered or read out by any software, including the 1st stage bootloader. - Operation (Signing): When the application needs to prove its identity, it prepares a piece of data (the “message,” often a random challenge from a server). It then passes this message to the DS peripheral via the
esp_ds
API and instructs it to generate a signature. - Result: The DS hardware internally calculates a hash of the message, signs the hash using the protected eFuse key, and returns only the resulting signature to the application. The private key remains securely locked within the silicon.
graph TD subgraph "Application Space (CPU, RAM, Flash)" A("<b>Application Code</b><br>Prepares message (challenge)") B{{"Result: Signature"}} end subgraph " " subgraph "Secure Hardware Boundary" C("<b>Digital Signature (DS) Peripheral</b>") D["eFuse Private Key<br><i>(Permanent & Inaccessible)</i>"] end end A -- "1- Message (Challenge)" --> C; C -- "2- Internally signs hash of message<br>using eFuse key" --> D; D -. "<b>KEY NEVER LEAVES</b>" .-> C; C -- "3- Returns <b>only</b> the signature" --> B; classDef app fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF classDef secure fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B classDef result fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 class A,B app class C,D secure class B result
This mechanism is the foundation for highly secure authentication protocols like challenge-response, where a server can cryptographically verify it is communicating with a genuine device.
Practical Example: Challenge-Response Authentication
Let’s build a system where the ESP32 proves its identity to a verifier. We will use the DS peripheral to sign a “challenge” message.
Warning: This example involves writing to eFuses, which is PERMANENT. An eFuse bit, once programmed from 0 to 1, can never be changed back. Perform these steps only on a designated development board that you are willing to dedicate to this purpose.
sequenceDiagram participant Verifier as "Verifier<br/>(Cloud Server)" participant Device as "ESP32 with DS<br/>(IoT Device)" participant DS as "DS Peripheral" Verifier->>Device: 1. Request Authentication Device->>Device: 2. Generate random "challenge" message Note right of Device: In a real system, the Verifier<br/>would generate and send the challenge. Device->>Device: 3. Calculate SHA-256 hash of challenge rect rgb(240, 248, 255) Note over Device, DS: Secure Hardware Operation Device->>DS: 4. esp_ds_sign(message_hash) Note over DS: Private key from eFuse is used internally.<br/>It is never loaded into RAM. DS->>Device: 5. Returns Signature end Device->>Verifier: 6. Send(challenge, signature) Verifier->>Verifier: 7. Verify Signature Note left of Verifier: Verifier uses the device's public key<br/>to check if signature is valid for the challenge. alt Signature is Valid Verifier->>Device: 8. SUCCESS: Authentication Granted else Signature is Invalid Verifier->>Device: 8. FAILURE: Access Denied end
Step 1: Generate Key and Provision the Device
This step is performed on your host PC with the ESP32 connected. We will use the ESP32-S3 as our target for this example.
- Generate a Private Key: We will create a 2048-bit RSA private key.
# This command requires esp-idf to be sourced in the terminal esp_secure.py generate_ds_key my_ds_key.pem --secure-version 2
This creates the private key filemy_ds_key.pem
. - Burn the Key to eFuse: This is the irreversible step. The command burns the key to the dedicated eFuse block and then permanently write-protects it.
# Replace (YOUR_PORT) with your device's serial port espefuse.py -p (YOUR_PORT) burn_key ds my_ds_key.pem RSA
The tool will ask for confirmation. Type “BURN” to proceed. Your device now has a permanent, hardware-protected private key. The corresponding public key is what you would provide to your server/verifier.
Step 2: Write the ESP32 Application
The device code will wait for a message, sign it, and print the signature. In a real application, this would be sent over a network.
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_ds.h"
#include "mbedtls/sha256.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "DS_EXAMPLE";
// In a real system, this challenge would come from a server.
const char *challenge_message = "Authenticate thyself, noble device!";
void app_main(void)
{
// Wait a moment for the system to fully start
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "Device provisioned with a hardware key.");
ESP_LOGI(TAG, "Attempting to sign a challenge: '%s'", challenge_message);
// 1. Calculate the SHA-256 hash of the message. The DS peripheral signs the hash.
unsigned char message_hash[32];
mbedtls_sha256((const unsigned char *)challenge_message, strlen(challenge_message), message_hash, 0);
// 2. Prepare for signing
esp_err_t ret;
size_t signature_len = 0;
// First, get the required signature length
ret = esp_ds_get_signature_len(&signature_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get signature length: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "Required signature length: %d bytes", signature_len);
unsigned char *signature = malloc(signature_len);
if (signature == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for signature");
return;
}
// 3. Perform the signing operation
// This function sends the hash to the hardware peripheral.
// The key is never loaded into RAM.
ret = esp_ds_sign(message_hash, sizeof(message_hash), signature, &signature_len);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Successfully signed the message!");
ESP_LOGI(TAG, "Signature (hex):");
// Print the signature as a hex string
for(int i = 0; i < signature_len; i++) {
printf("%02x", signature[i]);
}
printf("\n");
} else {
ESP_LOGE(TAG, "Failed to sign message: %s", esp_err_to_name(ret));
}
free(signature);
// The application would now send this signature to the server for verification.
}
Step 3: Build and Run
- Configure your project for the correct ESP32 variant (e.g.,
idf.py set-target esp32s3
). - Build and flash the application:
idf.py build flash
. - Monitor the output:
idf.py monitor
. You will see the log messages, followed by the long hexadecimal string of the signature. This signature can only be generated by the unique private key locked inside your device’s silicon.
Variant Notes
The availability and capability of the Digital Signature peripheral is one of the most significant security differentiators between ESP32 variants. It is crucial to select a chip that supports this feature if you require hardware-backed device identity.
Chip Variant | DS Peripheral Support | Supported Algorithms | Key Security Advantage |
---|---|---|---|
ESP32 | No | N/A | Feature not available. Private keys must be stored in software. |
ESP32-S2 | Yes | RSA up to 4096-bit | Provides hardware-backed key protection for RSA-based systems. |
ESP32-S3 | Yes | RSA up to 4096-bit | Same as ESP32-S2. |
ESP32-C3 | Yes | RSA up to 3072-bit | Provides hardware-backed key protection for RSA. |
ESP32-C6 | Yes | RSA up to 3072-bit & ECDSA (secp256r1) | Adds ECC support. ECDSA provides smaller keys and faster operations. |
ESP32-H2 | Yes | RSA up to 3072-bit & ECDSA (secp256r1) | Same as ESP32-C6, offering modern, efficient, hardware-backed keys. |
Tip: For new designs requiring the highest level of security and efficiency, prefer variants with ECDSA support (ESP32-C6, ESP32-H2). Elliptic Curve Cryptography offers equivalent security to RSA with much smaller key and signature sizes, saving memory, power, and transmission time.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Using DS API on original ESP32 | Code fails to compile with errors about undeclared functions like esp_ds_sign. Or, runtime error ESP_ERR_NOT_SUPPORTED. | The original ESP32 lacks the DS peripheral. Always check your variant’s datasheet. For cross-compatible code, use preprocessor guards: #if CONFIG_ESP_DS_ENABLED. |
Permanently burning a test key | Device authenticates correctly with a test/debug identity, but cannot be provisioned for production. The device is permanently “locked” to the test identity. | eFuse burning is irreversible. Use separate, clearly marked development boards for any eFuse experiments. Implement a strict key management protocol and never test on production-track hardware. |
Mismatched key and algorithm | Signature verification fails on the server. The esp_ds_sign function may return an error if the hardware doesn’t support the requested operation with the burned key type (e.g., trying ECDSA on an RSA-only key). | Ensure the algorithm specified in the espefuse.py burn_key command (e.g., RSA, ECDSA) matches the cryptographic scheme used by the device firmware and the verification server. |
Re-burning a write-protected key | The espefuse.py tool fails with an error indicating the key block is write-protected or already burned. | This is expected behavior and confirms the security mechanism is working correctly. The error is in the process (trying to re-burn), not a hardware fault. A device can only be provisioned once. |
Exercises
- Verifier Implementation (Conceptual): The practical example showed the device-side code. Write a Python script using the cryptography library that takes the role of the server. It should:a. Load the public key corresponding to my_ds_key.pem.b. Take the challenge string and the hex signature from the ESP32’s output as input.c. Verify the signature and print “SUCCESS” or “FAILURE”.
- Security Policy Design: You are designing a fleet of environmental sensors. Some will be based on the ESP32-S3 and some on the original ESP32 due to cost constraints. Write a short security policy document (2-3 paragraphs) outlining how you will manage device identity for both types of devices. How will you protect the private keys on the ESP32s? What are the accepted risks for that device type?
Summary
- A Hardware Security Module (HSM) protects private keys by ensuring they can be used but never read by software.
- On supported variants (S2, S3, C3, C6, H2), the Digital Signature (DS) peripheral acts as a built-in HSM.
- Provisioning the DS peripheral with a key via
espefuse.py
is a permanent, one-time operation. - The
esp_ds
API allows an application to sign data using the hardware-protected key, which is essential for secure authentication schemes like challenge-response. - This feature provides a hardware root of trust for device identity, offering a much higher level of security than storing keys in flash memory.
- Selecting a variant with DS support, especially ECDSA, is a critical design decision for secure, modern IoT products.
Further Reading
- ESP-IDF Digital Signature (DS) API Reference: https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/ds.html (Note: change
esp32s3
in the URL to your target chip). - eFuse Manager
espefuse.py
Documentation: https://docs.espressif.com/projects/esptool/en/latest/esp32/espefuse/index.html - ESP-IDF
esp_secure
Python Tool: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/kconfig.html#esp-secure-py