rust-cktap 0.1.0

A Rust implementation of the Coinkite Tap Protocol (cktap) for use with SATSCARD, TAPSIGNER, and SATSCHIP products.
Documentation
// Copyright (c) 2025 rust-cktap contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

use super::{CommandApdu, ResponseApdu};
use core::fmt::{self, Formatter};

use bitcoin::secp256k1;
use bitcoin_hashes::hex::DisplayHex as _;
use serde::{Deserialize, Serialize};

/// TAPSIGNER only - Provides the current XPUB (BIP-32 serialized), either at the top level (master)
/// or the derived key in use (see 'path' value in status response)
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct XpubCommand {
    cmd: &'static str, // always "xpub"
    master: bool,      // give master (`m`) XPUB, otherwise derived XPUB
    #[serde(with = "serde_bytes")]
    epubkey: [u8; 33], // app's ephemeral public key (required)
    #[serde(with = "serde_bytes")]
    xcvc: Vec<u8>, //encrypted CVC value (required)
}

impl CommandApdu for XpubCommand {
    fn name() -> &'static str {
        "xpub"
    }
}

impl XpubCommand {
    pub fn new(master: bool, epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
        Self {
            cmd: Self::name(),
            master,
            epubkey: epubkey.serialize(),
            xcvc,
        }
    }
}

#[derive(Deserialize, Clone)]
pub struct XpubResponse {
    #[serde(with = "serde_bytes")]
    pub xpub: Vec<u8>,
    #[serde(with = "serde_bytes")]
    pub card_nonce: [u8; 16],
}

impl ResponseApdu for XpubResponse {}

impl std::fmt::Debug for XpubResponse {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.debug_struct("XpubResponse")
            .field("xpub", &self.xpub.to_lower_hex_string())
            .field("card_nonce", &self.card_nonce.to_lower_hex_string())
            .finish()
    }
}

/// TAPSIGNER only - Change the PIN (CVC) used for card authentication to a new user provided one
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct ChangeCommand {
    // always "change"
    cmd: &'static str,

    // new cvc value (required) (6 - 32 bytes)
    #[serde(with = "serde_bytes")]
    data: Vec<u8>,

    // app's ephemeral public key (required)
    #[serde(with = "serde_bytes")]
    epubkey: [u8; 33],

    //encrypted CVC value (required) (6-32 bytes)
    #[serde(with = "serde_bytes")]
    xcvc: Vec<u8>,
}

impl CommandApdu for ChangeCommand {
    fn name() -> &'static str {
        "change"
    }
}

impl ChangeCommand {
    pub fn new(data: Vec<u8>, epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
        Self {
            cmd: Self::name(),
            data,
            epubkey: epubkey.serialize(),
            xcvc,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ChangeResponse {
    pub success: bool,
    #[serde(with = "serde_bytes")]
    pub card_nonce: [u8; 16],
}

impl ResponseApdu for ChangeResponse {}

/// TAPSIGNER only - Get an encrypted backup of the card's private key

#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct BackupCommand {
    // always "backup"
    cmd: &'static str,

    // app's ephemeral public key (required)
    #[serde(with = "serde_bytes")]
    epubkey: [u8; 33],

    //encrypted CVC value (required)
    #[serde(with = "serde_bytes")]
    xcvc: Vec<u8>,
}

impl CommandApdu for BackupCommand {
    fn name() -> &'static str {
        "backup"
    }
}

impl BackupCommand {
    pub fn new(epubkey: secp256k1::PublicKey, xcvc: Vec<u8>) -> Self {
        Self {
            cmd: Self::name(),
            epubkey: epubkey.serialize(),
            xcvc,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct BackupResponse {
    #[serde(with = "serde_bytes")]
    pub data: Vec<u8>,
    #[serde(with = "serde_bytes")]
    pub card_nonce: [u8; 16],
}

impl ResponseApdu for BackupResponse {}