Hazelnet implements the Client and Server roles of the CAN Bus Security (CBS) protocol, which secures the CAN FD traffic providing message encryption, authenticity, and freshness. CBS focuses on lightweight encryption using symmetric primitives only, multicast communication, and an assurance of freshness of the messages to prevent replay attacks.
The user of the library must handle the physical transmission and reception manually as this library only handles the building of messages to transmit and processing of received messages. This is done to guarantee better portability across systems. The internal library state keeps track of ongoing handshakes, timeouts and other events per each Group.
The library uses standard C11 code and is hardware-independent. The compile targets for a desktop OS add some features like heap memory allocation and use the time and TRNG functionality the OS provides. The any-platform version uses user-provided structs to operate on and requires the user to provide function pointers to custom timestamping and random-number-generators of the used platform; recommended for embedded systems.
Hazelnet depends on other projects from the same author:
- required: LibAscon crypto library (CC0 license).
- for testing only: Atto minimal unit test framework (BSD 3-clause license)
- for config generation only: HzlConfig to write the configuration of the entire bus in one JSON file and generate the binary configurations of each Hazelnet instance (CBS Party).
All dependencies are included in this project as Git Submodules, so be sure
to clone this repository with the --recurse-submodules
option. If you
have already cloned it normally, then run
git submodule update --init --recursive
.
You only need the C99/C11 standard library to compile the library for any platform, including embedded. No heap-allocation required (malloc).
stdint.h
stdbool.h
stddef.h
string.h
On the other hand, when compiling for a desktop OS, additional features are enabled. In particular configurations can be loaded from files, the library context is heap-allocated, and the OS provides the current time and true randomness.
stdlib.h
for heap-allocation (calloc)stdio.h
to access configuration files- On Windows also
sysinfoapi.h
to get the current time in millisecondsbcrypt.h
to get random numbers
- On Unix-like systems also
sys/time.h
to get the current time in milliseconds
The library is not thread safe. All API calls on the same context should be performed within the same thread (or RTOS task) or mutual exclusion locks should be placed by the library user around said API calls to protect the library from race conditions. For the time being, no thread-safe protections have been implemented as they are not easily portable.
- The
inc
folder contains the public headers of the library.hzl.h
is a common header for both the Client and Server. You always need this.hzl_Client.h
andhzl_Server.h
are the API header of the Client and Server libraries (respectively) when compiled for any platform. Pick one of the two roles.hzl_ClientOs.h
andhzl_ServerOs.h
are extensions of the API for the Client and Server libraries (respectively) when compiled for desktop operating systems (assuming a file system and heap-memory allocation).
- The
src
folder contains the library sources:src/common
is code shared between Client and Serversrc/client
andsrc/server
folder contain sources for the respective Parties- The
.c
files are generally named after the user-facing API function they implement.
See it in practice in the Hazelnet Demo Platform showcasing a few microcontrollers exchanging dummy encrypted messages. In particular the
Sources/hzlPlatform_TaskHzl.c
file.
To use the client library, the following headers are required:
#include "hzl.h"
#include "hzl_Client.h"
#include "hzl_ClientOs.h" /* Optional, for a desktop OS only. */
// Load the configuration from a binary file, use the OS for the current time
// and random number generation. A Python package is available in the
// `external/hazelnetconfig` project to generate the config files from a JSON
// file.
hzl_Err_t err;
hzl_ClientCtx* pCtx;
err = hzl_ClientNew(&pCtx, "myconfigfile.hzl");
if (err != HZL_OK) { custom_error_handling(err); }
hzl_CbsPduMsg_t* pPdu; // Packed data, fits into one CAN FD message
// Let's allocate the memory for a message we want to send
err = hzl_ClientNewMsg(&pPdu);
if (err != HZL_OK) { custom_error_handling(err); }
// To start secured communication within a Group, we need to start a handshake
hzl_Gid_t destinationGroupId = 2; // The set of nodes we want to talk to
err = hzl_ClientBuildRequest(pPdu, ctx, destinationGroupId);
if (err != HZL_OK) { custom_error_handling(err); }
// The Request message is built, the user must transmit it manually
myCustomTransmission(pPdu->data, pPdu->dataLen);
// The library takes care of storing the building timestamp, the current status
// and timeout checks. The user must only take care of transmission and
// reception.
// After some time, the Server will transmit a Response. To process received
// messages (often in a separate thread/task), do as follows:
hzl_RxSduMsg_t userData;
err = hzl_ClientProcessReceived(
pPdu, // automatic internal reaction message, if required
&userData, // decrypted user-data, if the message had any
pCtx,
receivedPackedData, // the CAN FD payload as received from the layer below
receivedPackedDataLen, // the CAN FD payload length in bytes (CAN DLC)
receivedCanId // the CAN ID of the message had
);
// The error codes are many, including the cases when the message is simply
// ignored or when it contains critical security errors. Be sure to check them!
if (err != HZL_OK) { custom_error_handling(err); }
// Transmit an automatic reaction message
if (pPdu->dataLen > 0) { myCustomTransmission(pPdu->data, pPdu->dataLen); }
// If the Response from the Server was received, we can now build a secured
// data message for the Group that had the handshake completed.
uint8_t secretMessage[] = "hello world";
err = hzl_ClientBuildSecuredFd(pPdu,
pCtx,
secretMessage,
sizeof(secretMessage),
destinationGroupId);
if (err != HZL_OK) { custom_error_handling(err); }
myCustomTransmission(pPdu->data, pPdu->dataLen);
// At any point in time, unsecured messages may be sent, even for Groups
// that don't have a handshake completed or are not in the configuration.
// USE IT ONLY FOR ALREADY SECURED or NON SENSITIVE DATA.
uint8_t publicMessage[] = "just some debug info";
err = hzl_ClientBuildSecuredFd(pPdu,
pCtx,
publicMessage,
sizeof(publicMessage),
0); // Group 0 is a broadcast
if (err != HZL_OK) { custom_error_handling(err); }
myCustomTransmission(pPdu->data, pPdu->dataLen);
// Free the heap memory when done
hzl_ClientFree(&pCtx);
hzl_ClientFreeMsg(&pPdu);
The usage is the same except compared to the desktop OS case, except for the Context init and deinit as it cannot necessarily happen on the heap or be loaded from a file. The user must prepare it manually:
// Prepare the Context manually.
hzl_Err_t err;
hazelnetGroupStates[10]; // Assuming 10 groups. No need to initialise!
hzl_ClientCtx ctx = {
// Add the pointers to the constant configuration structs (which are NEVER
// written to by the library). E.g. a pointer to the prepared data in the
// persistent flash memory. STRUCTS MUST INCLUDE ANY PADDINGS!
.clientConfig = MY_ADDR_OF_THE_HAZELNET_CONFIG_OF_THIS_CLIENT,
.groupConfigs = MY_ADDR_OF_THE_HAZELNET_GROUP_CONFIGS_OF_THIS_CLIENT,
// and the memory used for the group states, which is initialised
// and manager by the library.
.groupStates = hazelnetGroupStates,
// Finally the timestamping and random number generation functions
// provided as function pointers.
.io = {
.trng = &myPlatformTrueRandomNumberGeneratorWrappedForHazelnet,
.currentTime = &myPlatformCurrentTimeFuncWrappedForHazelnet
}
};
// The init function will run many checks verifying if the configuration is OK.
err = hzl_ClientInit(&ctx); // Instead of ClientNew()
if (err != HZL_OK) { custom_error_handling(err); }
// -----------
// Use the other library functions as in the Desktop OS example above.
// -----------
// When done or before entering sleep-mode/low-power-mode, deinit the context
// to erase security-critical information.
err = hzl_ClientDeInit(&ctx); // Instead of ClientFree()
if (err != HZL_OK) { custom_error_handling(err); }
The Server library has an analogous API to the Client, with the same interface when it comes to processing of the messages. The main difference is that the Server requires an additional configuration field in its context, namely the array of per-Client configurations.
To use the Server library, the following headers are required:
#include "hzl.h"
#include "hzl_Server.h"
#include "hzl_ServerOs.h" /* Optional, for a desktop OS only. */
There are multiple valid ways of doing so. The project is CMake-enabled, so this should be the easiest way. For embedded systems where CMake is not an option, the project may be also compiled using any custom build system.
The dependencies are included in this project as Git Submodules, so be sure
to clone this repository with the --recurse-submodules
option.
git clone --recurse-submodules https://github.com/TheMatjaz/Hazelnet
If you have already cloned the repo, but not the submodules, then run
git submodule init
git submodule update
In the project root folder, run the following to create an out-of-source build:
mkdir build # `build*/` folders are already ignored
cd build
cmake .. # Prepare the makefiles, default to MinSizeRel
cmake --build . --parallel # Equivalent to `make all`
By default, an optimised-for-size release build is performed.
To change it, append the -DCMAKE_BUILD_TYPE=Release
or Debug
to the cmake ..
command and build again.
You may already know this, which makes this section mostly a note for my future self. When compiling from the Windows command line using the MSVC toolchain:
- You will need
the environment variables
to use the MSVC toolchain from the command line
- Hint: search for "x64 Native tools" in the start menu, the first option
- Alternative: open the VS tools installer and click on "Launch"
- Be sure to select the generator NMake Makefiles when configuring CMake
- Use the CMake that comes with MSVC. Mine was installed in
"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
but yours may be different, especially for different VS versions. This is required if you already installed CMake through MSYS.
mkdir build
cd build
"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" .. -G "NMake Makefiles"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" --build .
hzl_client_any
,hzl_server_any
: Client- and Server-side static libraries for any platform, including embedded. No assumptions about the system, no heap memory, no files.hzl_client_desktop
,hzl_server_desktop
: Client- and Server-side static libraries for desktop operating systems, assuming malloc, a file system and using the OS-provided TRNG.hzl_client_desktop_shared
,hzl_server_desktop_shared
: likehzl_client_desktop
andhzl_server_desktop
but shared (dynamic) libraries.
All other targets are internal dependencies or test targets: the user should not worry about them.
-
Include the following directories in the search path for header files (
-Iinc
compiler option for GCC):inc/ src/common/ src/client/ XOR src/server/ -- THEY ARE MUTUALLY EXCLUSIVE external/atto/src/ external/libascon/inc external/libascon/src
For the test suite, also add
tst/
-
Include the following directories in the search path for source files:
src/ external/atto/src/ external/libascon/src/
For the test suite, also add
tst/ tst/client/ XOR src/server/ XOR tst/interop/ -- THEY ARE MUTUALLY EXCLUSIVE
-
Compile with your favourite toolchain. For the test suite, the
tst/(client|server|interop)/hzl(Client|Server|Interop)Test_Main.c
file runs all tests, depending which set of tests you are compiling.
You can run the test suite with ctest
:
ctest --output-on-failure --parallel
or by directly executing the test runner executables:
test_hzl_client_desktop.exe
test_hzl_client_desktop_shared.exe
test_hzl_server_desktop.exe
test_hzl_server_desktop_shared.exe
test_hzl_interop_desktop.exe
test_hzl_interop_desktop_shared.exe
To run the unit tests in an embedded environment, rename the main()
function
of the test runners
tst/(client|server)/hzl(Client|Server)Test_Main.c
into something else (e.g. runAllHazelnetTests()
), integrate it into your
embedded firware and call it from your embedded project. The Atto framework
assumes you have printf
available for the reports, but you can
search-and-replace its instances with another function, if you prefer.
The interop
tests are meant for desktops only, because it does not make
a lot of sense for an embedded device to make an interoperability test
with itself.
Doxygen is build separately (not part of make all
) to avoid running it
every time the library is recompiled during development:
cmake --build . --target hzl_doxygen
- CAN Bus Security protocol specification, protocol version: v1.3, document revision: 4