Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions bhwi-async/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod transport;
use std::{error::Error as StdError, fmt::Debug};

use async_trait::async_trait;
pub use bhwi::common::Version;
use bhwi::{
Interpreter,
bitcoin::{
Expand Down Expand Up @@ -34,6 +35,7 @@ pub trait HttpClient {
pub trait HWI {
type Error: Debug;
async fn unlock(&mut self, network: Network) -> Result<(), Self::Error>;
async fn get_version(&mut self) -> Result<Version, Self::Error>;
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error>;
async fn get_extended_pubkey(
&mut self,
Expand All @@ -53,6 +55,7 @@ pub trait HWI {
#[async_trait(?Send)]
pub trait HWIDevice {
async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError>;
async fn get_version(&mut self) -> Result<Version, HWIDeviceError>;
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError>;
async fn get_extended_pubkey(
&mut self,
Expand Down Expand Up @@ -109,6 +112,16 @@ where
Ok(())
}

async fn get_version(&mut self) -> Result<Version, Self::Error> {
if let common::Response::Version(version) =
run_command(self, common::Command::GetVersion).await?
{
Ok(version)
} else {
Err(common::Error::NoErrorOrResult.into())
}
}

async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error> {
if let common::Response::MasterFingerprint(fg) =
run_command(self, common::Command::GetMasterFingerprint).await?
Expand Down Expand Up @@ -166,6 +179,10 @@ where
.map_err(HWIDeviceError::new)
}

async fn get_version(&mut self) -> Result<Version, HWIDeviceError> {
HWI::get_version(self).await.map_err(HWIDeviceError::new)
}

async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
HWI::get_master_fingerprint(self)
.await
Expand Down
20 changes: 15 additions & 5 deletions bhwi-cli/src/bin/bhwi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,27 @@ async fn main() -> Result<()> {
Commands::Device(DeviceCommands::List) => {
let mut devices = dev_man.enumerate().await?;
for (i, device) in devices.iter_mut().enumerate() {
// XXX: Coldcard always needs unlocking
device.device().unlock(dev_man.config.network).await?;
let fingerprint = device.fingerprint().await?;
let name = device.name();
let name = device.name().to_string();
let is_emulated = device.is_emulated();
let version = device.version().await?;
match format {
Some(OutputFormat::Pretty) => {
if i == 0 {
println!("{:<18} | {:<8} | {:<15}", "Name", "Emulated", "Fingerprint");
println!(
"{:<18} | {:<8} | {:<15} | {:<12} | {:<8}",
"Name", "Emulated", "Fingerprint", "Network", "Version"
);
}
println!("{}", "-".repeat(55));
println!("{name:<18} | {is_emulated:<8} | {fingerprint:<15}");
println!("{}", "-".repeat(55));
println!("{}", "-".repeat(80));
let network = version.network.unwrap_or_default();
println!(
"{name:<18} | {is_emulated:<8} | {fingerprint:<15} | {network:<12} | {:<8}",
version.version
);
println!("{}", "-".repeat(80));
}
Some(OutputFormat::Json) => {}
None => println!("{fingerprint}"),
Expand Down
21 changes: 10 additions & 11 deletions bhwi-cli/src/coldcard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,16 @@ impl ColdcardDevice {
#[cfg(unix)]
async fn emulator_device(path: &str, rng: &mut OsRng) -> Result<Option<Device>> {
if std::fs::exists(path)? {
Ok(Some(
Device::new(
"Coldcard Emulator",
Box::new(Coldcard::new(
ColdcardTransportHID::new(emulator::EmulatorClient::new(path).await?),
rng,
)),
true,
)
.await?,
))
let Ok(client) = emulator::EmulatorClient::new(path).await else {
return Ok(None);
};
let device = Device::new(
"Coldcard Emulator",
Box::new(Coldcard::new(ColdcardTransportHID::new(client), rng)),
true,
)
.await?;
Ok(Some(device))
} else {
Ok(None)
}
Expand Down
14 changes: 14 additions & 0 deletions bhwi-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
use async_trait::async_trait;
use bhwi_async::HWIDevice;
use bhwi_async::Version;
use bitcoin::bip32::Fingerprint;
use clap::ValueEnum;
use futures::future::join_all;
Expand All @@ -24,6 +25,8 @@ pub struct Device {
is_emulated: bool,
#[serde(default, serialize_with = "option_fingerprint")]
fingerprint: Option<Fingerprint>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
version: Option<Version>,
}

impl Device {
Expand All @@ -33,6 +36,7 @@ impl Device {
device,
is_emulated,
fingerprint: None,
version: None,
})
}

Expand All @@ -57,6 +61,16 @@ impl Device {
Ok(fingerprint)
}
}

pub async fn version(&mut self) -> Result<Version> {
if let Some(ref version) = self.version {
Ok(version.clone())
} else {
let version = self.device.get_version().await?;
self.version = Some(version.clone());
Ok(version)
}
}
}

#[derive(Debug, EnumIter, strum::Display)]
Expand Down
21 changes: 18 additions & 3 deletions bhwi/src/coldcard/api.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// See https://github.com/Coldcard/ckcc-protocol for implementation details.
pub mod request {
use bitcoin::bip32::DerivationPath;

Expand All @@ -16,25 +17,26 @@ pub mod request {
}
}

// https://github.com/Coldcard/ckcc-protocol/blob/0bd92d4d6d01872e41ffc1e7d9a1e2f153130061/ckcc/protocol.py#L125
pub fn sign_message(message: &[u8], path: &DerivationPath) -> Vec<u8> {
let mut data = b"smsg".to_vec();
// coldcard can support a few different address types:
// https://github.com/Coldcard/ckcc-protocol/blob/0bd92d4d6d01872e41ffc1e7d9a1e2f153130061/ckcc/constants.py#L75-L90
data.extend((0x01u32 | 0x02 | 0x04).to_le_bytes()); // hardcoding to P2WPKH
let path_string = path.to_string();
data.extend((path_string.len() as u32).to_le_bytes());
// https://github.com/Coldcard/ckcc-protocol/blob/0bd92d4d6d01872e41ffc1e7d9a1e2f153130061/ckcc/constants.py#L27-L31
data.extend((message.len() as u32).to_le_bytes());
data.extend(path_string.as_bytes());
data.extend_from_slice(message);
data
}

// https://github.com/Coldcard/ckcc-protocol/blob/0bd92d4d6d01872e41ffc1e7d9a1e2f153130061/ckcc/protocol.py#L131
pub fn get_signed_message() -> Vec<u8> {
b"smok".to_vec()
}

pub fn get_version() -> Vec<u8> {
b"vers".to_vec()
}
}

#[cfg(test)]
Expand Down Expand Up @@ -188,6 +190,19 @@ pub mod response {
Ok(ColdcardResponse::Xpub(xpub(res)?))
}

pub fn version(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
let data = ResponseHandler::expect_response(res, ResponseMessage::Asci)?;
let version_string =
std::str::from_utf8(data).map_err(|e| ColdcardError::Serialization(e.to_string()))?;
let lines = version_string.lines().collect::<Vec<&str>>();
let version = lines.get(1).unwrap_or(&version_string).to_string();
let device_model = lines.last().cloned().unwrap_or_default().to_string();
Ok(ColdcardResponse::Version {
version,
device_model,
})
}

pub fn mypub(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
let data = ResponseHandler::expect_response(res, ResponseMessage::MyPb)?;
let (dev_pubkey, data) = split(data, 64)?;
Expand Down
29 changes: 24 additions & 5 deletions bhwi/src/coldcard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bitcoin::secp256k1::ecdsa::Signature;

use crate::Interpreter;
use crate::coldcard::api::response::ResponseMessage;
use crate::common::{Command, Error, Recipient, Response, Transmit};
use crate::common::{Command, Error, Recipient, Response, Transmit, Version};
use crate::device::DeviceId;

pub const DEFAULT_CKCC_SOCKET: &str = "/tmp/ckcc-simulator.sock";
Expand Down Expand Up @@ -54,6 +54,7 @@ impl ColdcardError {

pub enum ColdcardCommand {
StartEncryption,
GetVersion,
GetMasterFingerprint,
GetXpub(DerivationPath),
SignMessage {
Expand All @@ -65,6 +66,10 @@ pub enum ColdcardCommand {
pub enum ColdcardResponse {
Ok,
Busy,
Version {
version: String,
device_model: String,
},
MasterFingerprint(Fingerprint),
Xpub(Xpub),
MyPub {
Expand Down Expand Up @@ -131,6 +136,7 @@ where
payload: api::request::start_encryption(None, &self.encryption.pub_key()?),
encrypted: false,
},
ColdcardCommand::GetVersion => request(api::request::get_version(), self.encryption)?,
ColdcardCommand::GetMasterFingerprint => request(
api::request::get_xpub(&DerivationPath::master()),
self.encryption,
Expand All @@ -149,6 +155,11 @@ where
fn exchange(&mut self, data: Vec<u8>) -> Result<Option<Self::Transmit>, Self::Error> {
match &self.state {
State::New => Ok(None),
State::Running(ColdcardCommand::GetVersion) => {
let data = self.encryption.decrypt(data)?;
self.state = State::Finished(api::response::version(&data)?);
Ok(None)
}
State::Running(ColdcardCommand::GetMasterFingerprint) => {
let data = self.encryption.decrypt(data)?;
self.state = State::Finished(api::response::master_fingerprint(&data)?);
Expand Down Expand Up @@ -195,6 +206,7 @@ impl TryFrom<Command> for ColdcardCommand {
Command::GetMasterFingerprint => Ok(Self::GetMasterFingerprint),
Command::GetXpub { path, .. } => Ok(Self::GetXpub(path)),
Command::SignMessage { message, path } => Ok(Self::SignMessage { message, path }),
Command::GetVersion => Ok(Self::GetVersion),
}
}
}
Expand All @@ -204,6 +216,14 @@ impl From<ColdcardResponse> for Response {
match res {
ColdcardResponse::MasterFingerprint(fg) => Response::MasterFingerprint(fg),
ColdcardResponse::Xpub(xpub) => Response::Xpub(xpub),
ColdcardResponse::Version {
version,
device_model,
} => Response::Version(Version {
version: version.as_str().into(),
network: None,
firmware: Some(device_model),
}),
ColdcardResponse::MyPub { encryption_key, .. } => {
Response::EncryptionKey(encryption_key)
}
Expand Down Expand Up @@ -233,10 +253,9 @@ impl From<ColdcardError> for Error {
ColdcardError::MissingCommandInfo(e) => Error::MissingCommandInfo(e),
ColdcardError::NoErrorOrResult => Error::NoErrorOrResult,
ColdcardError::Serialization(s) => Error::Serialization(s),
ColdcardError::UnexpectedResponseMessage { got, expected } => Error::UnexpectedResult(
format!("unexpected device response message. got: {got}, expected: {expected:?}")
.as_bytes()
.to_vec(),
ColdcardError::UnexpectedResponseMessage { got, expected } => Error::unexpected_result(
format!("{got:?}").into_bytes(),
format!("coldcard unexpected response: expected {expected:?}, got {got:?}"),
),
}
}
Expand Down
28 changes: 23 additions & 5 deletions bhwi/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ pub struct UnlockOptions {
}

pub enum Command {
Unlock {
options: UnlockOptions,
},
GetMasterFingerprint,
GetVersion,
GetXpub {
path: DerivationPath,
display: bool,
Expand All @@ -22,17 +20,31 @@ pub enum Command {
message: Vec<u8>,
path: DerivationPath,
},
Unlock {
options: UnlockOptions,
},
}

pub enum Response {
TaskDone,
TaskBusy,
Version(Version),
MasterFingerprint(Fingerprint),
Xpub(Xpub),
EncryptionKey([u8; 64]),
Signature(u8, Signature),
}

/// Version information returned from a device.
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct Version {
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub firmware: Option<String>,
}

pub enum Recipient {
Device,
PinServer { url: String },
Expand All @@ -55,8 +67,8 @@ pub enum Error {
#[error("missing command info: {0}")]
MissingCommandInfo(&'static str),

#[error("unexpected result: {0:x?}")]
UnexpectedResult(Vec<u8>),
#[error("unexpected result for {1}: {0:x?}")]
UnexpectedResult(Vec<u8>, String),

#[error("rpc error {0}: {1:?}")]
Rpc(i32, Option<String>),
Expand All @@ -71,6 +83,12 @@ pub enum Error {
AuthenticationRefused,
}

impl Error {
pub fn unexpected_result(data: Vec<u8>, context: impl Into<String>) -> Self {
Error::UnexpectedResult(data, context.into())
}
}

pub type ColdcardInterpreter<'a> =
coldcard::ColdcardInterpreter<'a, Command, Transmit, Response, Error>;
pub type JadeInterpreter = jade::JadeInterpreter<Command, Transmit, Response, Error>;
Expand Down
18 changes: 16 additions & 2 deletions bhwi/src/jade/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// See https://github.com/Blockstream/Jade/blob/master/docs/index.rst
// See https://github.com/Blockstream/Jade/blob/master/docs/index.rst
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::{collections::BTreeMap, fmt::Display};

use super::JadeError;

Expand Down Expand Up @@ -133,6 +133,20 @@ pub enum JadeNetworks {
All,
}

impl Display for JadeNetworks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
JadeNetworks::Main => "main",
JadeNetworks::Test => "test",
JadeNetworks::All => "all",
}
)
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DescriptorInfoResponse {
pub descriptor_len: u32,
Expand Down
Loading
Loading