Skip to content

Commit

Permalink
rust: add ability to connect to BitBox02 simulator
Browse files Browse the repository at this point in the history
Beginning of adding useful tests for this library.
  • Loading branch information
benma committed Jun 3, 2024
1 parent 8ee1b85 commit 1aa9f8c
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 9 deletions.
18 changes: 15 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "bitbox-api"
authors = ["Marko Bencun <[email protected]>"]
version = "0.3.1"
version = "0.4.0"
homepage = "https://bitbox.swiss/"
repository = "https://github.com/BitBoxSwiss/bitbox-api-rs/"
readme = "README-rust.md"
Expand Down Expand Up @@ -43,7 +43,9 @@ wasm-bindgen-futures = { version ="0.4.42", optional = true }
web-sys = { version = "0.3.64", features = ["Storage", "Window"], optional = true }

[dev-dependencies]
async-trait = "0.1.68"
wasm-bindgen-test = "0.3.42"
tokio = { version = "1", features = ["time", "macros", "rt"] }

[build-dependencies]
prost-build = { version = "0.11" }
Expand Down Expand Up @@ -91,6 +93,7 @@ lto = true
# This may or may not cause trouble on macOS, see: https://github.com/libusb/hidapi/issues/503
multithreaded = []
usb = ["dep:hidapi"]
simulator = []
wasm = [
"dep:enum-assoc",
"dep:js-sys",
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2023 Shift Crypto AG, Switzerland. All rights reserved.
Copyright 2023-2024 Shift Crypto AG, Switzerland. All rights reserved.

Apache License
Version 2.0, January 2004
Expand Down
5 changes: 3 additions & 2 deletions ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ features=(
"usb"
"wasm"
"multithreaded,usb"
"simulator,tokio"
)

examples=(
Expand All @@ -23,8 +24,8 @@ cargo fmt --check

for feature_set in "${features[@]}"; do
echo $feature_set
cargo test --locked --features="$feature_set" --all-targets
cargo clippy --locked --features="$feature_set" --all-targets -- -D warnings -A clippy::empty-docs
cargo test --tests --locked --features="$feature_set"
cargo clippy --tests --locked --features="$feature_set" -- -D warnings -A clippy::empty-docs
done

for example in "${examples[@]}"; do
Expand Down
4 changes: 2 additions & 2 deletions src/communication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::util::Threading;
use async_trait::async_trait;
use thiserror::Error;

#[cfg(any(feature = "wasm", feature = "usb"))]
#[cfg(any(feature = "wasm", feature = "usb", feature = "simulator"))]
pub const FIRMWARE_CMD: u8 = 0x80 + 0x40 + 0x01;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -146,7 +146,7 @@ const HWW_RSP_BUSY: u8 = 0x02;
// Bad request.
const HWW_RSP_NACK: u8 = 0x03;

#[derive(Debug, Copy, Clone)]
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Product {
Unknown,
BitBox02Multi,
Expand Down
2 changes: 2 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#[cfg(any(feature = "wasm", feature = "usb"))]
pub const VENDOR_ID: u16 = 0x03eb;
#[cfg(any(feature = "wasm", feature = "usb"))]
pub const PRODUCT_ID: u16 = 0x2403;
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub enum Error {
#[cfg(feature = "usb")]
#[error("hid error: {0}")]
Hid(#[from] hidapi::HidError),
#[cfg(feature = "simulator")]
#[error("simulator error: {0}")]
Hid(#[from] crate::simulator::Error),
#[error("communication error: {0}")]
#[cfg_attr(feature = "wasm", assoc(js_code = "communication".into()))]
Communication(communication::Error),
Expand Down
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub mod error;
pub mod eth;
mod noise;
pub mod runtime;
#[cfg(feature = "simulator")]
pub mod simulator;
#[cfg(feature = "usb")]
pub mod usb;
#[cfg(feature = "wasm")]
Expand Down Expand Up @@ -97,6 +99,18 @@ impl<R: Runtime> BitBox<R> {
Self::from(comm, noise_config).await
}

#[cfg(feature = "simulator")]
pub async fn from_simulator(
endpoint: Option<&str>,
noise_config: Box<dyn NoiseConfig>,
) -> Result<BitBox<R>, Error> {
let comm = Box::new(communication::U2fHidCommunication::from(
crate::simulator::try_connect::<R>(endpoint).await?,
communication::FIRMWARE_CMD,
));
Self::from(comm, noise_config).await
}

/// Invokes the device unlock and pairing.
pub async fn unlock_and_pair(self) -> Result<PairingBitBox<R>, Error> {
self.communication
Expand Down
62 changes: 62 additions & 0 deletions src/simulator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use async_trait::async_trait;
use std::io::{Read, Write};
use std::net::TcpStream;

use std::sync::Mutex;

use super::communication::{Error as CommunicationError, ReadWrite};
use super::runtime::Runtime;

const DEFAULT_ENDPOINT: &str = "127.0.0.1:15423";

pub struct TcpClient {
stream: Mutex<TcpStream>,
}

impl TcpClient {
fn new(address: &str) -> Result<Self, std::io::Error> {
let stream = TcpStream::connect(address)?;
Ok(Self {
stream: Mutex::new(stream),
})
}
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("connection error")]
Connect,
}

/// Connect to a running simulator at this endpoint. Endpoint defaults to `127.0.0.1:15423`.
/// This tries to connect repeatedly for up to about 2 seconds.
pub async fn try_connect<R: Runtime>(endpoint: Option<&str>) -> Result<Box<TcpClient>, Error> {
for _ in 0..200 {
match TcpClient::new(endpoint.unwrap_or(DEFAULT_ENDPOINT)) {
Ok(client) => return Ok(Box::new(client)),
Err(_) => R::sleep(std::time::Duration::from_millis(10)).await,
}
}
Err(Error::Connect)
}

impl crate::util::Threading for TcpClient {}

#[async_trait(?Send)]
impl ReadWrite for TcpClient {
fn write(&self, msg: &[u8]) -> Result<usize, CommunicationError> {
let mut stream = self.stream.lock().unwrap();
stream.write(msg).map_err(|_| CommunicationError::Write)
}

async fn read(&self) -> Result<Vec<u8>, CommunicationError> {
let mut stream = self.stream.lock().unwrap();

let mut buffer = vec![0; 64];
let n = stream
.read(&mut buffer)
.map_err(|_| CommunicationError::Read)?;
buffer.truncate(n);
Ok(buffer)
}
}
Binary file added tests/simulator
Binary file not shown.
135 changes: 135 additions & 0 deletions tests/simulator_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#![cfg(feature = "simulator")]

#[cfg(not(feature = "tokio"))]
compile_error!("Enable the tokio feature to run simulator tests");

use bitcoin::hashes::Hash;
use std::process::Command;
use std::str::FromStr;

use bitbox_api::pb;

type PairedBitBox = bitbox_api::PairedBitBox<bitbox_api::runtime::TokioRuntime>;

async fn test_btc(bitbox: &PairedBitBox) {
// btc_xpub
{
let xpub = bitbox
.btc_xpub(
pb::BtcCoin::Tbtc,
&"m/49'/1'/0'".try_into().unwrap(),
pb::btc_pub_request::XPubType::Ypub,
false,
)
.await
.unwrap();
assert_eq!(
xpub.as_str(),
"ypub6WqXiL3fbDK5QNPe3hN4uSVkEvuE8wXoNCcecgggSuKVpU3Kc4fTvhuLgUhtnbAdaTb9gpz5PQdvzcsKPTLgW2CPkF5ZNRzQeKFT4NSc1xN",
);
}
// btc_address
{
let address = bitbox
.btc_address(
pb::BtcCoin::Tbtc,
&"m/84'/1'/0'/1/10".try_into().unwrap(),
&bitbox_api::btc::make_script_config_simple(
pb::btc_script_config::SimpleType::P2wpkh,
),
false,
)
.await
.unwrap();
assert_eq!(
address.as_str(),
"tb1qq064dxjgl9h9wzgsmzy6t6306qew42w9ka02u3"
);
}
// btc_sign_message
{
let keypath = bitbox_api::Keypath::try_from("m/49'/0'/0'/0/10").unwrap();

let xpub_str = bitbox
.btc_xpub(
pb::BtcCoin::Btc,
&"m/49'/0'/0'".try_into().unwrap(),
pb::btc_pub_request::XPubType::Xpub,
false,
)
.await
.unwrap();
let secp = bitcoin::secp256k1::Secp256k1::new();
let pubkey = bitcoin::bip32::Xpub::from_str(&xpub_str)
.unwrap()
.derive_pub(
&secp,
&"m/0/10".parse::<bitcoin::bip32::DerivationPath>().unwrap(),
)
.unwrap()
.to_pub()
.inner;

let result = bitbox
.btc_sign_message(
pb::BtcCoin::Btc,
pb::BtcScriptConfigWithKeypath {
script_config: Some(bitbox_api::btc::make_script_config_simple(
pb::btc_script_config::SimpleType::P2wpkhP2sh,
)),
keypath: keypath.to_vec(),
},
b"message",
)
.await
.unwrap();

pubkey
.verify(
&secp,
&bitcoin::secp256k1::Message::from_digest(
bitcoin::hashes::sha256d::Hash::hash(
b"\x18Bitcoin Signed Message:\n\x07message",
)
.to_byte_array(),
),
&bitcoin::secp256k1::ecdsa::Signature::from_compact(&result.sig).unwrap(),
)
.unwrap();
}
}

#[tokio::test]
async fn test_device() {
let _server = Command::new("./tests/simulator")
.spawn()
.expect("failed to start server");

let noise_config = Box::new(bitbox_api::NoiseConfigNoCache {});
let bitbox =
bitbox_api::BitBox::<bitbox_api::runtime::TokioRuntime>::from_simulator(None, noise_config)
.await
.unwrap();
let pairing_bitbox = bitbox.unlock_and_pair().await.unwrap();
let paired_bitbox = pairing_bitbox.wait_confirm().await.unwrap();

let device_info = paired_bitbox.device_info().await.unwrap();

assert_eq!(device_info.name, "My BitBox");
assert_eq!(paired_bitbox.product(), bitbox_api::Product::BitBox02Multi);

assert!(paired_bitbox.restore_from_mnemonic().await.is_ok());

// --- Tests that run on the initialized/seeded device follow.
// --- The simulator is initialized with the following mnemonic:
// --- boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide

assert_eq!(
paired_bitbox.root_fingerprint().await.unwrap().as_str(),
"4c00739d"
);

assert!(paired_bitbox.show_mnemonic().await.is_ok());

test_btc(&paired_bitbox).await;
}

0 comments on commit 1aa9f8c

Please sign in to comment.