Chapter 270: Secure Element Usage on ESP32-H2

Chapter Objectives

By the end of this chapter, you will be able to:

  • Define what a Secure Element (SE) is and its importance in IoT security.
  • Explain the security benefits of using a hardware SE over software-based key storage.
  • Describe the features of the ESP32-H2’s integrated Secure Element.
  • Use the Digital Signature peripheral API to generate and store a secure private key.
  • Perform a cryptographic signing operation using the SE without exposing the private key.
  • Understand which ESP32 variants include an integrated Secure Element and why this matters.

Introduction

As billions of devices connect to the internet, securing them against attacks has become one of the most significant challenges in the embedded systems industry. A device’s identity is typically proven by a secret—a cryptographic private key. But where should this secret be stored? Storing it in the main flash memory, even if encrypted, can leave it vulnerable to sophisticated software or physical attacks. If an attacker can extract a device’s private key, they can impersonate it, potentially compromising an entire network.

To solve this critical problem, modern high-security microcontrollers include a Secure Element (SE). An SE is essentially a hardware vault on the chip, a dedicated, tamper-resistant coprocessor designed for one purpose: to safeguard cryptographic secrets.

The ESP32-H2, with its focus on secure IoT protocols like Zigbee, Thread, and Matter, features a built-in hardware Secure Element. This capability elevates its security posture significantly, allowing developers to build products that meet the stringent security requirements of modern smart home and industrial ecosystems. This chapter will demystify the Secure Element and show you how to leverage it to protect your device’s most valuable secrets.

Theory

What is a Secure Element?

A Secure Element is an isolated, protected hardware subsystem integrated within the main SoC. Its primary function is to securely store cryptographic keys and perform cryptographic operations.

Analogy: A Bank’s Safe Deposit Box

Think of the main CPU as the bank lobby. You can go into the lobby and ask a bank teller to perform a transaction for you. You give the teller your request (the data) and your key to the safe deposit box. The teller goes into the vault, uses your key to access your box, performs the transaction, and returns with a receipt (the result). You, standing in the lobby, never touch or see the valuable contents of the box. The teller is the only one with access, and the vault is physically secure.

In this analogy:

  • You are the main CPU application.
  • The Teller is the Secure Element’s hardware interface.
  • The Vault is the tamper-resistant hardware of the SE.
  • The Contents of the box is the private key.
  • The Receipt is the digital signature.

The core principle is isolation. The main CPU can request that the SE perform an operation with a key, but it can never read the key itself.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph LR
    subgraph "Main Application"
        CPU["Main CPU"]
    end

    subgraph "Hardware Secure Element (SE)"
        direction LR
        Vault["<br><b>Private Key Vault</b><br><i>(Non-Exportable Key)</i>"]
        SE_HW["SE Hardware<br><i>(Crypto Operations)</i>"]
    end

    CPU -- "1- 'Sign this data hash'" --> SE_HW;
    SE_HW -- "2- Uses internal key" --> Vault;
    SE_HW -- "3- Returns result" --> CPU;
    subgraph " "
        direction LR
        Result["Signature"]
    end
    
    CPU -.-> Vault;
    subgraph " "
       NoAccess["<br><b>X</b><br><i>Direct Access<br>IMPOSSIBLE</i>"]
    end

    linkStyle 3 stroke:#DC2626,stroke-width:2px,stroke-dasharray: 5 5;
    
    SE_HW --> Result

    %% Styling
    classDef cpu fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef se fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef vault fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef noaccess fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B;

    class CPU cpu;
    class SE_HW,Result se;
    class Vault vault;
    class NoAccess noaccess;

Key Principles of a Secure Element

  1. Secure Key Storage: Private keys are either generated inside the SE or securely injected during manufacturing. Once inside, they are protected by hardware and are configured to be non-exportable.
  2. Cryptographic Acceleration: SEs contain dedicated hardware to perform complex cryptographic mathematics, such as Elliptic Curve Cryptography (ECC), much faster and more efficiently than a general-purpose CPU.
  3. Tamper Resistance: The hardware is designed to resist physical attacks aimed at extracting the secrets, such as microprobing or power analysis.

The ESP32-H2 Digital Signature (DS) Peripheral

On the ESP32-H2 (and C6), the Secure Element is managed by a peripheral known as the Digital Signature (DS) peripheral. The ESP-IDF provides a clean API (esp_ds.h) to interact with it.

The key features of the DS peripheral on the ESP32-H2 include:

  • Secure storage for an ECC private key (specifically, for the secp256r1 curve, which is common in TLS and Matter).
  • Hardware-accelerated generation of a digital signature from a given data hash (SHA-256).
  • Internal key generation, allowing the device to create its own unique private key that never exists in software.

