pub mod request {
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint};
pub const MAX_UPLOAD_CHUNK_LEN: usize = 2048;
pub fn start_encryption(version: Option<u32>, key: &[u8; 64]) -> Vec<u8> {
let mut data = "ncry".as_bytes().to_owned();
data.extend(version.unwrap_or(1).to_le_bytes());
data.extend(key);
data
}
pub fn start_backup() -> Vec<u8> {
b"back".to_vec()
}
pub fn get_xpub(path: &DerivationPath) -> Vec<u8> {
if path.is_master() {
"xpubm".as_bytes().to_vec()
} else {
format!("xpubm/{}", path).as_bytes().to_vec()
}
}
pub mod addr_fmt {
pub const AFC_PUBKEY: u32 = 0x01;
pub const AFC_SEGWIT: u32 = 0x02;
pub const AFC_BECH32: u32 = 0x04;
pub const AFC_SCRIPT: u32 = 0x08;
pub const AFC_WRAPPED: u32 = 0x10;
pub const AFC_BECH32M: u32 = 0x20;
pub const AF_P2PKH: u32 = AFC_PUBKEY;
pub const AF_P2WPKH: u32 = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32;
pub const AF_P2WPKH_P2SH: u32 = AFC_PUBKEY | AFC_SEGWIT | AFC_WRAPPED;
pub const AF_P2TR: u32 = AFC_PUBKEY | AFC_SEGWIT | AFC_BECH32M;
pub const AF_P2SH: u32 = AFC_SCRIPT;
pub const AF_P2WSH: u32 = AFC_SCRIPT | AFC_SEGWIT | AFC_BECH32;
pub const AF_P2WSH_P2SH: u32 = AFC_SCRIPT | AFC_SEGWIT | AFC_WRAPPED;
pub fn from_address_type(addr_type: bitcoin::address::AddressType) -> u32 {
match addr_type {
bitcoin::address::AddressType::P2pkh => AF_P2PKH,
bitcoin::address::AddressType::P2sh => AF_P2WPKH_P2SH,
bitcoin::address::AddressType::P2wpkh => AF_P2WPKH,
bitcoin::address::AddressType::P2wsh => AF_P2WSH,
bitcoin::address::AddressType::P2tr => AF_P2TR,
_ => AF_P2WPKH,
}
}
}
pub fn show_address(path: &DerivationPath, addr_fmt: u32) -> Vec<u8> {
let mut data = b"show".to_vec();
data.extend(addr_fmt.to_le_bytes());
data.extend(path.to_string().as_bytes());
data
}
pub fn show_p2sh_address(
threshold: u8,
key_paths: &[(Fingerprint, DerivationPath)],
redeem_script: &[u8],
addr_fmt: u32,
) -> Vec<u8> {
let mut data = b"p2sh".to_vec();
data.extend(addr_fmt.to_le_bytes());
data.push(threshold);
data.push(key_paths.len() as u8);
data.extend((redeem_script.len() as u16).to_le_bytes());
data.extend(redeem_script);
for (fingerprint, derivation_path) in key_paths {
let path = coldcard_xfp_path(*fingerprint, derivation_path);
data.push(path.len() as u8);
for child in path {
data.extend(child.to_le_bytes());
}
}
data
}
fn coldcard_xfp_path(fingerprint: Fingerprint, path: &DerivationPath) -> Vec<u32> {
let mut result = vec![u32::from_le_bytes(fingerprint.to_bytes())];
result.extend(path.as_ref().iter().copied().map(child_number_u32));
result
}
fn child_number_u32(child: ChildNumber) -> u32 {
u32::from(child)
}
pub fn miniscript_address(name: &str, change: bool, index: u32) -> Vec<u8> {
let mut data = b"msas".to_vec();
data.extend((change as u32).to_le_bytes());
data.extend(index.to_le_bytes());
data.extend(name.as_bytes());
data
}
pub fn sign_message(message: &[u8], path: &DerivationPath) -> Vec<u8> {
let mut data = b"smsg".to_vec();
data.extend((0x01u32 | 0x02 | 0x04).to_le_bytes()); let path_string = path.to_string();
data.extend((path_string.len() as u32).to_le_bytes());
data.extend((message.len() as u32).to_le_bytes());
data.extend(path_string.as_bytes());
data.extend_from_slice(message);
data
}
pub fn get_signed_message() -> Vec<u8> {
b"smok".to_vec()
}
pub fn upload(offset: u32, total_size: u32, data: &[u8]) -> Vec<u8> {
assert!(data.len() <= MAX_UPLOAD_CHUNK_LEN);
let mut rv = b"upld".to_vec();
rv.extend(offset.to_le_bytes());
rv.extend(total_size.to_le_bytes());
rv.extend(data);
rv
}
pub fn sha256() -> Vec<u8> {
b"sha2".to_vec()
}
pub fn sign_transaction(length: u32, file_sha: &[u8; 32]) -> Vec<u8> {
let mut rv = b"stxn".to_vec();
rv.extend(length.to_le_bytes());
rv.extend(0u32.to_le_bytes());
rv.extend(file_sha);
rv
}
pub fn multisig_enroll(length: u32, file_sha: &[u8; 32]) -> Vec<u8> {
let mut data = b"enrl".to_vec();
data.extend(length.to_le_bytes());
data.extend(file_sha);
data
}
pub fn get_signed_transaction() -> Vec<u8> {
b"stok".to_vec()
}
pub fn get_backup_file() -> Vec<u8> {
b"bkok".to_vec()
}
pub fn download(offset: u32, length: u32, file_number: u32) -> Vec<u8> {
let mut rv = b"dwld".to_vec();
rv.extend(offset.to_le_bytes());
rv.extend(length.to_le_bytes());
rv.extend(file_number.to_le_bytes());
rv
}
pub fn get_version() -> Vec<u8> {
b"vers".to_vec()
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use bitcoin::bip32::DerivationPath;
#[test]
fn test_derivation_path() {
let path = DerivationPath::from_str("m/48'/1'/0'/2'").unwrap();
assert_eq!("48'/1'/0'/2'", path.to_string());
}
#[test]
fn upload_request_encodes_offset_total_and_data() {
let req = super::request::upload(7, 11, b"psbt");
assert_eq!(&req[..4], b"upld");
assert_eq!(u32::from_le_bytes(req[4..8].try_into().unwrap()), 7);
assert_eq!(u32::from_le_bytes(req[8..12].try_into().unwrap()), 11);
assert_eq!(&req[12..], b"psbt");
}
#[test]
fn sign_transaction_request_encodes_length_flags_and_hash() {
let hash = [3u8; 32];
let req = super::request::sign_transaction(99, &hash);
assert_eq!(&req[..4], b"stxn");
assert_eq!(u32::from_le_bytes(req[4..8].try_into().unwrap()), 99);
assert_eq!(u32::from_le_bytes(req[8..12].try_into().unwrap()), 0);
assert_eq!(&req[12..], &hash);
}
#[test]
fn multisig_enroll_request_encodes_length_and_hash() {
let hash = [4u8; 32];
let req = super::request::multisig_enroll(321, &hash);
assert_eq!(&req[..4], b"enrl");
assert_eq!(u32::from_le_bytes(req[4..8].try_into().unwrap()), 321);
assert_eq!(&req[8..], &hash);
}
#[test]
fn p2sh_address_type_maps_to_nested_segwit_for_path_display() {
assert_eq!(
super::request::addr_fmt::from_address_type(bitcoin::address::AddressType::P2sh),
super::request::addr_fmt::AF_P2WPKH_P2SH
);
}
#[test]
fn backup_requests_encode_command_names() {
assert_eq!(super::request::start_backup(), b"back");
assert_eq!(super::request::get_backup_file(), b"bkok");
}
#[test]
fn upload_response_decodes_acknowledged_offset() {
let mut res = b"int1".to_vec();
res.extend(42u32.to_le_bytes());
assert_eq!(super::response::upload(&res).unwrap(), 42);
}
#[test]
fn signed_transaction_response_decodes_length_and_hash() {
let mut res = b"strx".to_vec();
res.extend(123u32.to_le_bytes());
res.extend([9u8; 32]);
match super::response::signed_transaction(&res).unwrap() {
super::response::SignedTransactionStatus::Complete { length, sha } => {
assert_eq!(length, 123);
assert_eq!(sha, [9u8; 32]);
}
super::response::SignedTransactionStatus::Pending => {
panic!("expected complete transaction")
}
}
}
#[test]
fn signed_transaction_response_treats_busy_as_pending() {
assert!(matches!(
super::response::signed_transaction(b"busy").unwrap(),
super::response::SignedTransactionStatus::Pending
));
}
#[test]
fn refu_frames_are_user_cancelled_errors() {
use crate::coldcard::ColdcardError;
assert!(matches!(
super::response::show_address(b"refu"),
Err(ColdcardError::UserCancelled)
));
assert!(matches!(
super::response::miniscript_address(b"refu"),
Err(ColdcardError::UserCancelled)
));
assert!(matches!(
super::response::sign_message(b"refu"),
Err(ColdcardError::UserCancelled)
));
assert!(matches!(
super::response::sign_transaction(b"refu"),
Err(ColdcardError::UserCancelled)
));
assert!(matches!(
super::response::signed_transaction(b"refu"),
Err(ColdcardError::UserCancelled)
));
}
#[test]
fn err_frames_during_signing_polls_are_device_errors() {
use crate::coldcard::ColdcardError;
for result in [
super::response::sign_transaction(b"err_No active request").map(|_| ()),
super::response::signed_transaction(b"err_No active request").map(|_| ()),
super::response::sign_message(b"err_No active request").map(|_| ()),
] {
match result {
Err(ColdcardError::Device(message)) => assert_eq!(message, "No active request"),
other => panic!("expected a device error, got {other:?}"),
}
}
}
#[test]
fn err_frames_keep_the_device_error_prefix() {
let error = match super::response::show_address(b"err_boom") {
Err(error) => error,
Ok(_) => panic!("expected a device error"),
};
assert_eq!(error.to_string(), "Coldcard Error: boom");
}
#[test]
fn every_parser_reports_refu_and_err_frames() {
use super::response;
use crate::coldcard::ColdcardError;
type Parser = fn(&[u8]) -> Result<(), ColdcardError>;
let parsers: [(&str, Parser); 8] = [
("get_xpub", |res| response::get_xpub(res).map(|_| ())),
("master_fingerprint", |res| {
response::master_fingerprint(res).map(|_| ())
}),
("upload", |res| response::upload(res).map(|_| ())),
("sha256", |res| response::sha256(res).map(|_| ())),
("okay", response::okay),
("download", |res| response::download(res).map(|_| ())),
("version", |res| response::version(res).map(|_| ())),
("mypub", |res| response::mypub(res).map(|_| ())),
];
for (name, parse) in parsers {
assert!(
matches!(parse(b"refu"), Err(ColdcardError::UserCancelled)),
"{name}"
);
assert!(
matches!(parse(b"err_boom"), Err(ColdcardError::Device(text)) if text == "boom"),
"{name}"
);
}
}
}
pub mod response {
use std::fmt::Display;
use std::str::FromStr;
use bitcoin::bip32::Xpub;
use bitcoin::secp256k1::ecdsa::Signature;
use crate::coldcard::{ColdcardError, ColdcardResponse};
pub enum SignedTransactionStatus {
Pending,
Complete {
length: u32,
sha: [u8; 32],
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseMessage {
Okay,
Fram,
Err_,
Refu,
Busy,
Biny,
Int1,
Int2,
Int3,
MyPb,
Asci,
Smrx,
Strx,
}
struct ResponseHandler<'a> {
message: ResponseMessage,
data: &'a [u8],
}
impl<'a> ResponseHandler<'a> {
fn parse_response(res: &[u8]) -> Result<(ResponseMessage, &[u8]), ColdcardError> {
ResponseHandler::try_from(res).map(|ResponseHandler { message, data }| (message, data))
}
fn expect_response(res: &[u8], expected: ResponseMessage) -> Result<&[u8], ColdcardError> {
match ResponseHandler::parse_response(res)? {
(msg, data) if msg == expected => Ok(data),
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(msg, _) => Err(ColdcardError::unexpected_response_message(msg, &[expected])),
}
}
}
impl Display for ResponseMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let response_str = match self {
ResponseMessage::Okay => "okay",
ResponseMessage::Fram => "fram",
ResponseMessage::Err_ => "err_",
ResponseMessage::Refu => "refu",
ResponseMessage::Busy => "busy",
ResponseMessage::Biny => "biny",
ResponseMessage::Int1 => "int1",
ResponseMessage::Int2 => "int2",
ResponseMessage::Int3 => "int3",
ResponseMessage::MyPb => "mypb",
ResponseMessage::Asci => "asci",
ResponseMessage::Smrx => "smrx",
ResponseMessage::Strx => "strx",
};
write!(f, "{}", response_str)
}
}
impl<'a> TryFrom<&'a [u8]> for ResponseHandler<'a> {
type Error = ColdcardError;
fn try_from(res: &'a [u8]) -> Result<Self, Self::Error> {
let (msg, data) = split(res, 4)?;
let message = match msg {
b"okay" => ResponseMessage::Okay,
b"fram" => ResponseMessage::Fram,
b"err_" => ResponseMessage::Err_,
b"refu" => ResponseMessage::Refu,
b"busy" => ResponseMessage::Busy,
b"biny" => ResponseMessage::Biny,
b"int1" => ResponseMessage::Int1,
b"int2" => ResponseMessage::Int2,
b"int3" => ResponseMessage::Int3,
b"mypb" => ResponseMessage::MyPb,
b"asci" => ResponseMessage::Asci,
b"smrx" => ResponseMessage::Smrx,
b"strx" => ResponseMessage::Strx,
_ => {
return Err(ColdcardError::Serialization(format!(
"unknown device response message: {:?}, data: {}",
String::from_utf8_lossy(msg),
String::from_utf8_lossy(data),
)));
}
};
Ok(ResponseHandler { message, data })
}
}
fn xpub(res: &[u8]) -> Result<Xpub, ColdcardError> {
let data = ResponseHandler::expect_response(res, ResponseMessage::Asci)?;
let s =
std::str::from_utf8(data).map_err(|e| ColdcardError::Serialization(e.to_string()))?;
Xpub::from_str(s).map_err(|e| ColdcardError::Serialization(e.to_string()))
}
pub fn show_address(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
match ResponseHandler::parse_response(res)? {
(ResponseMessage::Asci, data) => {
let address = String::from_utf8(data.to_owned())?;
Ok(ColdcardResponse::Address(address))
}
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(msg, _) => Err(ColdcardError::unexpected_response_message(
msg,
&[ResponseMessage::Asci, ResponseMessage::Refu],
)),
}
}
pub fn miniscript_address(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
match ResponseHandler::parse_response(res)? {
(ResponseMessage::Asci, data) => {
let address = String::from_utf8(data.to_owned())?;
Ok(ColdcardResponse::Address(address))
}
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(msg, _) => Err(ColdcardError::unexpected_response_message(
msg,
&[ResponseMessage::Asci, ResponseMessage::Refu],
)),
}
}
pub fn master_fingerprint(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
Ok(ColdcardResponse::MasterFingerprint(
xpub(res)?.fingerprint(),
))
}
pub fn get_xpub(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
Ok(ColdcardResponse::Xpub(xpub(res)?))
}
pub fn upload(res: &[u8]) -> Result<u32, ColdcardError> {
let data = ResponseHandler::expect_response(res, ResponseMessage::Int1)?;
decode_u32(Some(data))
}
pub fn sha256(res: &[u8]) -> Result<[u8; 32], ColdcardError> {
let data = ResponseHandler::expect_response(res, ResponseMessage::Biny)?;
data.try_into()
.map_err(|_| ColdcardError::Serialization("sha256".to_string()))
}
pub fn sign_transaction(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
match ResponseHandler::parse_response(res)? {
(ResponseMessage::Okay, _) => Ok(ColdcardResponse::Ok),
(ResponseMessage::Busy, _) => Ok(ColdcardResponse::Busy),
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(msg, _) => Err(ColdcardError::unexpected_response_message(
msg,
&[ResponseMessage::Okay, ResponseMessage::Busy],
)),
}
}
pub fn okay(res: &[u8]) -> Result<(), ColdcardError> {
ResponseHandler::expect_response(res, ResponseMessage::Okay).map(|_| ())
}
pub fn signed_transaction(res: &[u8]) -> Result<SignedTransactionStatus, ColdcardError> {
match ResponseHandler::parse_response(res)? {
(ResponseMessage::Okay, _) | (ResponseMessage::Busy, _) => {
Ok(SignedTransactionStatus::Pending)
}
(ResponseMessage::Strx, data) => {
let (len, sha) = split(data, 4)?;
let length = decode_u32(Some(len))?;
let sha = sha
.try_into()
.map_err(|_| ColdcardError::Serialization("signed transaction sha".into()))?;
Ok(SignedTransactionStatus::Complete { length, sha })
}
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(msg, _) => Err(ColdcardError::unexpected_response_message(
msg,
&[
ResponseMessage::Okay,
ResponseMessage::Busy,
ResponseMessage::Strx,
],
)),
}
}
pub fn download(res: &[u8]) -> Result<Vec<u8>, ColdcardError> {
Ok(ResponseHandler::expect_response(res, ResponseMessage::Biny)?.to_vec())
}
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)?;
let encryption_key = dev_pubkey
.try_into()
.map_err(|_| ColdcardError::Serialization("encryption_key".to_string()))?;
let xpub_fingerprint = data
.get(0..4)
.ok_or(ColdcardError::Serialization(
"xfp wants 4 bytes".to_string(),
))?
.try_into()
.expect("infallible");
let xpub_len = decode_u32(data.get(4..8))? as usize;
let xpub = if xpub_len > 0 {
if let Some(s) = data
.get(8..8 + xpub_len)
.map(|d| String::from_utf8(d.to_owned()))
.transpose()
.map_err(|e| ColdcardError::Serialization(e.to_string()))?
{
Some(Xpub::from_str(&s).map_err(|e| ColdcardError::Serialization(e.to_string()))?)
} else {
None
}
} else {
None
};
Ok(ColdcardResponse::MyPub {
encryption_key,
xpub_fingerprint,
xpub,
})
}
pub fn sign_message(res: &[u8]) -> Result<ColdcardResponse, ColdcardError> {
match ResponseHandler::parse_response(res)? {
(ResponseMessage::Okay, _) => Ok(ColdcardResponse::Ok),
(ResponseMessage::Refu, _) => Err(ColdcardError::UserCancelled),
(ResponseMessage::Err_, data) => Err(ColdcardError::Device(
String::from_utf8_lossy(data).into_owned(),
)),
(ResponseMessage::Smrx, data) => {
let addr_len = u32::from_le_bytes(data[..4].try_into().map_err(|_| {
ColdcardError::Serialization("couldn't parse address length into u32".into())
})?);
let (_addr, sig_bytes) = split(
&data[4..],
addr_len.try_into().map_err(|_| {
ColdcardError::Serialization("address length can't fit into usize".into())
})?,
)?;
let sig = Signature::from_compact(&sig_bytes[1..])
.map_err(|e| ColdcardError::Serialization(e.to_string()))?;
Ok(ColdcardResponse::Signature(sig_bytes[0], sig))
}
(ResponseMessage::Busy, _) => Ok(ColdcardResponse::Busy),
(msg, _) => Err(ColdcardError::unexpected_response_message(
msg,
&[
ResponseMessage::Okay,
ResponseMessage::Busy,
ResponseMessage::Smrx,
],
)),
}
}
fn split(bytes: &[u8], mid: usize) -> Result<(&[u8], &[u8]), ColdcardError> {
match bytes.len().cmp(&mid) {
std::cmp::Ordering::Less => Err(ColdcardError::Serialization(
"unexpected slice length".to_string(),
)),
_ => Ok(bytes.split_at(mid)),
}
}
fn decode_u32(bytes: Option<&[u8]>) -> Result<u32, ColdcardError> {
match bytes {
Some(bytes) if bytes.len() == 4 => {
Ok(u32::from_le_bytes(bytes.try_into().map_err(|_| {
ColdcardError::Serialization("u32".to_string())
})?))
}
_ => Err(ColdcardError::Serialization("u32".to_string())),
}
}
}