test(examples/security): Add an example to demonstrate signing using Key Manager keys

This commit is contained in:
harshal.patil
2026-02-27 12:41:26 +05:30
parent 61b9df4490
commit a8ffefe096
13 changed files with 1744 additions and 4 deletions
+10 -4
View File
@@ -11,7 +11,6 @@ examples/security/flash_encryption:
temporary: true
reason: lack of runners
depends_components:
- *common_components
- bootloader_support
examples/security/hmac_soft_jtag:
@@ -21,11 +20,20 @@ examples/security/hmac_soft_jtag:
- if: IDF_TARGET not in ["esp32c6"]
reason: sufficient to test on one HMAC-capable chip
depends_components:
- *common_components
- esp_hw_support
depends_filepatterns:
- examples/security/hmac_soft_jtag/**/*
examples/security/key_manager:
disable:
- if: SOC_KEY_MANAGER_SUPPORTED != 1
disable_test:
- if: IDF_TARGET not in ["esp32c5"]
reason: sufficient to test on one Key Manager-supported chip
depends_components:
- esp_security
- mbedtls
examples/security/nvs_encryption_hmac:
disable:
- if: SOC_HMAC_SUPPORTED != 1
@@ -34,7 +42,6 @@ examples/security/nvs_encryption_hmac:
temporary: true
reason: lack of runners
depends_components:
- *common_components
- nvs_flash
- nvs_sec_provider
depends_filepatterns:
@@ -44,7 +51,6 @@ examples/security/security_features_app:
disable:
- if: IDF_TARGET not in ["esp32c3", "esp32s3"]
depends_components:
- *common_components
- bootloader_support
examples/security/tee/tee_attestation:
+8
View File
@@ -37,6 +37,14 @@ The steps include:
* Generating token data from the key.
* Using commands to re-enable JTAG access.
## Key Manager Signing
This example demonstrates how to use the Key Manager peripheral for cryptographic signing operations on Key Manager supported chips. The Key Manager provides secure key deployment and management, allowing keys to be used for cryptographic operations without exposing the raw key material.
The example showcases:
* ECDSA signing using Key Manager deployed keys via PSA Crypto API.
* RSA-DS (Digital Signature) signing using Key Manager deployed keys via PSA Crypto API.
* Key deployment in AES mode with pre-generated key material.
# ESP-TEE (Trusted Execution Environment)
For examples demonstrating the ESP-TEE framework, please refer to the [ESP-TEE examples](tee/README.md) directory. It contains examples showcasing operations like AES encryption/decryption in the TEE, secure storage with signing and encryption capabilities, secure OTA updates for TEE applications, and entity attestation token generation.
@@ -0,0 +1,8 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(key_manager)
+294
View File
@@ -0,0 +1,294 @@
| Supported Targets | ESP32-C5 | ESP32-P4 |
| ----------------- | -------- | -------- |
# Key Manager Signing Example
## Overview
This example demonstrates how to use the Key Manager peripheral for cryptographic signing operations on Key Manager supported chips. The Key Manager provides secure key deployment and management, allowing keys to be used for cryptographic operations without exposing the raw key material.
The example showcases two signing use cases:
1. **ECDSA Signing**: Deploy an ECDSA private key via Key Manager and use it for ECDSA-P256 signature generation through the PSA Crypto API.
2. **RSA-DS Signing**: Deploy a Digital Signature (DS) key via Key Manager and use it for RSA-3072 signature generation through the PSA Crypto API.
## Key Manager Concepts
The Key Manager peripheral provides:
- **Secure Key Deployment**: Keys can be deployed in different modes (AES, ECDH0, Random) without exposing the raw key material.
- **Key Recovery**: Once deployed, keys can be recovered using the key recovery information for subsequent operations.
- **Hardware Protection**: Keys are protected by hardware and cannot be read back in plaintext.
### Key Deployment Modes
This example uses the **AES deployment mode** with pre-generated key material:
- `sw_init_key`: Software initialization key (32 bytes)
- `k2_info`: K2 information for key derivation (64 bytes)
- `k1_encrypted`: Encrypted K1 key material (32 bytes)
These parameters are used to derive and deploy the actual cryptographic key inside the Key Manager.
## How to Use the Example
### Hardware Required
This example requires an ESP32-C5 (Key Manager supported) development board.
### Configure the Project
```bash
idf.py set-target esp32c5
idf.py menuconfig
```
In the menuconfig, you can enable/disable the signing examples:
- `Key Manager Signing Example Configuration`
- `Enable ECDSA signing example` (default: enabled)
- `Enable RSA-DS signing example` (default: enabled)
### Build and Flash
```bash
idf.py build
idf.py -p PORT flash monitor
```
(Replace `PORT` with your serial port, e.g., `/dev/ttyUSB0` or `COM3`)
To exit the serial monitor, type `Ctrl-]`.
### Key Storage in NVS
The example stores **key recovery info** in the NVS namespace `key_mgr` so that keys persist across reboots:
- **First boot** (or after `idf.py erase-flash`): No key is found in NVS, so the example deploys a new key via Key Manager and saves the recovery info to NVS. You will see logs like `No ECDSA key in NVS, deploying new key via Key Manager...`.
- **Later boots** (reset or `idf.py flash monitor` without erase): The example finds the key in NVS and reuses it. You will see `Using existing ECDSA key from NVS` (or DS).
To force **new keys** to be deployed (e.g. for testing), run erase before flashing:
```bash
idf.py erase-flash
idf.py -p PORT flash monitor
```
After `erase-flash`, the first boot will always deploy and store new keys; subsequent runs without erase will use the stored keys.
### Verify the flow with idf.py
From the example directory:
```bash
cd path/to/esp-idf/examples/security/key_manager
# Build
idf.py build
idf.py -p PORT flash
# Erase the NVS partition to force fresh key deployment on first run
esptool -p PORT --after no-reset erase-region 0x9000 0x6000
idf.py -p PORT monitor
# Expect: "No ECDSA key in NVS, deploying new key..." and "No DS key in NVS, deploying new key..."
# Exit monitor with Ctrl+]
# Second run (reuse path): flash and monitor only (no erase)
idf.py -p PORT monitor
# Expect: "Using existing ECDSA key from NVS" and "Using existing DS key from NVS"
```
### Example Output
#### First Boot
```
I (283) key_mgr_sign: ECDSA Signing with Key Manager
I (283) key_mgr_sign: Step 1: No ECDSA key in NVS, deploying new key via Key Manager...
I (293) key_mgr_sign: Key deployed successfully in Key Manager
I (303) key_mgr_sign: Stored key_recovery_info in NVS for key_type "ecdsa"
I (303) key_mgr_sign: Step 2: Creating PSA opaque key reference...
I (313) key_mgr_sign: Step 3: Signing hash using Key Manager key...
I (333) key_mgr_sign: Signature generated successfully (64 bytes)
I (333) key_mgr_sign: Step 4: Verifying signature with known public key...
I (353) key_mgr_sign: ECDSA signature verification PASSED!
I (353) key_mgr_sign: ECDSA Signing Example Complete
I (353) key_mgr_sign: RSA-DS Signing with Key Manager
I (353) key_mgr_sign: Step 1: No DS key in NVS, deploying new key via Key Manager...
I (373) key_mgr_sign: Key deployed successfully in Key Manager
I (383) key_mgr_sign: Stored key_recovery_info in NVS for key_type "ds"
I (383) key_mgr_sign: Step 2: Preparing DS data context...
I (393) key_mgr_sign: Step 3: Creating PSA opaque key reference for RSA-DS...
I (393) key_mgr_sign: Step 4: Signing hash using RSA-DS with Key Manager key...
I (1563) key_mgr_sign: RSA-DS signature generated successfully (384 bytes)
I (1563) key_mgr_sign: Step 5: Verifying RSA-DS signature with known public key...
I (1573) key_mgr_sign: RSA-DS signature verification PASSED!
I (1573) key_mgr_sign: RSA-DS Signing Example Complete
I (1573) key_mgr_sign: Key Manager Signing Example finished.
```
#### Second Boot
```
I (273) key_mgr_sign: Key Manager Signing Example
I (283) key_mgr_sign: ECDSA Signing with Key Manager
I (283) key_mgr_sign: Step 1: Using existing ECDSA key from NVS
I (293) key_mgr_sign: Step 2: Creating PSA opaque key reference...
I (293) key_mgr_sign: Step 3: Signing hash using Key Manager key...
I (313) key_mgr_sign: Signature generated successfully (64 bytes)
I (313) key_mgr_sign: Step 4: Verifying signature with known public key...
I (333) key_mgr_sign: ECDSA signature verification PASSED!
I (333) key_mgr_sign: ECDSA Signing Example Complete
I (343) key_mgr_sign: RSA-DS Signing with Key Manager
I (353) key_mgr_sign: Step 1: Using existing DS key from NVS
I (353) key_mgr_sign: Step 2: Preparing DS data context...
I (363) key_mgr_sign: Step 3: Creating PSA opaque key reference for RSA-DS...
I (363) key_mgr_sign: Step 4: Signing hash using RSA-DS with Key Manager key...
I (1543) key_mgr_sign: RSA-DS signature generated successfully (384 bytes)
I (1543) key_mgr_sign: Step 5: Verifying RSA-DS signature with known public key...
I (1553) key_mgr_sign: RSA-DS signature verification PASSED!
I (1553) key_mgr_sign: RSA-DS Signing Example Complete
I (1553) key_mgr_sign: Key Manager Signing Example finished.
```
## Code Walkthrough
### ECDSA Signing Flow
1. **Deploy Key**: Use `esp_key_mgr_deploy_key_in_aes_mode()` to deploy an ECDSA key.
2. **Create Opaque Key**: Create an `esp_ecdsa_opaque_key_t` structure with the key recovery info.
3. **Import to PSA**: Use `psa_import_key()` with `PSA_KEY_LIFETIME_ESP_ECDSA_VOLATILE` lifetime.
4. **Sign**: Use `psa_sign_hash()` to generate the signature.
5. **Verify**: Import the known ECDSA public key (from `test_data.h`) and use `psa_verify_hash()` to verify the signature.
### RSA-DS Signing Flow
1. **Deploy Key**: Use `esp_key_mgr_deploy_key_in_aes_mode()` to deploy a DS (HMAC) key.
2. **Prepare DS Context**: Create `esp_ds_data_t` with IV and encrypted parameters.
3. **Create Opaque Key**: Create an `esp_rsa_ds_opaque_key_t` structure.
4. **Import to PSA**: Use `psa_import_key()` with `PSA_KEY_LIFETIME_ESP_RSA_DS` lifetime.
5. **Sign**: Use `psa_sign_hash()` to generate the RSA signature.
6. **Verify**: Import the known RSA public key (from `test_data.h`) and use `psa_verify_hash()` to verify the signature.
## Test Data
The example uses test data generated by `generate_test_data.py` and stored in `main/test_data.h`.
The included test data was generated with **seed 12345** for ESP32-C5 (default target):
```bash
python generate_test_data.py --seed 12345 --output-header main/test_data.h
```
### Target-Specific RSA Key Sizes
Different ESP32 chips support different maximum RSA key sizes for the Digital Signature (DS) peripheral:
| Target | SOC_RSA_MAX_BIT_LEN | RSA Key Size |
|--------|---------------------|--------------|
| ESP32-C5 | 3072 | 3072-bit |
| ESP32-P4 | 4096 | 4096-bit |
The `--target` option allows generating test data with the correct RSA key size for your target chip.
### Generating Custom Test Data
A Python script is provided to generate new test data. When keys are generated (not loaded from files), they are automatically saved to PEM files in the same directory as the output header:
- `ecdsa_private_key.pem`: ECDSA P-256 private key
- `rsa_private_key.pem`: RSA private key (size depends on target)
#### Basic Usage (Random Keys)
```bash
# Print test data to stdout (default: ESP32-C5, 3072-bit RSA)
python generate_test_data.py
# Generate for ESP32-C5 (3072-bit RSA, default)
python generate_test_data.py --seed 12345 --output-header main/test_data.h
# Generate for ESP32-P4 (4096-bit RSA)
python generate_test_data.py --target esp32p4 --seed 12345 --output-header main/test_data.h
# Use a different seed for new random keys
python generate_test_data.py --seed 54321 --output-header main/test_data.h
```
#### Using Your Own Keys from PEM Files
You can provide your own ECDSA and/or RSA private keys in PEM format. When a key is loaded from a file, it won't be saved again:
```bash
# Use your own ECDSA key (only RSA key will be generated and saved)
python generate_test_data.py --ecdsa-key my_ecdsa_key.pem --output-header main/test_data.h
# Use your own RSA key (only ECDSA key will be generated and saved)
python generate_test_data.py --rsa-key my_rsa_key.pem --output-header main/test_data.h
# Use both your own keys (no keys will be generated or saved)
python generate_test_data.py --ecdsa-key ecdsa.pem --rsa-key rsa.pem --output-header main/test_data.h
# Combine with seed for reproducible Key Manager parameters
python generate_test_data.py --seed 12345 --ecdsa-key test/ecdsa_private_key.pem --rsa-key test/rsa_private_key.pem --output-header main/test_data.h
```
**Key Requirements:**
- **ECDSA key**: Must be a P-256 (secp256r1) curve private key
- **RSA key**: Must match the target's RSA key size (3072-bit for ESP32-C5, 4096-bit for ESP32-P4)
**Generating PEM Keys Manually (using OpenSSL):**
```bash
# Generate ECDSA P-256 key
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_key.pem
# Generate RSA 3072-bit key (for ESP32-C5)
openssl genrsa -out rsa_key.pem 3072
# Generate RSA 4096-bit key (for ESP32-P4)
openssl genrsa -out rsa_key.pem 4096
```
The script generates:
- **Key Manager parameters**: `init_key`, `k2_info` for AES mode deployment
- **ECDSA key material**: `k1_ecdsa256_encrypt` (encrypted ECDSA private key)
- **DS key material**: `k1_ds_aes_key_encrypt`, `ds_iv`, `ds_c` (encrypted DS parameters)
> **Note:** The example verifies signatures against known public keys stored in `test_data.h`. When generating custom test data with new keys, you must also export the corresponding public keys and update `main/test_data.h`:
> ```bash
> # Export ECDSA public key (uncompressed point format for ecdsa_public_key[])
> openssl ec -in ecdsa_private_key.pem -pubout -outform DER -conv_form uncompressed | tail -c 65 | xxd -i
>
> # Export RSA public key (PKCS#1 DER format for rsa_public_key_der[])
> openssl rsa -in rsa_private_key.pem -RSAPublicKey_out -outform DER | xxd -i
> ```
#### How Key Manager Test Data is Generated
1. **Base Keys**: Random `init_key`, `k2`, and `rand_num` are generated. When `--seed` is provided, all three values are derived from the seeded RNG, making the entire output deterministic and reproducible across runs. Without `--seed`, `os.urandom` is used and every run produces different data.
2. **K2 Info**: Computed as `AES_ECB(init_key, AES_ECB(rand_num, k2) || rand_num)`
3. **K1 Encrypted**: The actual key (ECDSA or DS) is reversed and encrypted with K2 using AES-ECB
4. **DS Parameters**: RSA key parameters are encrypted using AES-CBC with the DS AES key derived from HMAC
> **Note:** The test data included in the example (`main/test_data.h`) was generated using `--seed 12345` together with the PEM keys stored in the `test/` directory (`test/ecdsa_private_key.pem` and `test/rsa_private_key.pem`). Using the same seed and key files will always produce identical output.
## Running the Test
This example includes a pytest that can be run with the ESP-IDF pytest framework:
```bash
# Run the test on ESP32-C5
pytest --target esp32c5
```
The test verifies:
- ECDSA key deployment and signing flow
- ECDSA signature verification using the known public key
- RSA-DS key deployment and signing flow
- RSA-DS signature verification using the known public key
@@ -0,0 +1,607 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
"""
Generate test data for the Key Manager Signing Example.
This script generates all the cryptographic test data needed for the example:
1. Key Manager AES mode deployment parameters (init_key, k2_info)
2. ECDSA key material (k1_ecdsa256_encrypt)
3. DS key material and encrypted parameters (k1_ds_aes_key_encrypt, ds_iv, ds_c)
The generated data can be used to replace the hardcoded values in key_mgr_sign_main.c
When keys are generated (not loaded from files), they are automatically saved to PEM files
in the same directory as the output header file:
- ecdsa_private_key.pem: ECDSA P-256 private key
- rsa_private_key.pem: RSA private key (size depends on target)
Target-specific RSA key sizes:
- ESP32-C5: 3072-bit (SOC_RSA_MAX_BIT_LEN = 3072)
- ESP32-P4: 4096-bit (SOC_RSA_MAX_BIT_LEN = 4096)
Usage:
python generate_test_data.py [--output-header FILENAME] [--target TARGET]
Options:
--output-header FILENAME Generate a C header file instead of printing to stdout
--seed SEED Random seed for reproducible output (default: random)
--target TARGET Target chip (esp32c5, esp32p4). Determines RSA key size.
Default: esp32c5 (3072-bit RSA)
--ecdsa-key FILE Path to ECDSA private key PEM file to load (P-256 curve)
--rsa-key FILE Path to RSA private key PEM file to load (must match target)
Examples:
# Generate for ESP32-C5 (3072-bit RSA, default)
python generate_test_data.py --seed 12345 --output-header main/test_data.h
# Generate for ESP32-P4 (4096-bit RSA)
python generate_test_data.py --target esp32p4 --seed 12345 --output-header main/test_data.h
# Use existing ECDSA key from PEM file (only RSA key will be generated and saved)
python generate_test_data.py --ecdsa-key my_ecdsa_key.pem --output-header main/test_data.h
# Use existing RSA key from PEM file (only ECDSA key will be generated and saved)
python generate_test_data.py --rsa-key my_rsa_key.pem --output-header main/test_data.h
# Use both keys from PEM files (no keys will be generated or saved)
python generate_test_data.py --ecdsa-key ecdsa.pem --rsa-key rsa.pem --output-header main/test_data.h
"""
import argparse
import hashlib
import hmac
import os
import struct
from collections.abc import Callable
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.rsa import _modinv as modinv # type: ignore
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.utils import int_to_bytes
# Constants matching ESP-IDF Key Manager
KEY_MGR_K1_ENCRYPTED_SIZE = 32
KEY_MGR_K2_INFO_SIZE = 64
KEY_MGR_SW_INIT_KEY_SIZE = 32
# DS peripheral constants
ESP_DS_IV_LEN = 16
# Target-specific RSA key sizes (SOC_RSA_MAX_BIT_LEN)
# These values match the SOC capabilities defined in soc_caps.h
TARGET_RSA_MAX_BIT_LEN: dict[str, int] = {
'esp32c5': 3072,
'esp32p4': 4096,
}
# Default target and RSA key size
DEFAULT_TARGET = 'esp32c5'
DEFAULT_RSA_KEY_SIZE = TARGET_RSA_MAX_BIT_LEN[DEFAULT_TARGET]
def get_rsa_key_size_for_target(target: str) -> int:
"""Get the maximum RSA key size for a given target."""
target_lower = target.lower()
if target_lower not in TARGET_RSA_MAX_BIT_LEN:
raise ValueError(f"Unknown target '{target}'. Supported targets: {', '.join(TARGET_RSA_MAX_BIT_LEN.keys())}")
return TARGET_RSA_MAX_BIT_LEN[target_lower]
# ESP_DS_C_LEN calculation:
# (max_key_size * 3 + 256 (MD) + 32 (M_prime) + 32 (length) + 64 (padding)) / 8
def calculate_ds_c_len(max_key_size: int) -> int:
return (max_key_size * 3 + 256 + 32 + 32 + 64) // 8
def bytes_to_c_array(data: bytes, name: str = '', indent: int = 4) -> str:
"""Convert bytes to C array format."""
indent_str = ' ' * indent
# Format with line breaks for readability
values = [f'0x{b:02x}' for b in data]
lines = []
for i in range(0, len(values), 8):
lines.append(indent_str + ', '.join(values[i : i + 8]) + ',')
if name:
return f'static const uint8_t {name}[] = {{\n' + '\n'.join(lines) + '\n};'
return '{\n' + '\n'.join(lines) + '\n}'
def bytes_to_c_array_oneline(data: bytes) -> str:
"""Convert bytes to single-line C array format."""
return '{ ' + ', '.join(f'0x{b:02x}' for b in data) + ' }'
def calculate_aes_ecb_encrypt(data: bytes, key: bytes) -> bytes:
"""Encrypt data using AES-ECB mode."""
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
encryptor = cipher.encryptor()
result: bytes = encryptor.update(data) + encryptor.finalize()
return result
def calculate_aes_cbc_encrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
"""Encrypt data using AES-CBC mode."""
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
result: bytes = encryptor.update(data) + encryptor.finalize()
return result
def number_as_bytes_le(number: int, pad_bits: int = 0) -> bytes:
"""Convert number to little-endian bytes with optional padding."""
result: bytes = int_to_bytes(number)[::-1] # Convert to little endian
while pad_bits != 0 and len(result) < (pad_bits // 8):
result += b'\x00'
return result
def load_ecdsa_private_key(pem_file: str) -> bytes:
"""
Load ECDSA private key from PEM file and return the raw private key bytes.
Args:
pem_file: Path to the PEM file containing an ECDSA P-256 private key
Returns:
32-byte raw private key value (big-endian)
Raises:
ValueError: If the key is not a P-256 ECDSA key
"""
with open(pem_file, 'rb') as f:
pem_data = f.read()
private_key = serialization.load_pem_private_key(pem_data, password=None, backend=default_backend())
if not isinstance(private_key, ec.EllipticCurvePrivateKey):
raise ValueError(f'Expected ECDSA private key, got {type(private_key).__name__}')
if not isinstance(private_key.curve, ec.SECP256R1):
raise ValueError(f'Expected P-256 (secp256r1) curve, got {private_key.curve.name}')
# Extract the raw private key value (32 bytes for P-256)
private_numbers = private_key.private_numbers()
private_value = private_numbers.private_value
# Convert to 32-byte big-endian representation
result: bytes = int_to_bytes(private_value).rjust(32, b'\x00')
return result
def load_rsa_private_key(pem_file: str, expected_key_size: int) -> rsa.RSAPrivateKey:
"""
Load RSA private key from PEM file.
Args:
pem_file: Path to the PEM file containing an RSA private key
expected_key_size: Expected RSA key size in bits (e.g., 3072, 4096)
Returns:
RSA private key object
Raises:
ValueError: If the key is not an RSA key or has wrong size
"""
with open(pem_file, 'rb') as f:
pem_data = f.read()
private_key = serialization.load_pem_private_key(pem_data, password=None, backend=default_backend())
if not isinstance(private_key, rsa.RSAPrivateKey):
raise ValueError(f'Expected RSA private key, got {type(private_key).__name__}')
if private_key.key_size != expected_key_size:
raise ValueError(f'Expected {expected_key_size}-bit RSA key, got {private_key.key_size}-bit')
return private_key
def save_ecdsa_private_key(private_value: bytes, pem_file: str) -> None:
"""
Save ECDSA private key to PEM file.
Args:
private_value: 32-byte raw private key value (big-endian)
pem_file: Path to save the PEM file
"""
# Convert raw bytes to integer
private_int = int.from_bytes(private_value, byteorder='big')
# Derive the public key from the private key
private_key = ec.derive_private_key(private_int, ec.SECP256R1(), default_backend())
# Serialize to PEM format
pem_data = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(pem_file, 'wb') as f:
f.write(pem_data)
print(f'Saved ECDSA key to: {pem_file}')
def save_rsa_private_key(private_key: rsa.RSAPrivateKey, pem_file: str) -> None:
"""
Save RSA private key to PEM file.
Args:
private_key: RSA private key object
pem_file: Path to save the PEM file
"""
pem_data = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
with open(pem_file, 'wb') as f:
f.write(pem_data)
print(f'Saved RSA key to: {pem_file}')
def generate_key_manager_params(init_key: bytes, k2: bytes, rand_num: bytes) -> bytes:
"""
Generate K2 info for Key Manager AES mode deployment.
The K2 info is computed as:
k2_info = AES_ECB(init_key, AES_ECB(rand_num, k2) || rand_num)
"""
temp_result_inner = calculate_aes_ecb_encrypt(k2, rand_num)
k2_info = calculate_aes_ecb_encrypt(temp_result_inner + rand_num, init_key)
return k2_info
def generate_k1_encrypted(k1: bytes, k2: bytes) -> bytes:
"""
Generate encrypted K1 for Key Manager.
K1 is reversed and then encrypted with K2 using AES-ECB.
"""
k1_reversed = k1[::-1]
return calculate_aes_ecb_encrypt(k1_reversed, k2)
def generate_ds_aes_key(hmac_key: bytes) -> bytes:
"""
Generate the AES key used for DS parameter encryption.
The DS AES key is derived from HMAC key: HMAC-SHA256(hmac_key, 0xFF * 32)
"""
return hmac.HMAC(hmac_key, b'\xff' * 32, hashlib.sha256).digest()
def generate_ds_encrypted_params(private_key: rsa.RSAPrivateKey, aes_key: bytes, iv: bytes, max_key_size: int) -> bytes:
"""
Generate encrypted DS parameters (the 'C' blob).
This follows the ESP-IDF DS parameter encryption format.
"""
priv_numbers = private_key.private_numbers()
pub_numbers = private_key.public_key().public_numbers()
Y = priv_numbers.d # Private exponent
M = pub_numbers.n # Modulus
key_size = private_key.key_size
# Calculate RSA helper values
rr = 1 << (key_size * 2)
rinv = rr % M
mprime = -modinv(M, 1 << 32) & 0xFFFFFFFF
length = key_size // 32 - 1
# Calculate MD (message digest) from parameters and IV
md_in = (
number_as_bytes_le(Y, max_key_size)
+ number_as_bytes_le(M, max_key_size)
+ number_as_bytes_le(rinv, max_key_size)
+ struct.pack('<II', mprime, length)
+ iv
)
md = hashlib.sha256(md_in).digest()
# Generate plaintext P for encryption
p = (
number_as_bytes_le(Y, max_key_size)
+ number_as_bytes_le(M, max_key_size)
+ number_as_bytes_le(rinv, max_key_size)
+ md
+ struct.pack('<II', mprime, length)
+ b'\x08' * 8 # Padding
)
# Encrypt P to get C
c = calculate_aes_cbc_encrypt(p, aes_key, iv)
return c
def generate_test_data(
seed: int | None = None,
target: str = DEFAULT_TARGET,
ecdsa_key_file: str | None = None,
rsa_key_file: str | None = None,
save_ecdsa_key_file: str | None = None,
save_rsa_key_file: str | None = None,
) -> dict:
"""
Generate all test data for the Key Manager signing example.
Args:
seed: Random seed for reproducible output (ignored for keys loaded from files)
target: Target chip name (e.g., 'esp32c5', 'esp32p4'). Determines RSA key size.
ecdsa_key_file: Path to ECDSA private key PEM file to load (P-256 curve)
rsa_key_file: Path to RSA private key PEM file to load (must match target's max RSA size)
save_ecdsa_key_file: Path to save generated ECDSA key as PEM file
save_rsa_key_file: Path to save generated RSA key as PEM file
Returns:
Dictionary containing all test data arrays
"""
# Get target-specific RSA key size
rsa_key_size = get_rsa_key_size_for_target(target)
print(f'Target: {target} (RSA key size: {rsa_key_size}-bit)')
urandom: Callable[[int], bytes]
if seed is not None:
import random
random.seed(seed)
# Use seeded random for reproducible output
def seeded_urandom(n: int) -> bytes:
return bytes(random.randint(0, 255) for _ in range(n))
urandom = seeded_urandom
else:
urandom = os.urandom
# Generate base keys for Key Manager AES mode
init_key = urandom(KEY_MGR_SW_INIT_KEY_SIZE)
k2 = urandom(32)
rand_num = urandom(32)
# Generate K2 info
k2_info = generate_key_manager_params(init_key, k2, rand_num)
# ECDSA key: load from PEM file or generate randomly
ecdsa_key_generated = False
if ecdsa_key_file:
print(f'Loading ECDSA key from: {ecdsa_key_file}')
k1_ecdsa = load_ecdsa_private_key(ecdsa_key_file)
else:
# Generate random ECDSA key (P-256, 32 bytes)
k1_ecdsa = urandom(32)
ecdsa_key_generated = True
k1_ecdsa256_encrypt = generate_k1_encrypted(k1_ecdsa, k2)
# Save ECDSA key if generated (not loaded from file)
if save_ecdsa_key_file and ecdsa_key_generated:
save_ecdsa_private_key(k1_ecdsa, save_ecdsa_key_file)
# Generate DS HMAC key and derive AES key
hmac_key_for_ds = urandom(32)
ds_aes_key = generate_ds_aes_key(hmac_key_for_ds)
# Generate K1 for DS (derived from HMAC key)
k1_ds = generate_ds_aes_key(hmac_key_for_ds)
k1_ds_encrypt = generate_k1_encrypted(k1_ds, k2)
# RSA key for DS: load from PEM file or generate randomly
rsa_key_generated = False
if rsa_key_file:
print(f'Loading RSA key from: {rsa_key_file}')
rsa_private_key = load_rsa_private_key(rsa_key_file, rsa_key_size)
else:
# Generate random RSA key with target-specific size
rsa_private_key = rsa.generate_private_key(
public_exponent=65537, key_size=rsa_key_size, backend=default_backend()
)
rsa_key_generated = True
# Save RSA key if generated (not loaded from file)
if save_rsa_key_file and rsa_key_generated:
save_rsa_private_key(rsa_private_key, save_rsa_key_file)
# Generate DS IV and encrypted parameters using target's max key size
ds_iv = urandom(ESP_DS_IV_LEN)
ds_c = generate_ds_encrypted_params(rsa_private_key, ds_aes_key, ds_iv, rsa_key_size)
# Generate a sample SHA-256 hash to sign
sha256_hash = urandom(32)
return {
'init_key': init_key,
'k2_info': k2_info,
'k1_ecdsa256_encrypt': k1_ecdsa256_encrypt,
'k1_ds_aes_key_encrypt': k1_ds_encrypt,
'ds_iv': ds_iv,
'ds_c': ds_c,
'sha256_hash': sha256_hash,
# Keep these for reference/debugging
'_target': target,
'_rsa_key_size': rsa_key_size,
'_k2': k2,
'_rand_num': rand_num,
'_k1_ecdsa': k1_ecdsa,
'_hmac_key_for_ds': hmac_key_for_ds,
'_ecdsa_key_file': ecdsa_key_file,
'_rsa_key_file': rsa_key_file,
'_save_ecdsa_key_file': save_ecdsa_key_file if ecdsa_key_generated else None,
'_save_rsa_key_file': save_rsa_key_file if rsa_key_generated else None,
}
def print_test_data(data: dict) -> None:
"""Print test data in a format suitable for copying into C code."""
target = data.get('_target', DEFAULT_TARGET)
rsa_key_size = data.get('_rsa_key_size', DEFAULT_RSA_KEY_SIZE)
print('/*')
print(' * Auto-generated test data for Key Manager Signing Example')
print(' * Generated by generate_test_data.py')
print(f' * Target: {target} (RSA key size: {rsa_key_size}-bit)')
print(' */')
print()
print('/* SHA-256 hash to sign */')
print(bytes_to_c_array(data['sha256_hash'], 'sha256_hash'))
print()
print('/* Software init key for Key Manager AES mode deployment */')
print(bytes_to_c_array(data['init_key'], 'init_key'))
print()
print('/* K2 info for Key Manager AES mode deployment */')
print(bytes_to_c_array(data['k2_info'], 'k2_info'))
print()
print('#if CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN')
print('/* K1 encrypted for ECDSA P-256 key */')
print(bytes_to_c_array(data['k1_ecdsa256_encrypt'], 'k1_ecdsa256_encrypt'))
print('#endif /* CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN */')
print()
print('#if CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN')
print(f'/* K1 encrypted for DS (AES) key - RSA {rsa_key_size}-bit */')
print(bytes_to_c_array(data['k1_ds_aes_key_encrypt'], 'k1_ds_aes_key_encrypt'))
print()
print(f'static const uint8_t ds_iv[{ESP_DS_IV_LEN}] = {bytes_to_c_array_oneline(data["ds_iv"])};')
print(f'static const uint8_t ds_c[{len(data["ds_c"])}] = {bytes_to_c_array_oneline(data["ds_c"])};')
print('#endif /* CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN */')
def generate_header_file(data: dict, filename: str, seed: int | None = None) -> None:
"""Generate a C header file with the test data."""
target = data.get('_target', DEFAULT_TARGET)
rsa_key_size = data.get('_rsa_key_size', DEFAULT_RSA_KEY_SIZE)
with open(filename, 'w') as f:
f.write('/*\n')
f.write(' * Auto-generated test data for Key Manager Signing Example\n')
f.write(' * Generated by generate_test_data.py\n')
f.write(' *\n')
# Add generation parameters info
gen_info = []
gen_info.append(f'target: {target}')
gen_info.append(f'RSA key size: {rsa_key_size}-bit')
if seed is not None:
gen_info.append(f'seed: {seed}')
if data.get('_ecdsa_key_file'):
gen_info.append(f'ECDSA key loaded from: {data["_ecdsa_key_file"]}')
elif data.get('_save_ecdsa_key_file'):
gen_info.append(f'ECDSA key saved to: {data["_save_ecdsa_key_file"]}')
if data.get('_rsa_key_file'):
gen_info.append(f'RSA key loaded from: {data["_rsa_key_file"]}')
elif data.get('_save_rsa_key_file'):
gen_info.append(f'RSA key saved to: {data["_save_rsa_key_file"]}')
if gen_info:
f.write(' * Generation parameters:\n')
for info in gen_info:
f.write(f' * {info}\n')
f.write(' *\n')
f.write(' * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD\n')
f.write(' * SPDX-License-Identifier: Unlicense OR CC0-1.0\n')
f.write(' */\n\n')
f.write('#pragma once\n\n')
f.write('#include <stdint.h>\n')
f.write('#include "sdkconfig.h"\n')
f.write('#include "esp_ds.h"\n\n')
f.write('/* SHA-256 hash to sign */\n')
f.write(bytes_to_c_array(data['sha256_hash'], 'sha256_hash') + '\n\n')
f.write('/* Software init key for Key Manager AES mode deployment */\n')
f.write(bytes_to_c_array(data['init_key'], 'init_key') + '\n\n')
f.write('/* K2 info for Key Manager AES mode deployment */\n')
f.write(bytes_to_c_array(data['k2_info'], 'k2_info') + '\n\n')
f.write('#if CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN\n')
f.write('/* K1 encrypted for ECDSA P-256 key */\n')
f.write(bytes_to_c_array(data['k1_ecdsa256_encrypt'], 'k1_ecdsa256_encrypt') + '\n')
f.write('#endif /* CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN */\n\n')
f.write('#if CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN\n')
f.write(f'/* K1 encrypted for DS (AES) key - RSA {rsa_key_size}-bit */\n')
f.write(bytes_to_c_array(data['k1_ds_aes_key_encrypt'], 'k1_ds_aes_key_encrypt') + '\n\n')
f.write(f'static const uint8_t ds_iv[ESP_DS_IV_LEN] = {bytes_to_c_array_oneline(data["ds_iv"])};\n')
f.write(f'static const uint8_t ds_c[ESP_DS_C_LEN] = {bytes_to_c_array_oneline(data["ds_c"])};\n')
f.write('#endif /* CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN */\n')
print(f'Generated header file: {filename}')
def main() -> None:
parser = argparse.ArgumentParser(
description='Generate test data for the Key Manager Signing Example',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
'--output-header', type=str, metavar='FILENAME', help='Generate a C header file with the test data'
)
parser.add_argument('--seed', type=int, default=None, help='Random seed for reproducible output')
parser.add_argument(
'--target',
type=str,
default=DEFAULT_TARGET,
choices=list(TARGET_RSA_MAX_BIT_LEN.keys()),
help=f'Target chip (determines RSA key size). Default: {DEFAULT_TARGET}',
)
parser.add_argument(
'--ecdsa-key',
type=str,
metavar='FILE',
help='Path to ECDSA private key PEM file to load (P-256/secp256r1 curve)',
)
parser.add_argument(
'--rsa-key',
type=str,
metavar='FILE',
help='Path to RSA private key PEM file to load (must match target RSA size)',
)
args = parser.parse_args()
# Determine output directory for generated key files
if args.output_header:
output_dir = os.path.dirname(args.output_header)
if not output_dir:
output_dir = '.'
else:
output_dir = '.'
# Set default paths for saving generated keys
save_ecdsa_key = None if args.ecdsa_key else os.path.join(output_dir, 'ecdsa_private_key.pem')
save_rsa_key = None if args.rsa_key else os.path.join(output_dir, 'rsa_private_key.pem')
data = generate_test_data(
seed=args.seed,
target=args.target,
ecdsa_key_file=args.ecdsa_key,
rsa_key_file=args.rsa_key,
save_ecdsa_key_file=save_ecdsa_key,
save_rsa_key_file=save_rsa_key,
)
if args.output_header:
generate_header_file(data, args.output_header, seed=args.seed)
else:
print_test_data(data)
if __name__ == '__main__':
main()
@@ -0,0 +1,3 @@
idf_component_register(SRCS "key_mgr_sign_main.c"
PRIV_REQUIRES esp_security mbedtls nvs_flash
INCLUDE_DIRS ".")
@@ -0,0 +1,22 @@
menu "Key Manager Signing Example Configuration"
config EXAMPLE_KEY_MGR_ECDSA_SIGN
bool "Enable ECDSA signing example"
default y
depends on SOC_KEY_MANAGER_SUPPORTED && SOC_KEY_MANAGER_ECDSA_KEY_DEPLOY
help
Enable the ECDSA signing example using Key Manager deployed key.
This demonstrates deploying an ECDSA private key via Key Manager
and using it for signing operations through the PSA Crypto API.
config EXAMPLE_KEY_MGR_RSA_DS_SIGN
bool "Enable RSA-DS signing example"
default y
depends on SOC_KEY_MANAGER_SUPPORTED && SOC_KEY_MANAGER_DS_KEY_DEPLOY
help
Enable the RSA Digital Signature (DS) signing example using
Key Manager deployed key. This demonstrates deploying a DS key
via Key Manager and using it for RSA signing operations through
the PSA Crypto API.
endmenu
@@ -0,0 +1,543 @@
/*
* Key Manager Signing Example
*
* Demonstrates ECDSA and RSA-DS signing operations using keys deployed
* via the Key Manager peripheral on Key Manager supported targets
*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "soc/soc_caps.h"
#include "psa/crypto.h"
#if SOC_KEY_MANAGER_SUPPORTED
#include "esp_key_mgr.h"
#endif
#if SOC_ECDSA_SUPPORTED
#include "psa_crypto_driver_esp_ecdsa.h"
#endif
#if SOC_DIG_SIGN_SUPPORTED
#include "psa_crypto_driver_esp_rsa_ds.h"
#endif
#define HASH_LEN 32
#define ECDSA_COMPONENT_LEN 32
#define ECDSA_UNCOMPRESSED_POINT_FORMAT 0x04
/*
* Test data generated by generate_test_data.py (seed: 12345)
* See README.md for instructions on regenerating this data.
*/
#include "test_data.h"
#if CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN
/* Use target-specific RSA key size from SOC capabilities */
#define RSA_KEY_BITS SOC_RSA_MAX_BIT_LEN
#define RSA_KEY_BYTES (RSA_KEY_BITS / 8)
/* Convert RSA key bits to esp_digital_signature_length_t enum value */
#define RSA_DS_LENGTH ((RSA_KEY_BITS / 32) - 1)
#endif /* CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN */
static const char *TAG = "key_mgr_sign";
#define NVS_KEY_MGR_NAMESPACE "key_mgr"
#if SOC_KEY_MANAGER_SUPPORTED
/**
* @brief Get NVS key string for a given key type (for "key_type" -> "key_recovery_info" storage)
*/
static const char *key_type_to_nvs_key(esp_key_mgr_key_type_t key_type)
{
switch (key_type) {
case ESP_KEY_MGR_ECDSA_KEY:
return "ecdsa";
case ESP_KEY_MGR_DS_KEY:
return "ds";
default:
return "unknown";
}
}
/**
* @brief Load key recovery info from NVS if present
*
* @param key_type Key type (used as NVS key name)
* @param key_recovery_info Output buffer to fill (must be at least sizeof(esp_key_mgr_key_recovery_info_t))
* @return ESP_OK if loaded, ESP_ERR_NVS_NOT_FOUND if no key stored, or other error
*/
static esp_err_t load_key_recovery_info_from_nvs(esp_key_mgr_key_type_t key_type,
esp_key_mgr_key_recovery_info_t *key_recovery_info)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_KEY_MGR_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
/* ESP_ERR_NVS_NOT_FOUND = namespace "key_mgr" does not exist yet (e.g. after erase_flash) */
if (err != ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to open NVS namespace: 0x%x", err);
}
return err;
}
/* Get the blob size first */
size_t required_size = 0;
err = nvs_get_blob(handle, key_type_to_nvs_key(key_type), NULL, &required_size);
if (err == ESP_ERR_NVS_NOT_FOUND) {
nvs_close(handle);
return ESP_ERR_NVS_NOT_FOUND;
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to get key_recovery_info size from NVS: 0x%x", err);
nvs_close(handle);
return err;
}
if (required_size != sizeof(esp_key_mgr_key_recovery_info_t)) {
ESP_LOGE(TAG, "Invalid key_recovery_info size in NVS (expected %zu, got %zu)",
sizeof(esp_key_mgr_key_recovery_info_t), required_size);
nvs_close(handle);
return ESP_ERR_INVALID_SIZE;
}
err = nvs_get_blob(handle, key_type_to_nvs_key(key_type), key_recovery_info, &required_size);
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read key_recovery_info from NVS: 0x%x", err);
return err;
}
return ESP_OK;
}
/**
* @brief Store key recovery info in NVS under key "key_type" -> blob
*
* @param key_type Key type (used as NVS key name)
* @param key_recovery_info Recovery info to store
* @return ESP_OK on success
*/
static esp_err_t save_key_recovery_info_to_nvs(esp_key_mgr_key_type_t key_type,
const esp_key_mgr_key_recovery_info_t *key_recovery_info)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_KEY_MGR_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS namespace: 0x%x", err);
return err;
}
err = nvs_set_blob(handle, key_type_to_nvs_key(key_type), key_recovery_info,
sizeof(esp_key_mgr_key_recovery_info_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write key_recovery_info to NVS: 0x%x", err);
nvs_close(handle);
return err;
}
err = nvs_commit(handle);
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to commit NVS: 0x%x", err);
return err;
}
ESP_LOGI(TAG, "Stored key_recovery_info in NVS for key_type \"%s\"", key_type_to_nvs_key(key_type));
return ESP_OK;
}
/**
* @brief Deploy a key using Key Manager in AES mode
*
* @param k1_encrypted Pointer to the encrypted K1 key material
* @param k1_encrypted_len Length of k1_encrypted (must be >= KEY_MGR_K1_ENCRYPTED_SIZE)
* @param key_type Type of key to deploy (ECDSA, DS, etc.)
* @param key_len Length of the key (for ECDSA keys)
* @param key_recovery_info Output: Key recovery information for later use
* @return esp_err_t ESP_OK on success
*/
static esp_err_t deploy_key_in_key_manager(const uint8_t *k1_encrypted,
size_t k1_encrypted_len,
esp_key_mgr_key_type_t key_type,
esp_key_mgr_key_len_t key_len,
esp_key_mgr_key_recovery_info_t *key_recovery_info)
{
esp_key_mgr_aes_key_config_t *key_config = NULL;
esp_err_t ret = ESP_FAIL;
/* Verify sizes before memcpy to avoid buffer overflows */
if (sizeof(k2_info) < KEY_MGR_K2_INFO_SIZE) {
ESP_LOGE(TAG, "k2_info size mismatch (need %d, have %u)", KEY_MGR_K2_INFO_SIZE, sizeof(k2_info));
return ESP_ERR_INVALID_SIZE;
}
if (k1_encrypted_len < KEY_MGR_K1_ENCRYPTED_SIZE) {
ESP_LOGE(TAG, "k1_encrypted size too small (need %d, have %u)", KEY_MGR_K1_ENCRYPTED_SIZE, k1_encrypted_len);
return ESP_ERR_INVALID_SIZE;
}
if (sizeof(init_key) < KEY_MGR_SW_INIT_KEY_SIZE) {
ESP_LOGE(TAG, "init_key size mismatch (need %d, have %u)", KEY_MGR_SW_INIT_KEY_SIZE, sizeof(init_key));
return ESP_ERR_INVALID_SIZE;
}
key_config = calloc(1, sizeof(esp_key_mgr_aes_key_config_t));
if (key_config == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for key config");
return ESP_ERR_NO_MEM;
}
key_config->key_type = key_type;
key_config->key_len = key_len;
key_config->use_pre_generated_sw_init_key = 1;
memcpy(key_config->k2_info, (uint8_t *)k2_info, KEY_MGR_K2_INFO_SIZE);
memcpy(key_config->k1_encrypted[0], (uint8_t *)k1_encrypted, KEY_MGR_K1_ENCRYPTED_SIZE);
memcpy(key_config->sw_init_key, (uint8_t *)init_key, KEY_MGR_SW_INIT_KEY_SIZE);
ret = esp_key_mgr_deploy_key_in_aes_mode(key_config, key_recovery_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deploy key in Key Manager: 0x%x", ret);
} else {
ESP_LOGI(TAG, "Key deployed successfully in Key Manager");
}
free(key_config);
return ret;
}
#if CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN
/**
* @brief Demonstrate ECDSA signing using Key Manager deployed key
*
* This function:
* 1. Deploys an ECDSA key using Key Manager
* 2. Creates a PSA opaque key reference
* 3. Signs a hash using psa_sign_hash()
* 4. Verifies the signature using the known ECDSA public key
*/
static void example_ecdsa_sign_with_key_manager(void)
{
ESP_LOGI(TAG, "ECDSA Signing with Key Manager");
esp_err_t ret;
psa_status_t status;
/* Allocate key recovery info */
esp_key_mgr_key_recovery_info_t *key_recovery_info = NULL;
key_recovery_info = calloc(1, sizeof(esp_key_mgr_key_recovery_info_t));
if (key_recovery_info == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for key recovery info");
return;
}
/* Step 1: Use existing key from NVS or deploy new ECDSA key and store in NVS */
ret = load_key_recovery_info_from_nvs(ESP_KEY_MGR_ECDSA_KEY, key_recovery_info);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGI(TAG, "Step 1: No ECDSA key in NVS, deploying new key via Key Manager...");
ret = deploy_key_in_key_manager(k1_ecdsa256_encrypt, sizeof(k1_ecdsa256_encrypt), ESP_KEY_MGR_ECDSA_KEY,
ESP_KEY_MGR_ECDSA_LEN_256, key_recovery_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deploy ECDSA key");
goto cleanup;
}
ret = save_key_recovery_info_to_nvs(ESP_KEY_MGR_ECDSA_KEY, key_recovery_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to store key_recovery_info in NVS");
goto cleanup;
}
} else if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to load key_recovery_info from NVS: 0x%x", ret);
goto cleanup;
} else {
ESP_LOGI(TAG, "Step 1: Using existing ECDSA key from NVS");
}
/* Step 2: Create PSA opaque key reference */
ESP_LOGI(TAG, "Step 2: Creating PSA opaque key reference...");
esp_ecdsa_opaque_key_t opaque_key = {
.curve = ESP_ECDSA_CURVE_SECP256R1,
.key_recovery_info = key_recovery_info,
};
psa_key_id_t priv_key_id = 0;
psa_key_attributes_t priv_attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&priv_attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_bits(&priv_attr, 256);
psa_set_key_usage_flags(&priv_attr, PSA_KEY_USAGE_SIGN_HASH);
psa_set_key_algorithm(&priv_attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
psa_set_key_lifetime(&priv_attr, PSA_KEY_LIFETIME_ESP_ECDSA_VOLATILE);
status = psa_import_key(&priv_attr, (uint8_t *)&opaque_key, sizeof(opaque_key), &priv_key_id);
psa_reset_key_attributes(&priv_attr);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to import opaque key: %d", (int)status);
goto cleanup;
}
/* Step 3: Sign the hash */
ESP_LOGI(TAG, "Step 3: Signing hash using Key Manager key...");
uint8_t signature[2 * ECDSA_COMPONENT_LEN];
size_t signature_len = 0;
status = psa_sign_hash(priv_key_id,
PSA_ALG_ECDSA(PSA_ALG_SHA_256),
sha256_hash, HASH_LEN,
signature, sizeof(signature),
&signature_len);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to sign hash: %d", (int)status);
psa_destroy_key(priv_key_id);
goto cleanup;
}
ESP_LOGI(TAG, "Signature generated successfully (%" PRIu32 " bytes)", (uint32_t)signature_len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, signature, signature_len, ESP_LOG_DEBUG);
/* Cleanup opaque key */
psa_destroy_key(priv_key_id);
/* Step 4: Verify the signature using the known ECDSA public key */
ESP_LOGI(TAG, "Step 4: Verifying signature with known public key...");
psa_key_id_t pub_key_id = 0;
psa_key_attributes_t pub_key_attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&pub_key_attr, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_usage_flags(&pub_key_attr, PSA_KEY_USAGE_VERIFY_HASH);
psa_set_key_algorithm(&pub_key_attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
psa_set_key_lifetime(&pub_key_attr, PSA_KEY_LIFETIME_VOLATILE);
status = psa_import_key(&pub_key_attr, ecdsa_public_key, sizeof(ecdsa_public_key), &pub_key_id);
psa_reset_key_attributes(&pub_key_attr);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to import ECDSA public key: %d", (int)status);
goto cleanup;
}
status = psa_verify_hash(pub_key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256),
sha256_hash, HASH_LEN, signature, signature_len);
psa_destroy_key(pub_key_id);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "ECDSA signature verification failed: %d", (int)status);
goto cleanup;
}
ESP_LOGI(TAG, "ECDSA signature verification PASSED!");
cleanup:
free(key_recovery_info);
ESP_LOGI(TAG, "ECDSA Signing Example Complete\n");
}
#endif /* CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN */
#if CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN
/**
* @brief Demonstrate RSA-DS signing using Key Manager deployed key
*
* This function:
* 1. Deploys a DS (HMAC) key using Key Manager
* 2. Prepares DS data context
* 3. Creates a PSA opaque key reference for RSA-DS
* 4. Signs a hash using psa_sign_hash()
* 5. Verifies the signature using the known RSA public key
*/
static void example_rsa_ds_sign_with_key_manager(void)
{
ESP_LOGI(TAG, "RSA-DS Signing with Key Manager");
esp_err_t ret;
psa_status_t status;
/* Allocate key recovery info */
esp_key_mgr_key_recovery_info_t *key_recovery_info = NULL;
key_recovery_info = calloc(1, sizeof(esp_key_mgr_key_recovery_info_t));
if (key_recovery_info == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for key recovery info");
return;
}
/* Step 1: Use existing key from NVS or deploy new DS key and store in NVS */
ret = load_key_recovery_info_from_nvs(ESP_KEY_MGR_DS_KEY, key_recovery_info);
if (ret == ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGI(TAG, "Step 1: No DS key in NVS, deploying new key via Key Manager...");
ret = deploy_key_in_key_manager(k1_ds_aes_key_encrypt, sizeof(k1_ds_aes_key_encrypt), ESP_KEY_MGR_DS_KEY, 0, key_recovery_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deploy DS key");
goto cleanup;
}
ret = save_key_recovery_info_to_nvs(ESP_KEY_MGR_DS_KEY, key_recovery_info);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to store key_recovery_info in NVS");
goto cleanup;
}
} else if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to load key_recovery_info from NVS: 0x%x", ret);
goto cleanup;
} else {
ESP_LOGI(TAG, "Step 1: Using existing DS key from NVS");
}
/* Step 2: Prepare DS data context */
ESP_LOGI(TAG, "Step 2: Preparing DS data context...");
esp_ds_data_t *ds_data = calloc(1, sizeof(esp_ds_data_t));
if (ds_data == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for DS data");
goto cleanup;
}
ds_data->rsa_length = RSA_DS_LENGTH;
memcpy(ds_data->iv, ds_iv, ESP_DS_IV_LEN);
memcpy(ds_data->c, ds_c, ESP_DS_C_LEN);
esp_ds_data_ctx_t ds_data_ctx = {
.esp_ds_data = ds_data,
.rsa_length_bits = RSA_KEY_BITS,
};
esp_rsa_ds_opaque_key_t rsa_ds_opaque_key = {
.ds_data_ctx = &ds_data_ctx,
.key_recovery_info = key_recovery_info,
};
/* Step 3: Create PSA opaque key reference for RSA-DS */
ESP_LOGI(TAG, "Step 3: Creating PSA opaque key reference for RSA-DS...");
psa_key_id_t ds_key_id = 0;
psa_key_attributes_t ds_key_attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&ds_key_attr, PSA_KEY_TYPE_RSA_KEY_PAIR);
psa_set_key_bits(&ds_key_attr, RSA_KEY_BITS);
psa_set_key_usage_flags(&ds_key_attr, PSA_KEY_USAGE_SIGN_HASH);
psa_set_key_algorithm(&ds_key_attr, PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256));
psa_set_key_lifetime(&ds_key_attr, PSA_KEY_LIFETIME_ESP_RSA_DS_VOLATILE);
status = psa_import_key(&ds_key_attr,
(const uint8_t *)&rsa_ds_opaque_key,
sizeof(rsa_ds_opaque_key),
&ds_key_id);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to import RSA-DS opaque key: %d", (int)status);
free(ds_data);
goto cleanup;
}
/* Step 4: Sign the hash using RSA-DS */
ESP_LOGI(TAG, "Step 4: Signing hash using RSA-DS with Key Manager key...");
uint8_t *rsa_signature = calloc(1, RSA_KEY_BYTES);
if (rsa_signature == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for RSA signature");
psa_destroy_key(ds_key_id);
free(ds_data);
goto cleanup;
}
size_t rsa_signature_len = 0;
status = psa_sign_hash(ds_key_id,
PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256),
sha256_hash, HASH_LEN,
rsa_signature, RSA_KEY_BYTES,
&rsa_signature_len);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to sign hash with RSA-DS: %d", (int)status);
psa_destroy_key(ds_key_id);
psa_reset_key_attributes(&ds_key_attr);
free(rsa_signature);
free(ds_data);
goto cleanup;
}
ESP_LOGI(TAG, "RSA-DS signature generated successfully (%" PRIu32 " bytes)", (uint32_t)rsa_signature_len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, rsa_signature, rsa_signature_len, ESP_LOG_DEBUG);
/* Cleanup DS opaque key */
psa_destroy_key(ds_key_id);
psa_reset_key_attributes(&ds_key_attr);
free(ds_data);
/* Step 5: Verify the signature using the known RSA public key */
ESP_LOGI(TAG, "Step 5: Verifying RSA-DS signature with known public key...");
psa_key_id_t rsa_pub_key_id = 0;
psa_key_attributes_t rsa_pub_key_attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&rsa_pub_key_attr, PSA_KEY_TYPE_RSA_PUBLIC_KEY);
psa_set_key_bits(&rsa_pub_key_attr, RSA_KEY_BITS);
psa_set_key_usage_flags(&rsa_pub_key_attr, PSA_KEY_USAGE_VERIFY_HASH);
psa_set_key_algorithm(&rsa_pub_key_attr, PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256));
psa_set_key_lifetime(&rsa_pub_key_attr, PSA_KEY_LIFETIME_VOLATILE);
status = psa_import_key(&rsa_pub_key_attr, rsa_public_key_der, sizeof(rsa_public_key_der), &rsa_pub_key_id);
psa_reset_key_attributes(&rsa_pub_key_attr);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "Failed to import RSA public key: %d", (int)status);
free(rsa_signature);
goto cleanup;
}
status = psa_verify_hash(rsa_pub_key_id, PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256),
sha256_hash, HASH_LEN, rsa_signature, rsa_signature_len);
psa_destroy_key(rsa_pub_key_id);
free(rsa_signature);
if (status != PSA_SUCCESS) {
ESP_LOGE(TAG, "RSA-DS signature verification failed: %d", (int)status);
goto cleanup;
}
ESP_LOGI(TAG, "RSA-DS signature verification PASSED!");
cleanup:
free(key_recovery_info);
ESP_LOGI(TAG, "RSA-DS Signing Example Complete\n");
}
#endif /* CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN */
#endif /* SOC_KEY_MANAGER_SUPPORTED */
void app_main(void)
{
ESP_LOGI(TAG, "Key Manager Signing Example");
#if !SOC_KEY_MANAGER_SUPPORTED
ESP_LOGE(TAG, "Key Manager is not supported on this chip!");
return;
#endif /* SOC_KEY_MANAGER_SUPPORTED */
/* Initialize NVS (required for storing key_recovery_info) */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
#if CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN
example_ecdsa_sign_with_key_manager();
#endif
#if CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN
example_rsa_ds_sign_with_key_manager();
#endif
ESP_LOGI(TAG, "Key Manager Signing Example finished.");
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
"""
Pytest for Key Manager Signing Example
This test verifies ECDSA and RSA-DS signing operations using keys deployed
via the Key Manager peripheral.
"""
import logging
import os
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32c5'], indirect=['target'])
def test_key_manager(dut: Dut) -> None:
# Log binary size
binary_file = os.path.join(dut.app.binary_path, 'key_manager.bin')
bin_size = os.path.getsize(binary_file)
logging.info(f'key_manager_bin_size: {bin_size // 1024}KB')
# Verify example starts
dut.expect('Key Manager Signing Example', timeout=30)
# Test ECDSA signing flow
dut.expect('ECDSA Signing with Key Manager', timeout=30)
match = dut.expect(
r'No ECDSA key in NVS, deploying new key via Key Manager|Using existing ECDSA key from NVS', timeout=30
)
if b'No ECDSA key' in match.group(0):
dut.expect('Key deployed successfully in Key Manager', timeout=30)
dut.expect('Step 2: Creating PSA opaque key reference', timeout=30)
dut.expect('Step 3: Signing hash using Key Manager key', timeout=30)
dut.expect(r'Signature generated successfully \(\d+ bytes\)', timeout=30)
dut.expect('Step 4: Verifying signature with known public key', timeout=30)
dut.expect('ECDSA signature verification PASSED!', timeout=30)
dut.expect('ECDSA Signing Example Complete', timeout=30)
# Test RSA-DS signing flow
dut.expect('RSA-DS Signing with Key Manager', timeout=30)
match = dut.expect(
r'No DS key in NVS, deploying new key via Key Manager|Using existing DS key from NVS', timeout=30
)
if b'No DS key' in match.group(0):
dut.expect('Key deployed successfully in Key Manager', timeout=30)
dut.expect('Step 2: Preparing DS data context', timeout=30)
dut.expect('Step 3: Creating PSA opaque key reference for RSA-DS', timeout=30)
dut.expect('Step 4: Signing hash using RSA-DS with Key Manager key', timeout=30)
dut.expect(r'RSA-DS signature generated successfully \(\d+ bytes\)', timeout=30)
dut.expect('RSA-DS Signing Example Complete', timeout=30)
# Verify example completes
dut.expect('Key Manager Signing Example finished.', timeout=30)
@@ -0,0 +1,10 @@
# Enable hardware ECDSA signing
CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN=y
CONFIG_MBEDTLS_HARDWARE_ECDSA_VERIFY=y
# Enable DS peripheral support
CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL=y
# Enable Key Manager signing examples
CONFIG_EXAMPLE_KEY_MGR_ECDSA_SIGN=y
CONFIG_EXAMPLE_KEY_MGR_RSA_DS_SIGN=y
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIP1eMYfPIevVXXxeDb6pWspbW3/T1aY9IutjZ0V3qZVAoAoGCCqGSM49
AwEHoUQDQgAEqyx6By5PjN+g3CL6mcKBbuo7nHcMAZKgwEvoRb7ixz729xEd7Ud+
InETrZCYY2czLKnTqCD/FekPOfET1D0wtw==
-----END EC PRIVATE KEY-----
@@ -0,0 +1,39 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----