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
25 changes: 22 additions & 3 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ struct SatsCardCli {
/// Commands supported by SatsCard cards
#[derive(Subcommand)]
enum SatsCardCommand {
/// Show the card status
Debug,
/// Show current deposit address
Address,
/// Check this card was made by Coinkite: Verifies a certificate chain up to root factory key.
Certs,
/// Read the pubkey, CVC not required
/// Read the pubkey
Read,
}

Expand All @@ -36,9 +40,11 @@ struct TapSignerCli {
/// Commands supported by TapSigner cards
#[derive(Subcommand)]
enum TapSignerCommand {
/// Show the card status
Debug,
/// Check this card was made by Coinkite: Verifies a certificate chain up to root factory key.
Certs,
/// Read the pubkey, CVC required
/// Read the pubkey (requires CVC)
Read,
}

Expand All @@ -50,13 +56,20 @@ fn main() -> Result<(), Error> {
CkTapCard::SatsCard(sc) => {
let cli = SatsCardCli::parse();
match cli.command {
SatsCardCommand::Debug => {
dbg!(&sc);
}
SatsCardCommand::Address => println!("Address: {}", sc.address().unwrap()),
SatsCardCommand::Certs => check_cert(sc),
SatsCardCommand::Read => read(sc, None),
}
}
CkTapCard::TapSigner(ts) | CkTapCard::SatsChip(ts) => {
let cli = TapSignerCli::parse();
match cli.command {
TapSignerCommand::Debug => {
dbg!(&ts);
}
TapSignerCommand::Certs => check_cert(ts),
TapSignerCommand::Read => read(ts, Some(cvc())),
}
Expand All @@ -80,7 +93,13 @@ fn check_cert<T: CkTransport>(card: &mut dyn Certificate<T>) {
}

fn read<T: CkTransport>(card: &mut dyn Read<T>, cvc: Option<String>) {
println!("{:?}", card.read(cvc))
match card.read(cvc) {
Ok(resp) => println!("{}", resp),
Err(e) => {
dbg!(&e);
println!("Failed to read with error: ")
}
}
}

fn cvc() -> String {
Expand Down
3 changes: 2 additions & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ secp256k1 = { version = "0.26.0", features = ["rand-std", "bitcoin-hashes-std",

# optional dependencies
pcsc = { version = "2", optional = true }
# bech32 = "0.9.1"

[[example]]
name = "pcsc"
required-features = ["pcsc"]
required-features = ["pcsc"]
12 changes: 6 additions & 6 deletions lib/src/apdu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ pub struct ReadResponse {

impl ResponseApdu for ReadResponse {}

impl fmt::Display for ReadResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "pubkey: {}", self.pubkey.to_hex())
}
}

impl Debug for ReadResponse {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("ReadResponse")
Expand All @@ -236,12 +242,6 @@ impl Debug for ReadResponse {
}
}

// impl ReadResponse {
// pub fn pubkey(&self) -> PublicKey {
// PublicKey::from_slice(self.pubkey.as_slice()).unwrap()
// }
// }

// Checks payment address derivation: https://github.com/coinkite/coinkite-tap-proto/blob/master/docs/protocol.md#satscard-checks-payment-address-derivation
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct DeriveCommand {
Expand Down
68 changes: 59 additions & 9 deletions lib/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use secp256k1::ecdh::SharedSecret;
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId, Signature};
use secp256k1::hashes::{sha256, Hash};
use secp256k1::{rand, All, Message, PublicKey, Secp256k1, SecretKey};

use std::convert::TryFrom;

use std::fmt::Debug;
Expand Down Expand Up @@ -87,19 +88,56 @@ where
T: CkTransport,
{
fn requires_auth(&self) -> bool;
fn slot(&self) -> Option<u8>;

fn read(&mut self, cvc: Option<String>) -> Result<ReadResponse, Error> {
let cmd = if self.requires_auth() {
let (_, epubkey, xcvc) = self.calc_ekeys_xcvc(cvc.unwrap(), &ReadCommand::name());
ReadCommand::authenticated(self.card_nonce().clone(), epubkey, xcvc)
let card_nonce = self.card_nonce().clone();
let rng = &mut rand::thread_rng();
let nonce = rand_nonce(rng).to_vec();

if self.requires_auth() {
let (eprivkey, epubkey, xcvc) =
self.calc_ekeys_xcvc(cvc.unwrap(), &ReadCommand::name());
let cmd = ReadCommand::authenticated(nonce.clone(), epubkey, xcvc);

let read_response: Result<ReadResponse, Error> = self.transport().transmit(cmd);
if let Ok(response) = &read_response {
let message = self.message_digest(card_nonce, nonce);
let signature = Signature::from_compact(response.sig.as_slice())
.expect("Failed to construct ECDSA signature from check response");
let session_key = SharedSecret::new(self.pubkey(), &eprivkey);
let pubkey = unzip(&mut response.pubkey.clone(), session_key)?;
self.secp().verify_ecdsa(&message, &signature, &pubkey)?;
self.set_card_nonce(response.card_nonce.clone());
}
read_response
} else {
ReadCommand::unauthenticated(self.card_nonce().clone())
};
let read_response: Result<ReadResponse, Error> = self.transport().transmit(cmd);
if let Ok(response) = &read_response {
self.set_card_nonce(response.card_nonce.clone());
let cmd = ReadCommand::unauthenticated(nonce.clone());
let read_response: Result<ReadResponse, Error> = self.transport().transmit(cmd);
if let Ok(response) = &read_response {
let message = self.message_digest(card_nonce, nonce);
let signature = Signature::from_compact(response.sig.as_slice())
.expect("Failed to construct ECDSA signature from check response");
let pubkey: PublicKey =
PublicKey::from_slice(response.pubkey.clone().as_slice()).unwrap();
self.secp().verify_ecdsa(&message, &signature, &pubkey)?;
self.set_card_nonce(response.card_nonce.clone());
}
read_response
}
read_response
}

fn message_digest(&self, card_nonce: Vec<u8>, app_nonce: Vec<u8>) -> Message {
let mut message_bytes: Vec<u8> = Vec::new();
message_bytes.extend("OPENDIME".as_bytes());
message_bytes.extend(card_nonce);
message_bytes.extend(app_nonce);
if let Some(slot) = self.slot() {
message_bytes.push(slot);
} else {
message_bytes.push(0);
}
Message::from_hashed_data::<sha256::Hash>(message_bytes.as_slice())
}
}

Expand Down Expand Up @@ -187,6 +225,18 @@ where
}
}

fn unzip(encoded: &mut Vec<u8>, session_key: SharedSecret) -> Result<PublicKey, secp256k1::Error> {
let session_key = session_key.as_ref(); // 32 bytes
let mut pubkey = encoded.clone();
let xor_bytes = encoded.split_off(1);
let xor_bytes = xor_bytes
.iter()
.zip(session_key.as_ref())
.map(|(x, y)| x ^ y);
pubkey.splice(1..33, xor_bytes);
PublicKey::from_slice(pubkey.as_slice())
}

// #[cfg(test)]
// mod tests {
// use super::*;
Expand Down
19 changes: 19 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use secp256k1::rand::Rng;
use secp256k1::{All, Message, PublicKey, Secp256k1};
use std::fmt;
use std::fmt::Debug;
// use bech32::{self, FromBase32, ToBase32, Variant};

pub mod apdu;
pub mod commands;
Expand Down Expand Up @@ -138,6 +139,10 @@ impl<T: CkTransport> Read<T> for TapSigner<T> {
fn requires_auth(&self) -> bool {
true
}

fn slot(&self) -> Option<u8> {
None
}
}

impl<T: CkTransport> Certificate<T> for TapSigner<T> {
Expand Down Expand Up @@ -285,6 +290,17 @@ impl<T: CkTransport> SatsCard<T> {
let dump_command = DumpCommand::new(slot, epubkey, xcvc);
self.transport.transmit(dump_command)
}

pub fn address(&mut self) -> Result<String, Error> {
// let pubkey = self.read(None).unwrap().pubkey;
// // let hrp = match self.testnet() { }
// let encoded = bech32::encode("bc", pubkey.to_base32(), Variant::Bech32);
// match encoded {
// Ok(e) => Ok(e),
// Err(_) => panic!("Failed to encoded pubkey")
// }
todo!()
}
}

impl<T: CkTransport> Wait<T> for SatsCard<T> {}
Expand All @@ -293,6 +309,9 @@ impl<T: CkTransport> Read<T> for SatsCard<T> {
fn requires_auth(&self) -> bool {
false
}
fn slot(&self) -> Option<u8> {
Some(self.slots.0 as u8)
}
}

impl<T: CkTransport> Certificate<T> for SatsCard<T> {
Expand Down