The typical workflow is simple yet powerful:

  1. Provisioning: The esp_ds_new_key() function is called. The DS peripheral commands the SE to generate a new private/public key pair. The private key is stored in the secure hardware eFuse block, making it permanent and read-protected. The public key is returned to the application.
  2. Runtime Signing: When the application needs to sign something (e.g., a challenge during a TLS handshake), it first calculates the SHA-256 hash of the data.
  3. Offloading to SE: The application calls esp_ds_sign(), passing the hash to the DS peripheral.
  4. Secure Operation: The DS peripheral performs the signing operation using the protected private key.
  5. Result: The function returns only the digital signature. The private key is never exposed.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    subgraph "One-Time Provisioning"
        A(Start) --> B{"Call<br><b>esp_ds_new_key()</b>"};
        B --> C[SE generates key pair];
        C --> D[Private Key is<br>burned to eFuse];
        D --> E[Public Key is<br>returned to App];
        E --> F(Provisioning Complete);
    end

    subgraph "Runtime Signing"
        G(App needs to sign data) --> H[Calculate SHA-256 hash<br>of the data];
        H --> I{"Call <b>esp_ds_sign()</b><br>with the hash"};
        I --> J[SE uses internal<br>private key to sign hash];
        J --> K[Signature is<br>returned to App];
        K --> L(Signing Complete);
    end

    %% Styling
    classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef secure fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef endo fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;

    class A,G start;
    class B,H,I process;
    class C,D,J secure;
    class E,F,K,L endo;

Practical Example: Generating a Key and Signing Data

This example will demonstrate the fundamental usage of the DS peripheral. We will generate a key, store it securely, and then use it to sign a piece of data.

1. Project Setup

Create a new ESP-IDF project in VS Code. This example does not require any special components beyond the standard ESP-IDF installation.

2. Code Implementation

Place the following code in your main/app_main.c. This code will generate a key on the first boot and then use it to sign a predefined message.

Warning: The esp_ds_new_key() function permanently writes to the device’s eFuse block. This operation is irreversible. While the DS eFuse block is separate from other configuration eFuses, be aware that you are permanently altering the device.

C
/* main/app_main.c */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_ds.h"
#include "esp_random.h"
#include "mbedtls/sha256.h"
#include "nvs_flash.h"

static const char *TAG = "DS_EXAMPLE";
static const char *NVS_KEY_NAME = "ds_key_present";

// Helper function to print byte arrays
void print_buf(const char *label, const uint8_t *buf, size_t len)
{
    printf("%s: ", label);
    for (size_t i = 0; i < len; i++) {
        printf("%02x", buf[i]);
    }
    printf("\n");
}

void app_main(void)
{
    esp_err_t err;

    // Initialize NVS to check if we've run before
    err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    nvs_handle_t nvs_handle;
    ESP_ERROR_CHECK(nvs_open("storage", NVS_READWRITE, &nvs_handle));

    // Check if we have already generated and stored a key
    uint8_t key_present = 0;
    err = nvs_get_u8(nvs_handle, NVS_KEY_NAME, &key_present);
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "No key found in NVS, generating a new one...");
        
        // This command generates a new private key and burns it into eFuse.
        // THIS IS A PERMANENT, ONE-TIME OPERATION per key slot.
        ESP_LOGW(TAG, "Generating new private key. This will permanently write to eFuse!");
        ESP_ERROR_CHECK(esp_ds_new_key());
        
        // Mark in NVS that the key has been provisioned
        ESP_ERROR_CHECK(nvs_set_u8(nvs_handle, NVS_KEY_NAME, 1));
        ESP_ERROR_CHECK(nvs_commit(nvs_handle));
        ESP_LOGI(TAG, "New key generated and stored securely.");
    } else {
        ESP_LOGI(TAG, "Secure key already present.");
    }
    nvs_close(nvs_handle);


    // --- Now, let's use the key to sign some data ---

    // 1. The message to be signed
    char *message = "This is a test message from ESP32-H2";
    uint8_t message_hash[32];
    
    // 2. Calculate the SHA-256 hash of the message
    mbedtls_sha256((const unsigned char *)message, strlen(message), message_hash, 0);
    print_buf("Message Hash (SHA-256)", message_hash, sizeof(message_hash));
    
    // 3. Prepare for the signing operation
    esp_ds_p_data_t p_data = {
        .p = message_hash, // Pointer to the hash
        .len = sizeof(message_hash)
    };
    uint8_t signature[64];

    // 4. Call esp_ds_sign() to perform the signing operation
    // The hardware uses the secure private key from eFuse.
    ESP_LOGI(TAG, "Signing hash with the secure key...");
    err = esp_ds_sign(&p_data, signature);

    if (err == ESP_OK) {
        ESP_LOGI(TAG, "Successfully signed the message!");
        print_buf("Resulting Signature (ECDSA)", signature, sizeof(signature));
    } else {
        ESP_LOGE(TAG, "Failed to sign message! Error: %s", esp_err_to_name(err));
    }

    // You can now use this signature to prove the device's identity.
    // For example, send the original message and the signature to a server.
    // The server can use the device's public key to verify the signature.
}

