keystore: port antiklepto protocol test to Rust
What changed, and why it matters
This commit is a straightforward refactoring: it moves an existing anti-klepto protocol unit test from C to Rust. No production firmware code is changed, and no security vulnerability is introduced or fixed. The same cryptographic checks are still performed, just in a different test language.
No security action needed. Treat as normal test-suite refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change deletes test_keystore_antiklepto.c and its CMake registration, then adds an equivalent Rust test (test_secp256k1_antiklepto_protocol) in keystore.rs. It also exposes the required secp256k1 ECDSA anti-exfil host-verify and opening-parse functions to Rust under the testing feature. The production signing path in signtx.rs is only touched to update a comment referencing the new test location.
Changed components
Rust unit tests (keystore.rs)C unit tests (test_keystore_antiklepto.c removed)Rust FFI bindings for secp256k1 anti-exfil host verification (testing-only feature)Inspect captured patch +102 / −123
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
index fa823ba..d284569 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs
@@ -2695,8 +2695,8 @@ mod tests {
let host_nonce = hex!("abababababababababababababababababababababababababababababababab");
// The host nonce commitment value does not impact this test, but an invalid commitment
// would fail the antiklepto signature check on the host. The host check is skipped here and
- // tested in test_keystore_antiklepto.c. That the host nonce was included in the sig is
- // tested by the siganture fixture test below.x
+ // tested in keystore::tests::test_secp256k1_antiklepto_protocol. That the host nonce was included in the sig is
+ // tested by the signature fixture test below.
let host_nonce_commitment = pb::AntiKleptoHostNonceCommitment {
commitment: bitbox02::secp256k1::ecdsa_anti_exfil_host_commit(SECP256K1, &host_nonce)
.unwrap(),
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 8bec507..93fb4d0 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -1202,6 +1202,67 @@ mod tests {
);
}
+ #[test]
+ fn test_secp256k1_antiklepto_protocol() {
+ mock_unlocked();
+
+ let mut keypath = [84 + HARDENED, 1 + HARDENED, 0 + HARDENED, 0, 0];
+ let mut msg = [0x23u8; 32];
+ let mut host_nonce = [0x55u8; 32];
+
+ for index in 0..3 {
+ keypath[4] = index;
+ msg[0] = index as u8;
+ host_nonce[0] = index as u8;
+
+ // Protocol steps are described in secp256k1/include/secp256k1_ecdsa_s2c.h under
+ // "ECDSA Anti-Klepto Protocol".
+
+ // Protocol step 1.
+ let host_commitment_vec =
+ bitbox02::secp256k1::ecdsa_anti_exfil_host_commit(SECP256K1, &host_nonce).unwrap();
+ let host_commitment: [u8; 32] = host_commitment_vec.try_into().unwrap();
+
+ // Get pubkey at keypath.
+ let private_key = secp256k1_get_private_key(&keypath).unwrap();
+ let private_key_bytes: [u8; 32] = private_key.as_slice().try_into().unwrap();
+ let secret_key = secp256k1::SecretKey::from_slice(&private_key_bytes).unwrap();
+ let public_key = secret_key.public_key(SECP256K1);
+
+ // Commit - protocol step 2.
+ let signer_commitment =
+ secp256k1_nonce_commit(&private_key_bytes, &msg, &host_commitment).unwrap();
+ // Protocol step 3: host_nonce sent from host to signer to be used in step 4.
+ // Sign - protocol step 4.
+ let sign_result = secp256k1_sign(&private_key_bytes, &msg, &host_nonce).unwrap();
+
+ let signature =
+ secp256k1::ecdsa::Signature::from_compact(&sign_result.signature).unwrap();
+ // Protocol step 5: host verification.
+ bitbox02::secp256k1::anti_exfil_host_verify(
+ SECP256K1,
+ &signature,
+ &msg,
+ &public_key,
+ &host_nonce,
+ &signer_commitment,
+ )
+ .unwrap();
+
+ let message = secp256k1::Message::from_digest_slice(&msg).unwrap();
+ let recoverable_sig = secp256k1::ecdsa::RecoverableSignature::from_compact(
+ &sign_result.signature,
+ secp256k1::ecdsa::RecoveryId::from_i32(sign_result.recid as i32).unwrap(),
+ )
+ .unwrap();
+ assert!(
+ SECP256K1
+ .verify_ecdsa(&message, &recoverable_sig.to_standard(), &public_key)
+ .is_ok()
+ );
+ }
+ }
+
#[test]
fn test_secp256k1_schnorr_sign() {
mock_unlocked_using_mnemonic(
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index 4b04e03..452cd7c 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -51,6 +51,9 @@ const ALLOWLIST_TYPES: &[&str] = &[
"confirm_params_t",
"trinary_input_string_params_t",
"securechip_error_t",
+ "secp256k1_ecdsa_s2c_opening",
+ "secp256k1_ecdsa_signature",
+ "secp256k1_pubkey",
];
const ALLOWLIST_FNS: &[&str] = &[
@@ -141,6 +144,8 @@ const ALLOWLIST_FNS: &[&str] = &[
"sd_write_bin",
"sdcard_create",
"secp256k1_ecdsa_anti_exfil_host_commit",
+ "secp256k1_ecdsa_s2c_opening_parse",
+ "secp256k1_anti_exfil_host_verify",
"securechip_attestation_sign",
"securechip_kdf",
"securechip_model",
diff --git a/src/rust/bitbox02/src/secp256k1.rs b/src/rust/bitbox02/src/secp256k1.rs
index dde5d98..bf619eb 100644
--- a/src/rust/bitbox02/src/secp256k1.rs
+++ b/src/rust/bitbox02/src/secp256k1.rs
@@ -32,6 +32,40 @@ pub fn ecdsa_anti_exfil_host_commit(secp: &Secp256k1<All>, rand32: &[u8]) -> Res
}
}
+#[cfg(feature = "testing")]
+pub fn anti_exfil_host_verify(
+ secp: &Secp256k1<All>,
+ signature: &bitcoin::secp256k1::ecdsa::Signature,
+ msg: &[u8; 32],
+ pubkey: &bitcoin::secp256k1::PublicKey,
+ host_nonce: &[u8; 32],
+ signer_commitment: &[u8; 33],
+) -> Result<(), ()> {
+ let mut opening = core::mem::MaybeUninit::<bitbox02_sys::secp256k1_ecdsa_s2c_opening>::uninit();
+ let parse_res = unsafe {
+ bitbox02_sys::secp256k1_ecdsa_s2c_opening_parse(
+ secp.ctx().as_ptr().cast(),
+ opening.as_mut_ptr(),
+ signer_commitment.as_ptr(),
+ )
+ };
+ if parse_res != 1 {
+ return Err(());
+ }
+ let opening = unsafe { opening.assume_init() };
+ let verify_res = unsafe {
+ bitbox02_sys::secp256k1_anti_exfil_host_verify(
+ secp.ctx().as_ptr().cast(),
+ signature.as_c_ptr() as *const bitbox02_sys::secp256k1_ecdsa_signature,
+ msg.as_ptr(),
+ pubkey.as_c_ptr() as *const bitbox02_sys::secp256k1_pubkey,
+ host_nonce.as_ptr(),
+ &opening,
+ )
+ };
+ if verify_res == 1 { Ok(()) } else { Err(()) }
+}
+
pub fn dleq_prove(
secp: &Secp256k1<All>,
sk: &[u8; 32],
diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt
index 1cebf04..6bde497 100644
--- a/test/unit-test/CMakeLists.txt
+++ b/test/unit-test/CMakeLists.txt
@@ -49,8 +49,6 @@ target_include_directories(mocks
set(TEST_LIST
cleanup
"-Wl,--wrap=util_cleanup_32"
- keystore_antiklepto
- ""
gestures
""
random
diff --git a/test/unit-test/test_keystore_antiklepto.c b/test/unit-test/test_keystore_antiklepto.c
deleted file mode 100644
index 6f8552d..0000000
--- a/test/unit-test/test_keystore_antiklepto.c
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2020 Shift Crypto AG
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include <setjmp.h>
-#include <stdarg.h>
-#include <stddef.h>
-#include <cmocka.h>
-
-#include <keystore.h>
-
-#include <rust/rust.h>
-#include <secp256k1_ecdsa_s2c.h>
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-#include <string.h>
-
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wnested-externs"
-
-static uint8_t _mock_seed[32] = {
- 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
- 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
-};
-
-static uint8_t _mock_bip39_seed[64] = {
- 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
- 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
- 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
- 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
-};
-
-#define BIP32_INITIAL_HARDENED_CHILD 0x80000000
-
-static void _test_keystore_antiklepto(void** state)
-{
- keystore_mock_unlocked(_mock_seed, sizeof(_mock_seed), _mock_bip39_seed);
-
- uint32_t keypath[] = {
- 84 + BIP32_INITIAL_HARDENED_CHILD,
- 1 + BIP32_INITIAL_HARDENED_CHILD,
- 0 + BIP32_INITIAL_HARDENED_CHILD,
- 0,
- 0,
- };
-
- uint8_t msg[32];
- memset(msg, 0x23, sizeof(msg));
-
- uint8_t host_nonce[32];
- memset(host_nonce, 0x55, sizeof(host_nonce));
-
- uint8_t signer_commitment[33] = {0};
-
- uint8_t sig[64];
- int recid;
-
- secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
-
- for (int i = 0; i < 3; i++) {
- keypath[4] = i;
- msg[0] = i;
- host_nonce[0] = i;
- uint8_t host_nonce_commitment[32];
-
- // Get pubkey at keypath
- uint8_t private_key[32] = {0};
- assert_true(rust_secp256k1_get_private_key(
- keypath, 5, rust_util_bytes_mut(private_key, sizeof(private_key))));
- secp256k1_pubkey public_key = {0};
- assert_true(secp256k1_ec_pubkey_create(ctx, &public_key, private_key));
-
- // Protocol steps are described in secp256k1/include/secp256k1_ecdsa_s2c.h under "ECDSA
- // Anti-Klepto Protocol".
-
- // Protocol step 1.
- assert_true(secp256k1_ecdsa_anti_exfil_host_commit(ctx, host_nonce_commitment, host_nonce));
-
- // Commit - protocol step 2.
- assert_true(keystore_secp256k1_nonce_commit(
- ctx, private_key, msg, host_nonce_commitment, signer_commitment));
- // Protocol step 3: host_nonce sent from host to signer to be used in step 4
- // Sign - protocol step 4.
- assert_true(keystore_secp256k1_sign(ctx, private_key, msg, host_nonce, sig, &recid));
-
- // Protocol step 5: host verification.
- secp256k1_ecdsa_signature parsed_signature;
- assert_true(secp256k1_ecdsa_signature_parse_compact(ctx, &parsed_signature, sig));
-
- secp256k1_ecdsa_s2c_opening opening;
- assert_true(secp256k1_ecdsa_s2c_opening_parse(ctx, &opening, signer_commitment));
- assert_true(secp256k1_anti_exfil_host_verify(
- ctx, &parsed_signature, msg, &public_key, host_nonce, &opening));
- }
-
- secp256k1_context_destroy(ctx);
-}
-
-int main(void)
-{
- const struct CMUnitTest tests[] = {
- cmocka_unit_test(_test_keystore_antiklepto),
- };
- return cmocka_run_group_tests(tests, NULL, NULL);
-}
-
-#pragma GCC diagnostic pop
Why this scored 13/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.