use Rust hmac/sha256/sha512 over libwally's functions
What changed, and why it matters
This commit swaps the cryptographic hashing and HMAC functions used throughout the BitBox02 firmware from the libwally library to equivalent Rust implementations. The goal is to remove the libwally dependency. The change touches sensitive code paths such as seed stretching, U2F key generation, and secure-chip authorization, but the commit itself does not claim to fix any security bug. The main risk is that any subtle difference in behavior between the old and new implementations could affect how keys are derived or how the device authenticates, though the diff shows no obvious vulnerability.
Treat this as a high-impact refactoring that requires careful review and regression testing. Verify that the new Rust HMAC/SHA implementations produce bit-identical outputs to libwally for all relevant input sizes and edge cases. Audit the unsafe FFI functions for pointer validity, length handling, and zeroization behavior. Run the existing cryptographic test vectors and add new ones covering the replaced call sites before relying on this in production firmware.
Security signals we found
Cryptographic primitive replacement across multiple security-critical subsystems
Removal of error-handling paths that previously aborted or returned errors on libwally failure
New unsafe C FFI functions with raw pointer contracts for key/data/out buffers
Dependency change from libwally to RustCrypto/bitcoin hash crates
No explicit security claim, CVE, or bug disclosure in commit message
Evidence from the diff
The patch replaces calls to libwally’s wally_sha256, wally_sha512, and wally_hmac_sha256/wally_hmac_sha512 with new Rust-based C FFI functions (rust_sha256, rust_hmac_sha256, rust_hmac_sha512) backed by the bitcoin crate’s hash/HMAC types and the RustCrypto sha2/hmac crates. It removes wally_crypto.h includes and the SC_ERR_HASH error code, updates Cargo manifests, and adds an UpdateCore implementation for a custom Sha512 wrapper. The affected code includes password/seed stretching (atecc/optiga/keystore), factory attestation and BLE firmware hashing, U2F key-handle generation and signing, and secure-chip mock KDF routines. No explicit security issue or bug fix is described in the commit message.
Changed components
src/atecc/atecc.csrc/factorysetup.csrc/keystore.csrc/optiga/optiga.csrc/u2f.csrc/rust/bitbox02-rust-c/src/sha2.rssrc/rust/bitbox02/src/lib.rssrc/rust/bitbox02-rust/src/hash.rstest/hardware-mocks/src/mock_securechip.cInspect captured patch +83 / −94
diff --git a/src/atecc/atecc.c b/src/atecc/atecc.c
index 27fa8a2..aedbf84 100644
--- a/src/atecc/atecc.c
+++ b/src/atecc/atecc.c
@@ -16,9 +16,9 @@
#include "hardfault.h"
#include "securechip/securechip.h"
#include <i2c_ecc.h>
+#include <rust/rust.h>
#include <salt.h>
#include <util.h>
-#include <wally_crypto.h>
// disabling some warnings, as it's an external library.
#pragma GCC diagnostic push
@@ -630,15 +630,8 @@ int atecc_stretch_password(const char* password, uint8_t* stretched_out)
password_salted_hashed)) {
return SC_ERR_SALT;
}
- if (wally_hmac_sha256(
- password_salted_hashed,
- sizeof(password_salted_hashed),
- stretched_out,
- 32,
- stretched_out,
- 32) != WALLY_OK) {
- return SC_ERR_HASH;
- }
+ rust_hmac_sha256(
+ password_salted_hashed, sizeof(password_salted_hashed), stretched_out, 32, stretched_out);
return 0;
}
diff --git a/src/factorysetup.c b/src/factorysetup.c
index 2af7fed..204bf00 100644
--- a/src/factorysetup.c
+++ b/src/factorysetup.c
@@ -311,9 +311,7 @@ static void _attestation_sighash(const uint8_t* attestation_device_pubkey, uint8
uint8_t msg[32 + 64];
memory_get_attestation_bootloader_hash(msg);
memcpy(msg + 32, attestation_device_pubkey, 64);
- if (wally_sha256(msg, sizeof(msg), sighash_out, SHA256_LEN) != WALLY_OK) {
- Abort("wally_sha256 failed here");
- }
+ rust_sha256(msg, sizeof(msg), sighash_out);
}
static void _api_msg(const uint8_t* input, size_t in_len, uint8_t* output, size_t* output_len)
@@ -469,13 +467,7 @@ static ble_error_code_t _verify_ble(const uint8_t* expected_ble_fw_hash, uint8_t
return BLE_ERR_READ_FW;
}
uint8_t flashed_ble_fw_hash[32] = {0};
- if (wally_sha256(
- flashed_ble_fw,
- flashed_ble_fw_size,
- flashed_ble_fw_hash,
- sizeof(flashed_ble_fw_hash)) != WALLY_OK) {
- Abort("_setup_ble: wally_sha256 failed");
- }
+ rust_sha256(flashed_ble_fw, flashed_ble_fw_size, flashed_ble_fw_hash);
if (flashed_ble_fw_size != da14531_firmware_size()) {
screen_print_debug("_setup_ble: size check failed", 0);
return BLE_ERR_FW_SIZE_MISMATCH;
@@ -503,11 +495,7 @@ static ble_error_code_t _setup_ble(void)
// Compute FW hash.
uint8_t ble_fw_hash[32] = {0};
- if (wally_sha256(
- da14531_firmware_start(), da14531_firmware_size(), ble_fw_hash, sizeof(ble_fw_hash)) !=
- WALLY_OK) {
- Abort("_setup_ble: wally_sha256 failed");
- }
+ rust_sha256(da14531_firmware_start(), da14531_firmware_size(), ble_fw_hash);
if (!MEMEQ(ble_fw_hash, _allowed_ble_fw_hash, 32)) {
return BLE_ERR_FW_NOT_ALLOWED;
diff --git a/src/keystore.c b/src/keystore.c
index e51b5c1..34aa514 100644
--- a/src/keystore.c
+++ b/src/keystore.c
@@ -79,9 +79,7 @@ USE_RESULT static keystore_error_t _stretch_retained_seed_encryption_key(
if (!salt_hash_data(encryption_key, 32, purpose_out, salted_hashed)) {
return KEYSTORE_ERR_SALT;
}
- if (wally_hmac_sha256(salted_hashed, sizeof(salted_hashed), out, 32, out, 32) != WALLY_OK) {
- return KEYSTORE_ERR_HASH;
- }
+ rust_hmac_sha256(salted_hashed, sizeof(salted_hashed), out, 32, out);
return KEYSTORE_OK;
}
@@ -547,10 +545,7 @@ bool keystore_get_u2f_seed(uint8_t* seed_out)
return false;
}
const uint8_t message[] = "u2f";
- if (wally_hmac_sha256(bip39_seed, 64, message, sizeof(message), seed_out, SHA256_LEN) !=
- WALLY_OK) {
- return false;
- }
+ rust_hmac_sha256(bip39_seed, 64, message, sizeof(message), seed_out);
return true;
}
@@ -567,10 +562,7 @@ bool keystore_get_ed25519_seed(uint8_t* seed_out)
// Derive a 64 byte expanded ed25519 private key and put it into seed_out.
memcpy(seed_out, bip39_seed, 64);
do {
- if (wally_hmac_sha512(key, sizeof(key), seed_out, 64, seed_out, 64) != WALLY_OK) {
- util_zero(seed_out, 64);
- return false;
- }
+ rust_hmac_sha512(key, sizeof(key), seed_out, 64, seed_out);
} while (seed_out[31] & 0x20);
seed_out[0] &= 248;
@@ -582,11 +574,7 @@ bool keystore_get_ed25519_seed(uint8_t* seed_out)
message[0] = 0x01;
memcpy(&message[1], bip39_seed, 64);
util_zero(bip39_seed, sizeof(bip39_seed));
- if (wally_hmac_sha256(key, sizeof(key), message, sizeof(message), &seed_out[64], 32) !=
- WALLY_OK) {
- util_zero(message, sizeof(message));
- return false;
- }
+ rust_hmac_sha256(key, sizeof(key), message, sizeof(message), &seed_out[64]);
util_zero(message, sizeof(message));
return true;
}
diff --git a/src/optiga/optiga.c b/src/optiga/optiga.c
index 4e38c1a..a4b6869 100644
--- a/src/optiga/optiga.c
+++ b/src/optiga/optiga.c
@@ -27,7 +27,6 @@
#include <salt.h>
#include <securechip/securechip.h>
#include <util.h>
-#include <wally_crypto.h>
// Set this to 1 for a more convenience during development.
// Factory setup will be performed in the normal firmware, which makes it easier to tinker with the
@@ -735,11 +734,7 @@ static int _authorize(uint16_t oid_auth, const uint8_t* auth_secret, size_t auth
}
uint8_t hmac[32] = {0};
- if (wally_hmac_sha256(
- auth_secret, auth_secret_len, random_data, sizeof(random_data), hmac, sizeof(hmac)) !=
- WALLY_OK) {
- return 1;
- }
+ rust_hmac_sha256(auth_secret, auth_secret_len, random_data, sizeof(random_data), hmac);
res = _optiga_crypt_hmac_verify_sync(
_crypt,
OPTIGA_HMAC_SHA_256,
@@ -1711,11 +1706,7 @@ int optiga_stretch_password(const char* password, uint8_t* stretched_out)
return res;
}
- if (wally_hmac_sha256(
- password_secret, sizeof(password_secret), stretched_out, 32, stretched_out, 32) !=
- WALLY_OK) {
- return SC_ERR_HASH;
- }
+ rust_hmac_sha256(password_secret, sizeof(password_secret), stretched_out, 32, stretched_out);
if (!salt_hash_data(
(const uint8_t*)password,
@@ -1724,15 +1715,8 @@ int optiga_stretch_password(const char* password, uint8_t* stretched_out)
password_salted_hashed)) {
return SC_ERR_SALT;
}
- if (wally_hmac_sha256(
- password_salted_hashed,
- sizeof(password_salted_hashed),
- stretched_out,
- 32,
- stretched_out,
- 32) != WALLY_OK) {
- return SC_ERR_HASH;
- }
+ rust_hmac_sha256(
+ password_salted_hashed, sizeof(password_salted_hashed), stretched_out, 32, stretched_out);
return 0;
}
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 3aa4373..8db59b3 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -164,6 +164,7 @@ dependencies = [
"bitbox02-rust",
"bitcoin",
"der",
+ "digest",
"hex",
"p256",
"sha2",
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 30d07bf..f844e0b 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -41,6 +41,8 @@ num-bigint = { version = "0.4.6", default-features = false }
# be a backend config option. See https://github.com/RustCrypto/hashes/pull/686.
sha2 = { version = "0.10.9", default-features = false, features = ["force-soft-compact"] }
sha3 = { version = "0.10.8", default-features = false }
+digest = "0.10.6"
+hmac = { version = "0.12.1", default-features = false, features = ["reset"] }
# We don't rely on this dep directly, the sha3 dep does. We list it here to enable the
# no_unroll feature to reduce the binary size, saving around 1528 bytes (as measured at time of
# writing, this might fluctuate over time).
diff --git a/src/rust/bitbox02-rust-c/Cargo.toml b/src/rust/bitbox02-rust-c/Cargo.toml
index 1074d8d..991071d 100644
--- a/src/rust/bitbox02-rust-c/Cargo.toml
+++ b/src/rust/bitbox02-rust-c/Cargo.toml
@@ -33,6 +33,7 @@ p256 = { version = "0.13.2", default-features = false, features = ["arithmetic",
der = { version = "0.7.9", default-features = false, optional = true }
hex = { workspace = true }
sha2 = { workspace = true, optional = true }
+digest = { workspace = true, optional = true }
bitcoin = { workspace = true, optional = true }
bip39 = { workspace = true }
zeroize = { workspace = true }
@@ -76,7 +77,7 @@ platform-bitbox02 = []
platform-bitbox02plus = ["sha2"]
bootloader = []
-firmware = ["bitbox02-rust", "bitbox02", "bitbox02-noise", "sha2", "p256", "der"]
+firmware = ["bitbox02-rust", "bitbox02", "bitbox02-noise", "sha2", "dep:bitcoin", "digest", "p256", "der"]
# Only to be enabled in Rust unit tests.
testing = ["bitbox02-rust/testing", "bitbox02/testing"]
diff --git a/src/rust/bitbox02-rust-c/src/sha2.rs b/src/rust/bitbox02-rust-c/src/sha2.rs
index ddd2c5f..abda21e 100644
--- a/src/rust/bitbox02-rust-c/src/sha2.rs
+++ b/src/rust/bitbox02-rust-c/src/sha2.rs
@@ -59,6 +59,50 @@ pub unsafe extern "C" fn rust_sha256(data: *const c_void, len: usize, out: *mut
out.copy_from_slice(&hash[..]);
}
+/// Safety: `key` and `data` must be a valid buffers of the corresponding sizes. `out` must be 32
+/// bytes long.
+#[cfg(feature = "firmware")]
+#[no_mangle]
+pub unsafe extern "C" fn rust_hmac_sha256(
+ key: *const c_void,
+ key_len: usize,
+ data: *const c_void,
+ data_len: usize,
+ out: *mut c_uchar,
+) {
+ let out = core::slice::from_raw_parts_mut(out, 32);
+ let key = core::slice::from_raw_parts(key as *const u8, key_len);
+ let data = core::slice::from_raw_parts(data as *const u8, data_len);
+
+ use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
+ let mut engine = HmacEngine::<sha256::Hash>::new(key);
+ engine.input(data);
+ let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
+ out.as_mut().copy_from_slice(hmac_result.as_byte_array());
+}
+
+/// Safety: `key` and `data` must be a valid buffers of the corresponding sizes. `out` must be 64
+/// bytes long.
+#[cfg(feature = "firmware")]
+#[no_mangle]
+pub unsafe extern "C" fn rust_hmac_sha512(
+ key: *const c_void,
+ key_len: usize,
+ data: *const c_void,
+ data_len: usize,
+ out: *mut c_uchar,
+) {
+ let out = core::slice::from_raw_parts_mut(out, 64);
+ let key = core::slice::from_raw_parts(key as *const u8, key_len);
+ let data = core::slice::from_raw_parts(data as *const u8, data_len);
+
+ use bitcoin::hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine};
+ let mut engine = HmacEngine::<sha512::Hash>::new(key);
+ engine.input(data);
+ let hmac_result: Hmac<sha512::Hash> = Hmac::from_engine(engine);
+ out.as_mut().copy_from_slice(hmac_result.as_byte_array());
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml
index 6a64f52..17c6548 100644
--- a/src/rust/bitbox02-rust/Cargo.toml
+++ b/src/rust/bitbox02-rust/Cargo.toml
@@ -37,7 +37,7 @@ hex = { workspace = true }
sha2 = { workspace = true }
sha3 = { workspace = true, optional = true }
keccak = { workspace = true, optional = true }
-digest = "0.10.6"
+digest = { workspace = true }
zeroize = { workspace = true }
num-bigint = { workspace = true, optional = true }
num-traits = { version = "0.2", default-features = false }
@@ -48,7 +48,7 @@ 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 }
ed25519-dalek = { version = "2.1.1", default-features = false, features = ["hazmat", "digest"], optional = true }
-hmac = { version = "0.12.1", default-features = false, features = ["reset"] }
+hmac = { workspace = true }
miniscript = { version = "12.2.0", default-features = false, features = ["no-std"], optional = true }
bitcoin = { workspace = true }
diff --git a/src/rust/bitbox02-rust/src/hash.rs b/src/rust/bitbox02-rust/src/hash.rs
index 2495a98..1ac4069 100644
--- a/src/rust/bitbox02-rust/src/hash.rs
+++ b/src/rust/bitbox02-rust/src/hash.rs
@@ -54,3 +54,10 @@ impl digest::Reset for Sha512 {
impl digest::core_api::BlockSizeUser for Sha512 {
type BlockSize = digest::typenum::U128;
}
+
+impl digest::core_api::UpdateCore for Sha512 {
+ fn update_blocks(&mut self, blocks: &[digest::core_api::Block<Self>]) {
+ self.message
+ .extend(blocks.iter().flat_map(|b| b.iter().copied()))
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/lib.rs b/src/rust/bitbox02-rust/src/lib.rs
index f7931b9..240998e 100644
--- a/src/rust/bitbox02-rust/src/lib.rs
+++ b/src/rust/bitbox02-rust/src/lib.rs
@@ -33,7 +33,7 @@ pub mod bb02_async;
mod bip32;
pub mod bip39;
pub mod hal;
-mod hash;
+pub mod hash;
pub mod hww;
pub mod keystore;
mod version;
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index 95101de..a00db0d 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -159,7 +159,6 @@ const ALLOWLIST_FNS: &[&str] = &[
"util_format_datetime",
"wally_free_string",
"wally_get_secp_context",
- "wally_sha512",
"communication_mode_ble_enabled",
];
diff --git a/src/rust/bitbox02/src/lib.rs b/src/rust/bitbox02/src/lib.rs
index c383a59..4684ddd 100644
--- a/src/rust/bitbox02/src/lib.rs
+++ b/src/rust/bitbox02/src/lib.rs
@@ -242,16 +242,8 @@ pub fn println_stdout(msg: &str) {
}
pub fn sha512(msg: &[u8]) -> [u8; 64] {
- let mut result = [0u8; 64];
- unsafe {
- bitbox02_sys::wally_sha512(
- msg.as_ptr(),
- msg.len() as _,
- result.as_mut_ptr(),
- result.len() as _,
- );
- }
- result
+ use bitcoin::hashes::Hash;
+ bitcoin::hashes::sha512::Hash::hash(msg).to_byte_array()
}
#[cfg(not(feature = "testing"))]
diff --git a/src/securechip/securechip.h b/src/securechip/securechip.h
index 9e34768..2188eeb 100644
--- a/src/securechip/securechip.h
+++ b/src/securechip/securechip.h
@@ -28,7 +28,6 @@ typedef enum {
SC_ERR_INVALID_ARGS = -2,
SC_ERR_CONFIG_MISMATCH = -3,
SC_ERR_SALT = -4,
- SC_ERR_HASH = -5,
// Currently only used by Optiga, but it is in the common errors so that the API of the
// securechip is consistent and the caller does not need to distinguish between the chips at the
// callsite.
diff --git a/src/u2f.c b/src/u2f.c
index 0cdd4da..b58bd51 100644
--- a/src/u2f.c
+++ b/src/u2f.c
@@ -263,19 +263,11 @@ USE_RESULT static bool _keyhandle_gen(
// Concatenate AppId and Nonce as input for the first HMAC round
memcpy(hmac_in, appId, U2F_APPID_SIZE);
memcpy(hmac_in + U2F_APPID_SIZE, nonce, U2F_NONCE_LENGTH);
- int res = wally_hmac_sha256(
- seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), privkey, HMAC_SHA256_LEN);
- if (res != WALLY_OK) {
- return false;
- }
+ rust_hmac_sha256(seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), privkey);
// Concatenate AppId and privkey for the second HMAC round
memcpy(hmac_in + U2F_APPID_SIZE, privkey, HMAC_SHA256_LEN);
- res = wally_hmac_sha256(
- seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), mac, HMAC_SHA256_LEN);
- if (res != WALLY_OK) {
- return false;
- }
+ rust_hmac_sha256(seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), mac);
return true;
}
@@ -475,7 +467,7 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet)
memcpy(sig_base.pubKey, &response->pubKey, U2F_EC_POINT_SIZE);
uint8_t hash[SHA256_LEN] = {0};
- wally_sha256((uint8_t*)&sig_base, sizeof(sig_base), hash, SHA256_LEN);
+ rust_sha256((uint8_t*)&sig_base, sizeof(sig_base), hash);
rust_p256_sign(
rust_util_bytes(U2F_ATT_PRIV_KEY, sizeof(U2F_ATT_PRIV_KEY)),
@@ -666,7 +658,7 @@ static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet)
memcpy(sig_base.challenge, auth_request->challenge, U2F_NONCE_LENGTH);
uint8_t hash[SHA256_LEN] = {0};
- wally_sha256((uint8_t*)&sig_base, sizeof(sig_base), hash, SHA256_LEN);
+ rust_sha256((uint8_t*)&sig_base, sizeof(sig_base), hash);
rust_p256_sign(
rust_util_bytes(privkey, sizeof(privkey)),
diff --git a/test/hardware-mocks/src/mock_securechip.c b/test/hardware-mocks/src/mock_securechip.c
index 1a5e141..9dc9ed3 100644
--- a/test/hardware-mocks/src/mock_securechip.c
+++ b/test/hardware-mocks/src/mock_securechip.c
@@ -18,11 +18,11 @@
#include <stddef.h>
#include <cmocka.h>
+#include <rust/rust.h>
#include <salt.h>
#include <securechip/securechip.h>
#include <stdio.h>
#include <string.h>
-#include <wally_crypto.h>
static uint32_t _u2f_counter;
@@ -38,12 +38,12 @@ static const uint8_t _kdfkey[32] =
int securechip_kdf(const uint8_t* msg, size_t len, uint8_t* kdf_out)
{
- wally_hmac_sha256(_kdfkey, 32, msg, len, kdf_out, 32);
+ rust_hmac_sha256(_kdfkey, 32, msg, len, kdf_out);
return 0;
}
int securechip_kdf_rollkey(const uint8_t* msg, size_t len, uint8_t* kdf_out)
{
- wally_hmac_sha256(_rollkey, 32, msg, len, kdf_out, 32);
+ rust_hmac_sha256(_rollkey, 32, msg, len, kdf_out);
return 0;
}
int securechip_init_new_password(const char* password)
@@ -54,8 +54,7 @@ int securechip_init_new_password(const char* password)
int securechip_stretch_password(const char* password, uint8_t* stretched_out)
{
uint8_t key[9] = "unit-test";
- wally_hmac_sha256(
- key, sizeof(key), (const uint8_t*)password, strlen(password), stretched_out, 32);
+ rust_hmac_sha256(key, sizeof(key), (const uint8_t*)password, strlen(password), stretched_out);
return 0;
}
bool securechip_u2f_counter_set(uint32_t counter)
Why this scored 32/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.