3. Build, Flash, and Run

  1. Set the Target: In VS Code, set your project’s target to esp32h2.
  2. Build and Flash: idf.py build flash -p /dev/ttyUSB0.
  3. Monitor the Output: idf.py monitor -p /dev/ttyUSB0.

First Run:

On the very first run, you will see logs indicating that a new key is being generated and written to eFuse.

Plaintext
I (xxx) DS_EXAMPLE: No key found in NVS, generating a new one...
W (xxx) DS_EXAMPLE: Generating new private key. This will permanently write to eFuse!
I (xxx) DS_EXAMPLE: New key generated and stored securely.
I (xxx) DS_EXAMPLE: Message Hash (SHA-256): 18a5...
I (xxx) DS_EXAMPLE: Signing hash with the secure key...
I (xxx) DS_EXAMPLE: Successfully signed the message!
I (xxx) DS_EXAMPLE: Resulting Signature (ECDSA): 3044...

Subsequent Runs:

On all future boots, the device will detect that the key is already present and will skip the generation step, proceeding directly to signing.

Plaintext
I (xxx) DS_EXAMPLE: Secure key already present.
I (xxx) DS_EXAMPLE: Message Hash (SHA-256): 18a5...
I (xxx) DS_EXAMPLE: Signing hash with the secure key...
I (xxx) DS_EXAMPLE: Successfully signed the message!
I (xxx) DS_EXAMPLE: Resulting Signature (ECDSA): 3044...

Variant Notes

The integrated Secure Element is a specialized feature found only in Espressif’s newer, security-focused SoCs.

Chip Integrated Secure Element (DS Peripheral) Notes
ESP32-H2 Ideal for high-security, low-power end devices (Zigbee, Thread, Matter).
ESP32-C6 Excellent for secure, multi-protocol gateways needing to protect network credentials.
ESP32 Requires an external SE chip for hardware-backed key storage.
ESP32-S2 Relies on other security features like Flash Encryption and Digital Signature.
ESP32-S3 Like S2, requires an external SE for hardware-isolated key storage.
ESP32-C3 Cost-effective variant that does not include the integrated SE.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Trying to Read Private Key There is no API to read the key, and any attempt to do so is fundamentally misunderstanding the SE’s purpose. – This is impossible by design.
– The application should only work with the public key and the final signature. The private key is for the SE’s use only.
Calling esp_ds_new_key() twice The second call to the function fails with an error. – The key is stored in a One-Time-Programmable (OTP) eFuse.
– Use NVS or a bootloader flag to check if the key has already been provisioned, and only call the function once in the device’s lifetime.
Build Fails for Wrong Chip Build fails with errors about missing esp_ds.h or undefined DS functions. – The DS peripheral is only on the ESP32-H2 and ESP32-C6.
– Ensure your project’s target in menuconfig or your IDE is set correctly. This code will not compile for an ESP32-S3, for example.
Misunderstanding Signatures Trying to “decrypt” the signature, or expecting the signature to be the same for the same message. – A signature is not encrypted data; it’s a mathematical proof of authenticity.
– ECDSA signatures include a random element, so they will be different each time.
– The signature is verified using the public key, the original data hash, and the signature itself.

Exercises

  1. Public Key Extraction: Modify the practical example. After generating the new key, call the esp_ds_get_pubkey() function to retrieve the corresponding public key. Print the public key to the console. This is the key you would typically provide to a server or cloud service to register your device.
  2. Signature Verification: This is a more advanced but highly valuable exercise. Extend the practical example by adding code to verify the signature you just created. You will need to use a software cryptography library like mbedTLS (included in ESP-IDF). The steps are:a. Get the public key from esp_ds_get_pubkey().b. Get the message hash and the signature from esp_ds_sign().c. Use the mbedTLS ECDSA functions (mbedtls_ecdsa_read_signature, mbedtls_ecdsa_verify) to verify the signature against the hash and the public key. This proves the entire cryptographic cycle works correctly.

Summary

  • A Secure Element (SE) is a hardware-based vault for storing cryptographic keys and performing secure operations.
  • The fundamental principle of an SE is isolation: private keys are generated and stored in a way that they can never be read by the main CPU.
  • The ESP32-H2 and ESP32-C6 feature an integrated SE, accessible via the Digital Signature (DS) peripheral API (esp_ds.h).
  • Using the SE is critical for high-security applications that need to protect a device’s unique identity, such as those using TLS, Matter, or other secure protocols.
  • Other ESP32 variants (original, S2, S3, C3) lack this specific integrated SE and would require an external SE chip for a similar level of hardware-backed key protection.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top