da14531: port da14531_protocol_format to Rust
What changed, and why it matters
This commit rewrites a low-level serial-link framing and CRC routine from C into Rust for the BitBox02 hardware wallet's Bluetooth companion-chip (DA14531) communication. It removes a hand-generated C CRC implementation and uses a standard Rust `crc` crate instead. The change is a refactor/port, not a fix for a known vulnerability. There is no direct evidence in the commit or supplied references that this change addresses a security bug.
Treat as a normal refactor. Verify that the Rust CRC output matches the legacy pycrc output across all edge cases (empty input, maximum-length payloads, payloads containing SOF/escape bytes), and confirm the new `crc` crate version is pinned and audited as part of the existing workspace dependency review. No urgent security action is indicated by this commit alone.
Security signals we found
Removal of hand-generated C CRC code reduces risk of subtle implementation bugs in cryptographic-adjacent checksum logic.
Introduction of a new external Rust dependency (`crc` crate) adds supply-chain/dependency risk, though the crate is already used elsewhere in the workspace.
New unsafe FFI boundary (`rust_util_bytes`, `rust_util_bytes_mut`) between C and Rust; correctness depends on the `Bytes`/`BytesMut` wrappers.
No bounds-checking regression observed: the Rust port keeps the same `assert!`/`ASSERT` patterns as the C code.
Evidence from the diff
The patch ports da14531_protocol_format and the CRC helper from src/da14531/crc.c/crc.h to a new Rust crate bitbox-framed-serial-link. The C code is replaced by rust_da14531_protocol_format and rust_da14531_crc, which use crc::Crc::<u16>::new(&crc::CRC_16_ARC) (CRC-16/ARC, equivalent to the previous pycrc configuration: poly 0x8005, init 0, reflect in/out). The framing/escaping logic is preserved byte-for-byte. Existing C unit tests are said to still pass. No functional change is intended; this is a language migration and dependency consolidation.
Changed components
BitBox02 firmware DA14531 serial link protocol layersrc/da14531/da14531_protocol.cnew Rust crate src/rust/bitbox-framed-serial-linkRust/C FFI bindings (bitbox02-cbindgen.toml, bitbox02-rust-c)Inspect captured patch +171 / −219
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ab2af1c..c1f5fdf 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -124,7 +124,6 @@ set(QTOUCH-SOURCES ${QTOUCH-SOURCES} PARENT_SCOPE)
# The additional files required for the plus platform
set(PLATFORM-BITBOX02-PLUS-SOURCES
- ${CMAKE_SOURCE_DIR}/src/da14531/crc.c
${CMAKE_SOURCE_DIR}/src/da14531/da14531.c
${CMAKE_SOURCE_DIR}/src/da14531/da14531_protocol.c
${CMAKE_SOURCE_DIR}/src/da14531/da14531_handler.c
diff --git a/src/da14531/crc.c b/src/da14531/crc.c
deleted file mode 100644
index 5019aac..0000000
--- a/src/da14531/crc.c
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * \file
- * Functions and types for CRC checks.
- *
- * Generated on Mon Mar 3 11:14:10 2025
- * by pycrc v0.10.0, https://pycrc.org
- * using the configuration:
- * - Width = 16
- * - Poly = 0x8005
- * - XorIn = 0x0000
- * - ReflectIn = True
- * - XorOut = 0x0000
- * - ReflectOut = True
- * - Algorithm = bit-by-bit-fast
- */
-#include "crc.h" /* include the header file generated with pycrc */
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdlib.h>
-
-crc_t crc_reflect(crc_t data, size_t data_len)
-{
- unsigned int i;
- crc_t ret;
-
- ret = data & 0x01;
- for (i = 1; i < data_len; i++) {
- data >>= 1;
- ret = (ret << 1) | (data & 0x01);
- }
- return ret;
-}
-
-crc_t crc_update(crc_t crc, const void* data, size_t data_len)
-{
- const unsigned char* d = (const unsigned char*)data;
- unsigned int i;
- crc_t bit;
- unsigned char c;
-
- while (data_len--) {
- c = *d++;
- for (i = 0x01; i & 0xff; i <<= 1) {
- bit = (crc & 0x8000) ^ ((c & i) ? 0x8000 : 0);
- crc <<= 1;
- if (bit) {
- crc ^= 0x8005;
- }
- }
- crc &= 0xffff;
- }
- return crc & 0xffff;
-}
diff --git a/src/da14531/crc.h b/src/da14531/crc.h
deleted file mode 100644
index c2b79d2..0000000
--- a/src/da14531/crc.h
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * \file
- * Functions and types for CRC checks.
- *
- * Generated on Mon Mar 3 11:14:13 2025
- * by pycrc v0.10.0, https://pycrc.org
- * using the configuration:
- * - Width = 16
- * - Poly = 0x8005
- * - XorIn = 0x0000
- * - ReflectIn = True
- * - XorOut = 0x0000
- * - ReflectOut = True
- * - Algorithm = bit-by-bit-fast
- *
- * This file defines the functions crc_init(), crc_update() and crc_finalize().
- *
- * The crc_init() function returns the initial \c crc value and must be called
- * before the first call to crc_update().
- * Similarly, the crc_finalize() function must be called after the last call
- * to crc_update(), before the \c crc is being used.
- * is being used.
- *
- * The crc_update() function can be called any number of times (including zero
- * times) in between the crc_init() and crc_finalize() calls.
- *
- * This pseudo-code shows an example usage of the API:
- * \code{.c}
- * crc_t crc;
- * unsigned char data[MAX_DATA_LEN];
- * size_t data_len;
- *
- * crc = crc_init();
- * while ((data_len = read_data(data, MAX_DATA_LEN)) > 0) {
- * crc = crc_update(crc, data, data_len);
- * }
- * crc = crc_finalize(crc);
- * \endcode
- */
-#ifndef CRC_H
-#define CRC_H
-
-#include <stdint.h>
-#include <stdlib.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * The definition of the used algorithm.
- *
- * This is not used anywhere in the generated code, but it may be used by the
- * application code to call algorithm-specific code, if desired.
- */
-#define CRC_ALGO_BIT_BY_BIT_FAST 1
-
-/**
- * The type of the CRC values.
- *
- * This type must be big enough to contain at least 16 bits.
- */
-typedef uint_fast16_t crc_t;
-
-/**
- * Reflect all bits of a \a data word of \a data_len bytes.
- *
- * \param[in] data The data word to be reflected.
- * \param[in] data_len The width of \a data expressed in number of bits.
- * \return The reflected data.
- */
-crc_t crc_reflect(crc_t data, size_t data_len);
-
-/**
- * Calculate the initial crc value.
- *
- * \return The initial crc value.
- */
-static inline crc_t crc_init(void)
-{
- return 0x0000;
-}
-
-/**
- * Update the crc value with new data.
- *
- * \param[in] crc The current crc value.
- * \param[in] data Pointer to a buffer of \a data_len bytes.
- * \param[in] data_len Number of bytes in the \a data buffer.
- * \return The updated crc value.
- */
-crc_t crc_update(crc_t crc, const void* data, size_t data_len);
-
-/**
- * Calculate the final crc value.
- *
- * \param[in] crc The current crc value.
- * \return The final crc value.
- */
-static inline crc_t crc_finalize(crc_t crc)
-{
- return crc_reflect(crc, 16);
-}
-
-#ifdef __cplusplus
-} /* closing brace for extern "C" */
-#endif
-
-#endif /* CRC_H */
diff --git a/src/da14531/da14531_protocol.c b/src/da14531/da14531_protocol.c
index 7a6759a..8480681 100644
--- a/src/da14531/da14531_protocol.c
+++ b/src/da14531/da14531_protocol.c
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include "da14531/da14531_protocol.h"
-#include "crc.h"
#include "da14531/da14531_binary.h"
#include "platform_config.h"
#include "uart.h"
@@ -295,9 +294,7 @@ static struct da14531_protocol_frame* _serial_link_in_poll(
uint16_t crc_frame = *(uint16_t*)&self->frame[3 + len];
// Recalculate CRC
- crc_t crc = crc_init();
- crc = crc_update(crc, &self->frame[0], 3 + len);
- crc = crc_finalize(crc);
+ uint16_t crc = rust_da14531_crc(rust_util_bytes(&self->frame[0], 3 + len));
self->state = SERIAL_LINK_STATE_READING;
self->frame_len = 0;
@@ -314,22 +311,6 @@ static struct da14531_protocol_frame* _serial_link_in_poll(
return NULL;
}
-static void _serial_link_format_byte(uint8_t data, uint8_t* buf, uint16_t buf_len, uint16_t* idx)
-{
- ASSERT(*idx + 2 <= buf_len);
- (void)buf_len;
- switch (data) {
- case SL_SOF:
- case SL_ESCAPE:
- buf[(*idx)++] = SL_ESCAPE;
- buf[(*idx)++] = data ^ SL_XOR;
- break;
- default:
- buf[(*idx)++] = data;
- break;
- }
-}
-
uint16_t da14531_protocol_format(
uint8_t* buf,
uint16_t buf_len,
@@ -337,38 +318,10 @@ uint16_t da14531_protocol_format(
const uint8_t* payload,
uint16_t payload_len)
{
- uint16_t idx = 0;
- crc_t crc = crc_init();
-
- ASSERT(idx < buf_len);
- buf[idx++] = SL_SOF;
-
- crc = crc_update(crc, &type, 1);
- _serial_link_format_byte(type, buf, buf_len, &idx);
-
- uint8_t len = payload_len & 0xff;
- crc = crc_update(crc, &len, 1);
- _serial_link_format_byte(len, buf, buf_len, &idx);
-
- len = (payload_len >> 8) & 0xff;
- crc = crc_update(crc, &len, 1);
- _serial_link_format_byte(len, buf, buf_len, &idx);
-
- for (int i = 0; i < payload_len; i++) {
- _serial_link_format_byte(payload[i], buf, buf_len, &idx);
- }
-
- crc = crc_update(crc, &payload[0], payload_len);
- crc = crc_finalize(crc);
-
- // crc_t is the "fastest" type that holds u16, so can be longer than 2 bytes
- for (unsigned int i = 0; i < sizeof(uint16_t); i++) {
- _serial_link_format_byte(crc & 0xff, buf, buf_len, &idx);
- crc >>= 8;
- }
- ASSERT(idx < buf_len);
- buf[idx++] = SL_SOF;
- return idx;
+ return rust_da14531_protocol_format(
+ rust_util_bytes_mut(buf, buf_len),
+ (ProtocolPacketType)type,
+ rust_util_bytes(payload, payload_len));
}
struct da14531_protocol_frame* da14531_protocol_poll(
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 372c243..18ce88b 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -97,6 +97,15 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "bitbox-framed-serial-link"
+version = "0.1.0"
+dependencies = [
+ "crc",
+ "hex_lit",
+ "util",
+]
+
[[package]]
name = "bitbox02"
version = "0.1.0"
@@ -161,6 +170,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"bitbox-aes",
+ "bitbox-framed-serial-link",
"bitbox02",
"bitbox02-noise",
"bitbox02-rust",
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 6aeca1c..7f5910c 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -5,6 +5,7 @@
members = [
"bitbox02-rust-c",
"bitbox02-rust",
+ "bitbox-framed-serial-link",
"util",
"bitbox02-noise",
"bitbox02",
@@ -40,6 +41,7 @@ keccak = { version = "0.1.4", default-features = false, features = ["no_unroll"]
zeroize = "1.7.0"
futures-lite = { version = "2.6.1", default-features = false }
hex_lit = { version = "0.1.1", default-features = false }
+crc = "3.0.1"
[patch.crates-io]
rtt-target = { git = "https://github.com/probe-rs/rtt-target.git", rev = "117d9519a5d3b1f4bc024bc05f9e3c5dec0a57f5" }
diff --git a/src/rust/bitbox-framed-serial-link/Cargo.toml b/src/rust/bitbox-framed-serial-link/Cargo.toml
new file mode 100644
index 0000000..73e01ea
--- /dev/null
+++ b/src/rust/bitbox-framed-serial-link/Cargo.toml
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "bitbox-framed-serial-link"
+version = "0.1.0"
+authors = ["Shift Crypto AG <support@bitbox.swiss>"]
+edition = "2024"
+license = "Apache-2.0"
+description = "Framed serial link helpers"
+
+[dependencies]
+crc = { workspace = true }
+util = { path = "../util" }
+
+[dev-dependencies]
+hex_lit = { workspace = true }
diff --git a/src/rust/bitbox-framed-serial-link/src/lib.rs b/src/rust/bitbox-framed-serial-link/src/lib.rs
new file mode 100644
index 0000000..49a4019
--- /dev/null
+++ b/src/rust/bitbox-framed-serial-link/src/lib.rs
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+
+use crc::Crc;
+use util::bytes::{Bytes, BytesMut};
+
+const SL_SOF: u8 = 0x7e;
+const SL_ESCAPE: u8 = 0x7d;
+const SL_XOR: u8 = 0x20;
+
+#[repr(u8)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum ProtocolPacketType {
+ /// 0b00101101
+ Ack = 0x2d,
+ /// 0b01011010
+ Nak = 0x5a,
+ /// 0b00111100
+ BleData = 0x3c,
+ /// 0b10110100
+ CtrlData = 0xb4,
+ /// 0b01001011
+ Ping = 0x4b,
+}
+
+fn format_byte(byte: u8, out: &mut [u8], idx: &mut usize) {
+ assert!(*idx + 2 <= out.len());
+ if byte == SL_SOF || byte == SL_ESCAPE {
+ out[*idx] = SL_ESCAPE;
+ out[*idx + 1] = byte ^ SL_XOR;
+ *idx += 2;
+ } else {
+ out[*idx] = byte;
+ *idx += 1;
+ }
+}
+
+/// Formats a packet into buf for sending over serial. Worst case the buf_len needs to fit:
+/// SOF - 1 byte
+/// type - 1 byte
+/// len - 2 bytes
+/// payload - payload_len bytes
+/// CRC - 2 bytes
+/// EOF - 1 byte
+///
+/// Type, len, payload and crc will have some bytes escaped so worst case takes twice the space.
+///
+/// 2 + (1+2+payload_len+2)*2 = 2 + (5+payload_len)*2 = 12 + 2*payload_len
+///
+/// For example, 64 bytes require 140 byte buffer worst case.
+///
+/// Returns number of formatted bytes
+pub fn protocol_format(out: &mut [u8], packet_type: ProtocolPacketType, payload: &[u8]) -> usize {
+ let payload_len = u16::try_from(payload.len()).expect("payload too large");
+ let len_bytes = payload_len.to_le_bytes();
+
+ let mut idx = 0usize;
+ assert!(idx < out.len());
+ out[idx] = SL_SOF;
+ idx += 1;
+
+ format_byte(packet_type as u8, out, &mut idx);
+ format_byte(len_bytes[0], out, &mut idx);
+ format_byte(len_bytes[1], out, &mut idx);
+
+ for &byte in payload {
+ format_byte(byte, out, &mut idx);
+ }
+
+ let crc = Crc::<u16>::new(&crc::CRC_16_ARC);
+ let mut digest = crc.digest();
+ digest.update(&[packet_type as u8]);
+ digest.update(&len_bytes);
+ digest.update(payload);
+ let crc = digest.finalize();
+
+ let crc_bytes = crc.to_le_bytes();
+ format_byte(crc_bytes[0], out, &mut idx);
+ format_byte(crc_bytes[1], out, &mut idx);
+
+ assert!(idx < out.len());
+ out[idx] = SL_SOF;
+ idx += 1;
+
+ idx
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_da14531_protocol_format(
+ mut out: BytesMut,
+ packet_type: ProtocolPacketType,
+ payload: Bytes,
+) -> u16 {
+ let len = protocol_format(out.as_mut(), packet_type, payload.as_ref());
+ len.try_into().unwrap()
+}
+
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_da14531_crc(data: Bytes) -> u16 {
+ Crc::<u16>::new(&crc::CRC_16_ARC).checksum(data.as_ref())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use util::bytes::rust_util_bytes;
+
+ #[test]
+ fn test_rust_da14531_crc_empty() {
+ let data = [];
+ let crc = rust_da14531_crc(unsafe { rust_util_bytes(data.as_ptr(), data.len()) });
+ assert_eq!(crc, 0x0000);
+ }
+
+ #[test]
+ fn test_rust_da14531_crc_known() {
+ let data = *b"123456789";
+ let crc = rust_da14531_crc(unsafe { rust_util_bytes(data.as_ptr(), data.len()) });
+ assert_eq!(crc, 0xbb3d);
+ }
+}
diff --git a/src/rust/bitbox02-cbindgen.toml b/src/rust/bitbox02-cbindgen.toml
index c0b10b4..13e91ef 100644
--- a/src/rust/bitbox02-cbindgen.toml
+++ b/src/rust/bitbox02-cbindgen.toml
@@ -22,10 +22,10 @@ header = '''
parse_deps = true
# ... but only parse these crates.
-include = ["bitbox02-rust", "util", "bitbox-aes"]
+include = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link"]
# also generate bindings from these crates.
-extra_bindings = ["bitbox02-rust", "util", "bitbox-aes"]
+extra_bindings = ["bitbox02-rust", "util", "bitbox-aes", "bitbox-framed-serial-link"]
[export]
# malloc, free declared in bitbox02-rust-c/src/alloc.rs, but does not need to be exported, as it
diff --git a/src/rust/bitbox02-rust-c/Cargo.toml b/src/rust/bitbox02-rust-c/Cargo.toml
index f176934..c0a6756 100644
--- a/src/rust/bitbox02-rust-c/Cargo.toml
+++ b/src/rust/bitbox02-rust-c/Cargo.toml
@@ -14,6 +14,7 @@ bitbox02 = { path = "../bitbox02", optional = true }
bitbox02-noise = { path = "../bitbox02-noise", optional = true }
cortex-m = { workspace = true }
util = { path = "../util" }
+bitbox-framed-serial-link = { path = "../bitbox-framed-serial-link" }
der = { version = "0.7.9", default-features = false, optional = true }
hex = { workspace = true }
sha2 = { workspace = true, optional = true }
diff --git a/src/rust/bitbox02-rust-c/src/lib.rs b/src/rust/bitbox02-rust-c/src/lib.rs
index a7b643e..62d0697 100644
--- a/src/rust/bitbox02-rust-c/src/lib.rs
+++ b/src/rust/bitbox02-rust-c/src/lib.rs
@@ -28,6 +28,9 @@ extern crate bitbox_aes;
))]
extern crate bitbox02_rust;
+// Expose C interface defined in bitbox-framed-serial-link
+extern crate bitbox_framed_serial_link;
+
// Expose C interface defined in util
extern crate util;
diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml
index 4f1e5a5..ddf1f6e 100644
--- a/src/rust/bitbox02-rust/Cargo.toml
+++ b/src/rust/bitbox02-rust/Cargo.toml
@@ -33,7 +33,7 @@ num-traits = { version = "0.2", default-features = false }
bip32-ed25519 = { git = "https://github.com/BitBoxSwiss/rust-bip32-ed25519", tag = "v0.2.1", optional = true }
blake2 = { version = "0.10.6", default-features = false, optional = true }
minicbor = { version = "0.24.0", default-features = false, features = ["alloc"], optional = true }
-crc = { version = "3.0.1", optional = true }
+crc = { workspace = true, optional = true }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["hazmat", "digest"], optional = true }
hmac = { workspace = true }
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index ba0c776..0bc6998 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -204,7 +204,6 @@ const RUSTIFIED_ENUMS: &[&str] = &[
// BITBOX02_SOURCES are only used for native builds (simulator). Avoid cross-target specific files.
const BITBOX02_SOURCES: &[&str] = &[
- "src/da14531/crc.c",
"src/da14531/da14531_handler.c",
"src/da14531/da14531_protocol.c",
"src/da14531/da14531.c",
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index 18646da..73a8941 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -285,6 +285,14 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "bitbox-framed-serial-link"
+version = "0.1.0"
+dependencies = [
+ "crc",
+ "util",
+]
+
[[package]]
name = "bitbox02"
version = "0.1.0"
@@ -346,6 +354,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"bitbox-aes",
+ "bitbox-framed-serial-link",
"bitbox02",
"bitbox02-noise",
"bitbox02-rust",
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.