The problem
Classic CAN has no authentication: any node can send any ID, so a compromised device can forge battery or throttle messages. Secure onboard communication schemes add a short MAC and a freshness value to protect important frames, but they must fit in 8 bytes and run fast on small controllers.
Objectives
- Design a secured frame layout with data, freshness counter and truncated MAC within 8 bytes.
- Implement AES-128-CMAC in software on the STM32 and verify it against test vectors.
- Authenticate a BMS status message and reject forged and replayed frames.
- Measure computation time and the effect on bus load.
What you need
From the EV Embedded Development Kit
- STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
- CAN bus interface
- 1S battery with connector and T-sense input
- SPI flash memory
- 16×2 LCD
- RGB LEDs and a buzzer
- Serial UART interface
Software
- STM32CubeIDE
- Embedded C with HAL drivers
- A small, well-tested open-source AES-128 implementation in C
- Python with python-can and the cryptography package (CMAC)
Extra parts
- A USB-CAN adapter — The PC acts as the verifying receiver and as the attacker that sends forged and replayed frames; the kit has one CAN node.
This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.
Key concepts
- Message authentication code (MAC)
- A short tag computed from the message and a secret key. Without the key, an attacker cannot produce a valid tag for a changed message.
- CMAC
- A MAC built on a block cipher such as AES-128, standardised in NIST SP 800-38B. Its 128-bit output can be truncated, with security falling as the tag gets shorter.
- Freshness counter
- A value that increases with every message and is included in the MAC, so a recorded old frame is rejected when replayed.
- Truncation trade-off
- With a 32-bit tag an attacker guessing at random succeeds with probability 1 in about 4.3 billion per attempt; shorter tags make guessing easier, so receivers should also limit failures.
Step-by-step guide
- 1
Design the secured frame
Choose one message to protect, such as a BMS status frame with cell voltage (2 bytes) and temperature (1 byte). Lay out 8 bytes: 3 bytes data, 1 byte low bits of the freshness counter and 4 bytes truncated CMAC. Compute the MAC over the CAN ID, the data and the full 32-bit counter, with only the low bits sent. Write down the threat model: an attacker can read, inject and replay frames but does not know the key.
Checkpoint: A frame layout and threat model document reviewed by your guide.
- 2
Configure CAN and the board
Turn on the DIP switches for CAN, battery, SPI flash, LCD, LEDs and UART. Configure bxCAN at 500 kbit/s, ADC for voltage and T-sense, SPI for the flash and a UART for debug. Connect the USB-CAN adapter with 120 ohm termination at both ends. Check cell polarity and never short the terminals. Measure the bus load and timing of the plain frames now, since they are the baseline for the overhead comparison later.
Checkpoint: Plain BMS status frames are received on the PC at 10 Hz.
- 3
Implement and verify AES-CMAC
Add a small, well-tested open-source AES-128 implementation and write CMAC on top: generate subkeys K1 and K2 from AES of a zero block, pad the last block and chain. Verify against the official NIST SP 800-38B example vectors, then against the Python cryptography package on random messages. Measure cycles per MAC with the DWT counter. Keep the AES code unchanged from its source so it remains easy to review.
Checkpoint: All test vectors pass, and the cycle count per MAC is recorded.
- 4
Handle keys and counters
Store a 128-bit project key in a protected area, explaining that a real ECU would use secure key storage which the F103 lacks; enabling read-out protection is a partial measure. Store the freshness counter in SPI flash, writing it in blocks (for example reserving the next 256 values) so a power cut never reuses a counter and flash wear stays low.
Checkpoint: After a power cycle, the counter always continues above any value previously sent.
- 5
Build the sender
In the 100 ms task, read the sensors, increment the counter, compute the MAC over ID, data and full counter, truncate and pack the frame, then send with HAL_CAN_AddTxMessage. Show the counter and status on the LCD. Make sure the counter is saved to flash in blocks before it passes the reserved range, so a reset can never cause reuse. Add a UART debug mode that prints each frame's full counter and tag so you can compare them with the PC verifier's calculation.
Checkpoint: Secured frames appear on the PC, and the MAC bytes change every frame even when data is unchanged.
- 6
Build the PC verifier
Write a Python receiver that reconstructs the full counter from the transmitted low bits and its last accepted value, recomputes the CMAC with the shared key and accepts the frame only if the tag matches and the counter is newer. Display accepted values and rejections with the reason. Add a resynchronisation rule for a large counter gap, such as a separate sync frame.
Checkpoint: All genuine frames are accepted across several restarts of the kit.
- 7
Run attack tests
From a second script on the PC, send forged frames with altered data and a random MAC, replay recorded genuine frames, and flood random tags. Note that on a shared bus the PC verifier receives frames from both, so test that it accepts only genuine ones. For the reverse direction, also make the kit verify a secured command from the PC and signal rejections with the red LED and buzzer.
Checkpoint: 100% of forged and replayed frames rejected in each test, with logs.
- 8
Measure overheads
Measure MAC computation time on the STM32 and the extra latency from sensor read to frame sent. Calculate the bus load effect: here the data fits in the same single frame, so payload capacity is reduced rather than adding frames. Compare with a design using a second frame for the MAC. Also note how many payload bytes remain for signal data, since that limits which messages can be protected in a single frame.
Checkpoint: A table of computation time, latency and bus load for the secured and plain designs.
- 9
Document the design
Write up the threat model, frame design, CMAC implementation, key and counter handling, tests and overheads. Discuss limitations: shared symmetric keys, no secure key storage on the F103 and a 32-bit tag. Relate the design to how secure onboard communication is done in production. Include the frame layout figure, the test results for every attack and the timing table. Explain how key distribution would be handled in a real vehicle and what changes CAN FD would allow.
Checkpoint: A report and a demo where an attack is shown being rejected.
Testing & validation
- CMAC passes NIST SP 800-38B example vectors and matches the Python library.
- Counter never repeats across power cycles.
- All forged and replayed frames rejected in tests.
- Computation time and latency measured on the target.
Expected results
Authenticated CAN messaging between the kit and a PC with a truncated CMAC and freshness counter, demonstrated rejection of forgery and replay attacks, and measured costs on a Cortex-M3.
Take it further
- Compare CMAC with a lighter MAC such as SipHash or Ascon.
- Protect several messages with different keys and counters.
- Add a key update procedure over UDS-style diagnostics.
- Use a second kit as the verifier instead of the PC.
Viva and review questions
Why include a freshness counter in the MAC?
Without it, a recorded valid frame stays valid forever and can be replayed. Including an increasing counter makes old frames fail verification.
Why only send part of the counter?
To save payload bytes. The receiver rebuilds the full counter from the low bits and its last accepted value, and the MAC is computed over the full value so the shortcut does not weaken it.
Is a 32-bit tag secure enough?
It gives a 1 in about 4.3 billion chance per random guess. Combined with rate limiting and counting failed verifications, it is a common compromise for an 8-byte classic CAN payload.