pub mod tap_signer;
use crate::error::{ErrorResponse, ReadError};
use crate::{CardError, CkTapError};
use bitcoin::bip32::ChainCode;
use bitcoin::secp256k1::{self, ecdh::SharedSecret, ecdsa::Signature};
use bitcoin::{Network, PrivateKey, PublicKey};
use bitcoin_hashes::hex::DisplayHex;
use ciborium::de::from_reader;
use ciborium::ser::into_writer;
use ciborium::value::Value;
use serde::{Deserialize, Serialize, Serializer};
use serde_bytes::ByteBuf;
use std::fmt;
use std::fmt::{Debug, Formatter};
const APP_ID: [u8; 15] = *b"\xf0CoinkiteCARDv1";
const SELECT_CLA_INS_P1P2: [u8; 4] = [0x00, 0xA4, 0x04, 0x00];
const CBOR_CLA_INS_P1P2: [u8; 4] = [0x00, 0xCB, 0x00, 0x00];
pub trait CommandApdu {
fn name() -> &'static str;
fn apdu_bytes(&self) -> Vec<u8>
where
Self: serde::Serialize + Debug,
{
let mut command = Vec::new();
into_writer(&self, &mut command).unwrap();
build_apdu(&CBOR_CLA_INS_P1P2, command.as_slice())
}
}
pub trait ResponseApdu {
fn from_cbor<'a>(cbor: Vec<u8>) -> Result<Self, CkTapError>
where
Self: Deserialize<'a> + Debug,
{
let cbor_value: Value = from_reader(&cbor[..])?;
let cbor_struct: Result<ErrorResponse, _> = cbor_value.deserialized();
if let Ok(error_resp) = cbor_struct {
let error = CardError::error_from_code(error_resp.code).unwrap_or(CardError::BadCBOR);
return Err(CkTapError::Card(error));
}
let cbor_struct: Self = cbor_value.deserialized()?;
Ok(cbor_struct)
}
}
fn build_apdu(header: &[u8], command: &[u8]) -> Vec<u8> {
let command_len = command.len();
assert!(command_len <= 255, "apdu command too long"); [header, &[command_len as u8], command].concat()
}
#[derive(Default, Serialize, Clone, Debug, PartialEq, Eq)]
pub struct AppletSelect {}
impl CommandApdu for AppletSelect {
fn name() -> &'static str {
""
}
fn apdu_bytes(&self) -> Vec<u8> {
build_apdu(&SELECT_CLA_INS_P1P2, &APP_ID)
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct StatusCommand {
cmd: &'static str,
}
impl Default for StatusCommand {
fn default() -> Self {
StatusCommand { cmd: Self::name() }
}
}
impl CommandApdu for StatusCommand {
fn name() -> &'static str {
"status"
}
}
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct StatusResponse {
pub proto: usize,
pub ver: String,
pub birth: usize,
pub slots: Option<(u8, u8)>,
pub addr: Option<String>,
pub tapsigner: Option<bool>,
pub satschip: Option<bool>,
pub path: Option<Vec<usize>>,
pub num_backups: Option<usize>,
#[serde(with = "serde_bytes")]
pub pubkey: Vec<u8>,
#[serde(with = "serde_bytes")]
pub card_nonce: [u8; 16],
pub testnet: Option<bool>,
#[serde(default)]
pub auth_delay: Option<usize>,
}
impl ResponseApdu for StatusResponse {}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct ReadCommand {
cmd: &'static str,
#[serde(with = "serde_bytes")]
nonce: [u8; 16],
#[serde(with = "serde_bytes")]
epubkey: Option<[u8; 33]>,
#[serde(with = "serde_bytes")]
xcvc: Option<Vec<u8>>,
}
impl ReadCommand {
pub fn authenticated(nonce: [u8; 16], epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
ReadCommand {
cmd: Self::name(),
nonce,
epubkey: Some(epubkey.serialize()),
xcvc: Some(xcvc),
}
}
pub fn unauthenticated(nonce: [u8; 16]) -> Self {
ReadCommand {
cmd: Self::name(),
nonce,
epubkey: None,
xcvc: None,
}
}
}
impl CommandApdu for ReadCommand {
fn name() -> &'static str {
"read"
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct ReadResponse {
#[serde(with = "serde_bytes")]
sig: Vec<u8>,
#[serde(with = "serde_bytes")]
pubkey: Vec<u8>,
#[serde(with = "serde_bytes")]
pub(crate) card_nonce: [u8; 16],
}
impl ResponseApdu for ReadResponse {}
impl ReadResponse {
pub fn signature(&self) -> Result<Signature, ReadError> {
Signature::from_compact(self.sig.as_slice()).map_err(ReadError::from)
}
pub fn pubkey(&self, session_key: Option<SharedSecret>) -> Result<PublicKey, ReadError> {
if let Some(sk) = session_key {
let pubkey_bytes = unzip(&self.pubkey, sk);
return PublicKey::from_slice(pubkey_bytes.as_slice()).map_err(ReadError::from);
};
let pubkey_bytes = self.pubkey.as_slice();
PublicKey::from_slice(pubkey_bytes).map_err(ReadError::from)
}
}
fn unzip(encoded: &[u8], session_key: SharedSecret) -> Vec<u8> {
let zipped_bytes = encoded.to_owned().split_off(1);
let unzipped_bytes = zipped_bytes
.iter()
.zip(session_key.as_ref())
.map(|(x, y)| x ^ y);
let mut pubkey = encoded.to_owned();
pubkey.splice(1..33, unzipped_bytes);
pubkey
}
impl fmt::Display for ReadResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "pubkey: {}", self.pubkey.to_lower_hex_string())
}
}
impl Debug for ReadResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("ReadResponse")
.field("sig", &self.sig.to_lower_hex_string())
.field("pubkey", &self.pubkey.to_lower_hex_string())
.field("card_nonce", &self.card_nonce.to_lower_hex_string())
.finish()
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct DeriveCommand {
cmd: &'static str,
#[serde(with = "serde_bytes")]
nonce: [u8; 16],
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_some"
)]
path: Option<Vec<u32>>, #[serde(with = "serde_bytes")]
epubkey: Option<[u8; 33]>,
#[serde(with = "serde_bytes")]
xcvc: Option<Vec<u8>>,
}
impl CommandApdu for DeriveCommand {
fn name() -> &'static str {
"derive"
}
}
impl DeriveCommand {
pub fn for_satscard(nonce: [u8; 16]) -> Self {
DeriveCommand {
cmd: Self::name(),
nonce,
path: None,
epubkey: None,
xcvc: None,
}
}
pub fn for_tapsigner(
nonce: [u8; 16],
path: Vec<u32>,
epubkey: secp256k1::PublicKey,
xcvc: Vec<u8>,
) -> Self {
DeriveCommand {
cmd: Self::name(),
nonce,
path: Some(path),
epubkey: Some(epubkey.serialize()),
xcvc: Some(xcvc),
}
}
}
#[derive(Deserialize, Clone)]
pub struct DeriveResponse {
#[serde(with = "serde_bytes")]
pub sig: [u8; 64],
#[serde(with = "serde_bytes")]
pub chain_code: [u8; 32],
#[serde(with = "serde_bytes")]
pub master_pubkey: [u8; 33],
#[serde(with = "serde_bytes")]
#[serde(default = "Option::default")]
pub pubkey: Option<[u8; 33]>, #[serde(with = "serde_bytes")]
pub card_nonce: [u8; 16],
}
impl ResponseApdu for DeriveResponse {}
impl Debug for DeriveResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("DeriveResponse")
.field("sig", &self.sig.to_lower_hex_string())
.field("chain_code", &self.chain_code.to_lower_hex_string())
.field("master_pubkey", &self.master_pubkey.to_lower_hex_string())
.field("pubkey", &self.pubkey.map(|pk| pk.to_lower_hex_string()))
.field("card_nonce", &self.card_nonce.to_lower_hex_string())
.finish()
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct CertsCommand {
cmd: &'static str,
}
impl CommandApdu for CertsCommand {
fn name() -> &'static str {
"certs"
}
}
impl Default for CertsCommand {
fn default() -> Self {
CertsCommand { cmd: Self::name() }
}
}
#[derive(Deserialize, Clone)]
pub struct CertsResponse {
cert_chain: Vec<ByteBuf>,
}
impl ResponseApdu for CertsResponse {}
impl CertsResponse {
pub fn cert_chain(&self) -> Vec<Vec<u8>> {
self.clone()
.cert_chain
.into_iter()
.map(|bb| bb.to_vec())
.collect()
}
}
impl Debug for CertsResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let cert_hexes: Vec<String> = self
.cert_chain()
.iter()
.map(|key| key.to_lower_hex_string())
.collect();
f.debug_struct("CertsResponse")
.field("cert_chain", &cert_hexes)
.finish()
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct CheckCommand {
cmd: &'static str,
#[serde(with = "serde_bytes")]
nonce: [u8; 16],
}
impl CommandApdu for CheckCommand {
fn name() -> &'static str {
"check"
}
}
impl CheckCommand {
pub fn new(nonce: [u8; 16]) -> Self {
CheckCommand {
cmd: Self::name(),
nonce,
}
}
}
#[derive(Deserialize, Clone)]
pub struct CheckResponse {
#[serde(with = "serde_bytes")]
pub(crate) auth_sig: Vec<u8>,
#[serde(with = "serde_bytes")]
pub(crate) card_nonce: [u8; 16],
}
impl ResponseApdu for CheckResponse {}
impl Debug for CheckResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("CheckResponse")
.field("auth_sig", &self.auth_sig.to_lower_hex_string())
.field("card_nonce", &self.card_nonce.to_lower_hex_string())
.finish()
}
}
#[allow(unused)]
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct NfcCommand {
cmd: &'static str,
}
impl Default for NfcCommand {
fn default() -> Self {
Self { cmd: Self::name() }
}
}
impl CommandApdu for NfcCommand {
fn name() -> &'static str {
"nfc"
}
}
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NfcResponse {
pub url: String,
}
impl ResponseApdu for NfcResponse {}
pub(crate) fn serialize_some<T, S>(opt: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
opt.as_ref().unwrap().serialize(serializer)
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct SignCommand {
cmd: &'static str,
slot: Option<u8>,
#[serde(
rename = "subpath",
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_some"
)]
sub_path: Option<Vec<u32>>,
#[serde(with = "serde_bytes")]
digest: [u8; 32],
#[serde(with = "serde_bytes")]
epubkey: [u8; 33],
#[serde(with = "serde_bytes")]
xcvc: Vec<u8>,
}
impl SignCommand {
pub fn for_satscard(
slot: u8,
digest: [u8; 32],
epubkey: secp256k1::PublicKey,
xcvc: Vec<u8>,
) -> Self {
SignCommand {
cmd: Self::name(),
slot: Some(slot),
sub_path: None, digest,
epubkey: epubkey.serialize(),
xcvc,
}
}
pub fn for_tapsigner(
sub_path: Vec<u32>,
digest: [u8; 32],
epubkey: secp256k1::PublicKey,
xcvc: Vec<u8>,
) -> Self {
SignCommand {
cmd: Self::name(),
slot: Some(0),
sub_path: Some(sub_path), digest,
epubkey: epubkey.serialize(),
xcvc,
}
}
}
impl CommandApdu for SignCommand {
fn name() -> &'static str {
"sign"
}
}
#[derive(Deserialize, Clone, PartialEq, Eq)]
pub struct SignResponse {
pub slot: u8,
#[serde(with = "serde_bytes")]
pub sig: [u8; 64],
#[serde(with = "serde_bytes")]
pub pubkey: [u8; 33],
#[serde(with = "serde_bytes")]
pub card_nonce: [u8; 16],
}
impl ResponseApdu for SignResponse {}
impl Debug for SignResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("SignResponse")
.field("slot", &self.slot)
.field("sig", &self.sig.to_lower_hex_string())
.field("pubkey", &self.pubkey.to_lower_hex_string())
.field("card_nonce", &self.card_nonce.to_lower_hex_string())
.finish()
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct WaitCommand {
cmd: &'static str,
#[serde(with = "serde_bytes")]
epubkey: Option<[u8; 33]>,
#[serde(with = "serde_bytes")]
xcvc: Option<Vec<u8>>,
}
impl WaitCommand {
pub fn new(epubkey: Option<secp256k1::PublicKey>, xcvc: Option<Vec<u8>>) -> Self {
WaitCommand {
cmd: Self::name(),
epubkey: epubkey.map(|pk| pk.serialize()),
xcvc,
}
}
}
impl CommandApdu for WaitCommand {
fn name() -> &'static str {
"wait"
}
}
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct WaitResponse {
success: bool,
#[serde(default)]
pub(crate) auth_delay: usize,
}
impl ResponseApdu for WaitResponse {}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct NewCommand {
cmd: &'static str,
slot: u8,
#[serde(with = "serde_bytes")]
chain_code: Option<[u8; 32]>, #[serde(with = "serde_bytes")]
epubkey: [u8; 33],
#[serde(with = "serde_bytes")]
xcvc: Vec<u8>, }
impl NewCommand {
pub fn new(
slot: Option<u8>,
chain_code: Option<ChainCode>,
epubkey: secp256k1::PublicKey,
xcvc: Vec<u8>,
) -> Self {
let slot = slot.unwrap_or_default();
let chain_code = chain_code.map(|cc| cc.to_bytes());
NewCommand {
cmd: Self::name(),
slot,
chain_code,
epubkey: epubkey.serialize(),
xcvc,
}
}
}
impl CommandApdu for NewCommand {
fn name() -> &'static str {
"new"
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct NewResponse {
pub(crate) slot: u8,
#[serde(with = "serde_bytes")]
pub(crate) card_nonce: [u8; 16], }
impl ResponseApdu for NewResponse {}
impl fmt::Display for NewResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "slot {}", self.slot)
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct UnsealCommand {
cmd: &'static str,
slot: u8,
#[serde(with = "serde_bytes")]
epubkey: [u8; 33],
#[serde(with = "serde_bytes")]
xcvc: Vec<u8>,
}
impl UnsealCommand {
pub fn new(slot: u8, epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
UnsealCommand {
cmd: Self::name(),
slot,
epubkey: epubkey.serialize(),
xcvc,
}
}
}
impl CommandApdu for UnsealCommand {
fn name() -> &'static str {
"unseal"
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct UnsealResponse {
pub slot: u8,
#[serde(with = "serde_bytes")]
pub privkey: Vec<u8>,
#[serde(with = "serde_bytes")]
pub pubkey: Vec<u8>,
#[serde(with = "serde_bytes")]
pub master_pk: Vec<u8>,
#[allow(unused)]
#[serde(with = "serde_bytes")]
pub chain_code: Vec<u8>, #[serde(with = "serde_bytes")]
pub card_nonce: [u8; 16],
}
impl ResponseApdu for UnsealResponse {}
impl fmt::Display for UnsealResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let master = PublicKey::from_slice(self.master_pk.as_slice()).unwrap();
let pubkey = PublicKey::from_slice(self.pubkey.as_slice()).unwrap();
let privkey = PrivateKey::from_slice(self.privkey.as_slice(), Network::Bitcoin).unwrap();
writeln!(f, "slot: {}", self.slot)?;
writeln!(f, "master_pk: {master}")?;
writeln!(f, "pubkey: {pubkey}")?;
writeln!(f, "privkey: {}", privkey.to_wif())
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct DumpCommand {
cmd: &'static str,
slot: u8,
#[serde(with = "serde_bytes")]
#[serde(skip_serializing_if = "Option::is_none")]
epubkey: Option<[u8; 33]>,
#[serde(with = "serde_bytes")]
#[serde(skip_serializing_if = "Option::is_none")]
xcvc: Option<Vec<u8>>,
}
impl DumpCommand {
pub fn new(slot: u8, epubkey: Option<secp256k1::PublicKey>, xcvc: Option<Vec<u8>>) -> Self {
DumpCommand {
cmd: Self::name(),
slot,
epubkey: epubkey.map(|pk| pk.serialize()),
xcvc,
}
}
}
impl CommandApdu for DumpCommand {
fn name() -> &'static str {
"dump"
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct DumpResponse {
#[allow(unused)] pub slot: usize,
#[serde(with = "serde_bytes")]
#[serde(default)]
pub privkey: Option<Vec<u8>>,
#[serde(with = "serde_bytes")]
#[serde(default)]
pub pubkey: Option<Vec<u8>>,
#[allow(unused)] #[serde(with = "serde_bytes")]
#[serde(default)]
pub chain_code: Option<Vec<u8>>,
#[allow(unused)] #[serde(with = "serde_bytes")]
#[serde(default)]
pub master_pk: Option<Vec<u8>>,
#[serde(default)]
pub tampered: Option<bool>,
#[serde(default)]
pub used: Option<bool>,
pub sealed: Option<bool>,
#[allow(unused)] #[serde(default)]
pub addr: Option<String>,
#[serde(with = "serde_bytes")]
pub card_nonce: [u8; 16],
}
impl ResponseApdu for DumpResponse {}