Testing
Overview
libedhoc uses a 3-tier testing architecture to ensure correctness, robustness, and protocol compliance:
Unit tests — Test individual functions and modules in isolation with mocked dependencies.
Integration tests — Exercise full EDHOC handshake flows and protocol message composition/processing against RFC test vectors.
Fuzz tests — LibFuzzer-based targets that stress message parsing and processing with random inputs.
Test Architecture
Directory layout:
tests/
├── unit/ # Unit tests
├── integration/ # Integration tests
├── common/ # Shared test helpers
│ ├── include/
│ └── src/
├── fuzz/ # LibFuzzer fuzz targets
├── test_main.c # Test runner
Naming Convention
Files: test_<subject>.c
TEST_GROUP: Matches the file subject (e.g., test_api.c → TEST_GROUP(api))
TEST cases: Descriptive snake_case (e.g., TEST(api, context_init))
Test groups by tier:
- Unit tests:
crypto_suite0— EDHOC cipher suite 0 (EdDSA, ECDH, HKDF, AEAD, HASH)crypto_suite2— EDHOC cipher suite 2 (ECDSA via hash-then-sign, ECDH, HKDF, AEAD, HASH)api— EDHOC public API (context init, methods, cipher suites, bindings)api_negative— Negative API tests (null args, invalid state, error paths)error_message— EDHOC error message compose/process (success, unspecified, wrong suite, unknown cred)exporters— PRK exporter, OSCORE session export, key updatehelpers— Connection ID, flow prepend/extract, CoAP transport helperscoverage— Mock-based failure injection for deep internal error pathsinternals— Internal function testing via test hooks (LIBEDHOC_TEST_HOOKS)message_paths— Message composition/processing round-trips with real crypto
- Integration tests:
rfc9529_chapter2— RFC 9529 Ch.2 vectors (signatures, x5t), message 1–4, handshake, EADrfc9529_chapter3— RFC 9529 Ch.3 vectors (static DH, kid), message 1–4, handshakerfc9528_negotiation— Cipher suite negotiation (RFC 9528 Ch.6.3.2)handshake_x5chain_sig_suite0— Full handshake, x5chain, signatures, suite 0handshake_x5chain_sig_suite2— Full handshake, x5chain, signatures, suite 2handshake_x5chain_dh_suite2— Full handshake, x5chain, static DH, suite 2handshake_x5t_sig_suite2— Full handshake, x5t, signatures, suite 2handshake_auth_methods— Handshake with auth methods 1 and 2
- Fuzz targets:
fuzz_message_1_process— EDHOC message 1 processingfuzz_message_2_process— EDHOC message 2 processingfuzz_message_3_process— EDHOC message 3 processingfuzz_message_4_process— EDHOC message 4 processingfuzz_message_error_process— EDHOC error message processing
Test Documentation
Each test uses a structured documentation format in Doxygen-style comments:
@scenario — What is being tested
@env — Test environment / preconditions
@action — The action performed (API call, input, etc.)
@expected — Expected outcome or return value
Example:
/**
* @scenario EDHOC context initialization and deinitialization.
* @env None.
* @action Call edhoc_context_init() on zeroed context, verify is_init,
* then call edhoc_context_deinit().
* @expected Both calls return EDHOC_SUCCESS; ctx.is_init is true after init.
*/
TEST(api, context_init)
Running Tests
The recommended entry point is the unified scripts/ci.sh script.
1. Build and run all tests
./scripts/ci.sh build --gcc
./scripts/ci.sh test
2. Run with coverage
./scripts/ci.sh coverage
# Or open report in browser:
./scripts/ci.sh coverage --open
3. Run with sanitizers (ASan + UBSan) — GCC
./scripts/ci.sh sanitizers asan-ubsan
4. Run with Valgrind
./scripts/ci.sh valgrind
5. Run fuzzing — Clang only (LibFuzzer)
Fuzzing uses Clang’s built-in LibFuzzer; GCC does not ship an equivalent.
./scripts/ci.sh fuzz 60
6. Run full CI pipeline locally
./scripts/ci.sh all
Local Reproducibility
To reproduce CI jobs locally, use the following commands:
CI Job |
Local Command |
|---|---|
GCC + Coverage |
|
Clang |
|
Sanitizers (asan-ubsan) |
|
Valgrind |
|
Static Analysis |
|
Fuzz |
|
Full pipeline |
|
Compiler selection
The ci.sh script selects the compiler automatically based on the task:
Task |
Compiler |
Reason |
|---|---|---|
Build / tests / coverage |
GCC |
Default; |
Clang build |
Clang |
Secondary compiler gate |
ASan + UBSan |
GCC |
Works with both; GCC chosen for consistency |
Fuzz |
Clang |
LibFuzzer is a Clang-only feature |
Static analysis |
Clang |
|
Valgrind |
GCC |
Runtime tool; compiler independent |
Coverage
Coverage is measured with gcov (instrumentation) and lcov (report generation).
The build uses -DLIBEDHOC_ENABLE_COVERAGE=ON to add --coverage flags.
To generate coverage:
./scripts/ci.sh coverage
# Report: build/coverage_html/index.html
Coverage excludes:
externals/tests/backends/cbor/src//usr/*
Codecov.io integration uploads coverage from CI. See codecov.yml and the
codecov/codecov-action step in .github/workflows/ci-linux.yml.
Zephyr Benchmark (native_sim)
A Zephyr benchmark app at sample/benchmark/ runs a full EDHOC handshake
with cipher suite 2 (P-256/ES256) on native_sim. This exercises all library
code paths, producing accurate flash footprint data and per-phase handshake
timing. Build and measure locally (requires west + Zephyr SDK):
west build -b native_sim sample/benchmark -p always
./build/zephyr/zephyr.exe # handshake timing (JSON on stdout)
# Flash footprint by function (from final linked binary):
nm --print-size --size-sort --defined-only build/zephyr/zephyr.exe \
| grep -i edhoc | awk '{printf "%6d %s\n", strtonum("0x"$2), $4}' | sort -rn
# Total flash (single number):
nm --print-size --size-sort --defined-only build/zephyr/zephyr.exe \
| grep -i edhoc \
| awk '{sum += strtonum("0x"$2)} END {printf "libedhoc flash: %d bytes (%.1f KiB)\n", sum, sum/1024}'
Note
Zephyr’s rom_report / ram_report targets require a fully-linked
(non-relocatable) ELF. On native_sim the intermediate zephyr.elf is a
partial link (-r), so those reports show most code as “(hidden)”. The nm
analysis of the final zephyr.exe is authoritative instead.
The CI automatically builds this and uploads flash_report.txt and
benchmark_timing.json as artifacts.
Expected library flash footprint is ~20 KiB (0 bytes static RAM — all
state lives on the stack).
Test Categories
Unit Tests
File |
Group |
Description |
|---|---|---|
|
|
EDHOC public API: context init, methods, cipher suites, connection ID, bindings |
|
|
Negative tests: null pointers, invalid state, error paths for all API functions |
|
|
Cipher suite 0: EdDSA, ECDH, HKDF, AEAD, HASH |
|
|
Cipher suite 2: ECDSA, ECDH, HKDF, AEAD, HASH |
|
|
Error message compose/process: success, unspecified, wrong cipher suite, unknown cred |
|
|
PRK exporter, OSCORE session export, key update, error handling |
|
|
Connection ID equal/prepend/extract, flow prepend/extract, CoAP helpers |
|
|
Mock-based failure injection for deep internal error paths |
|
|
Internal function testing via test hooks (LIBEDHOC_TEST_HOOKS) |
|
|
Message composition/processing round-trips with real crypto |
Integration Tests
File |
Group |
Description |
|---|---|---|
|
|
RFC 9529 Ch.2: signatures, x5t; message 1–4, handshake, PRK exporter, EAD |
|
|
RFC 9529 Ch.3: static DH, kid; message 1–4, handshake |
|
|
RFC 9528 Ch.6.3.2: cipher suite negotiation (single/list) |
|
|
Full handshake, x5chain, signatures, suite 0; 1–2 certs in chain |
|
|
Full handshake, x5chain, signatures, suite 2; single/multiple EAD |
|
|
Full handshake, x5chain, static DH, suite 2; single EAD |
|
|
Full handshake, x5t, signatures, suite 2; cert hashes |
|
|
Handshake with auth methods 1 and 2 |
Fuzz Tests
Target |
Description |
|---|---|
|
Fuzzes EDHOC message 1 processing |
|
Fuzzes EDHOC message 2 processing |
|
Fuzzes EDHOC message 3 processing |
|
Fuzzes EDHOC message 4 processing |
|
Fuzzes EDHOC error message processing |
Static Analysis
Cppcheck
./scripts/ci.sh cppcheck
Runs cppcheck on library/ with --enable=warning,style and include paths for
include/, helpers/include/, backends/cbor/include/.
Clang-tidy
./scripts/ci.sh clang-tidy
CI Integration
The GitHub Actions workflow .github/workflows/ci-linux.yml runs:
GCC + Coverage — GCC build with coverage, tests, lcov, Codecov upload
Clang — Clang + Ninja build and tests
ASan + UBSan — AddressSanitizer + UndefinedBehaviorSanitizer (GCC)
Valgrind — Memcheck and DRD
Static Analysis — cppcheck and clang-tidy
Fuzz — LibFuzzer smoke test (60s per target, Clang)
Benchmark — Binary size analysis and handshake timing (Release, GCC)
The unified scripts/ci.sh script can replicate most of this locally with
./scripts/ci.sh all.