Internally, Heimlig works by processing requests for cryptographic work and returning corresponding responses. Every request must be answered by exactly one response that contains the result or an error. These request-response pairs are exchanged between three main component groups:
- The HSM API running on the host cores where client applications are located.
- The Heimlig core running on the HSM core that handles incoming requests and outgoing responses.
- The crypto workers that do the actual cryptographic work. Both hardware-specific workers and software-only workers that run on the HSM core are possible.
These component groups are connected with hardware-specific queues to communicate with one another.
The flow of request-response pairs is as follows:
- A client application running on a host core calls the HSM API to do some cryptographic work.
- The host-side API generates a request message and adds it to the queue leading to the Heimlig core.
- The core receives the request, checks it for validity and forwards it to the corresponding crypto worker via the queue to that worker.
- The crypto worker accepts the request, processes the desired cryptographic operation and returns it to the core.
- The core receives the response and forwards it to the client that sent the original request.
- The host-side API receives the response and either copies the received data to the application or returns an error to it.
Heimlig is a no_std
crate, meaning it
uses neither the heap nor the Rust standard library. For stack allocations, Heimlig requires an
exclusive protected memory region.
For data that is referenced in requests and responses, memory is usually allocated in a shared memory region that is visible from both the host and the HSM cores. To prevent data races between these cores, allocating and deallocating are done by the host only. This means that the memory that might be required by a non-error response is already allocated when the request is generated.
If a client calls the API on the host, the API allocates memory from the shared memory region and copies data from the client to it. References to the allocated memory are then handed over to the Heimlig core as part of the crypto request. Until the corresponding response arrives, the allocated memory is under exclusive control of the HSM core. Once the response does arrive, the results are copied back to the client and the memory is deallocated.
Heimlig uses the standard Rust async/await mechanism to efficiently process incoming requests. This
means that both the core as well as each individual crypto worker can run in separate system tasks.
For example,
embassy_executor::task
can be used for this purpose. Synchronization between different tasks is done by communicating via
queues like
embassy_sync::channel
or
heapless::spsc::Queue
.
As a low-level component, Heimlig consists of both hardware-independent and -dependent parts. Where possible, interfacing with hardware-dependent parts is modelled with traits that then have to be implemented by an integrator. Alternatively, an implementation from the examples or from an external crate such as Embassy can be used.
Hardware-dependent components of Heimlig include:
- Async-executor and tasks (e.g. embassy-executor)
- Queues to transmit requests and responses (e.g. embassy-sync or heapless)