From 1e3fa35bdf7a49a87e2a17adf8636fb6c7fdbb02 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 3 Jul 2021 00:15:32 +0800 Subject: [PATCH] feat: u2f --- Cargo.lock | 159 +++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/digest.rs | 7 +++ src/fido.rs | 62 +++++++++++++++++++ src/main.rs | 11 +++- src/register.rs | 109 ++++++++++++--------------------- src/sign.rs | 88 +++++++++++---------------- 7 files changed, 311 insertions(+), 126 deletions(-) create mode 100644 src/digest.rs create mode 100644 src/fido.rs diff --git a/Cargo.lock b/Cargo.lock index e10d001..83480a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + [[package]] name = "base64" version = "0.13.0" @@ -58,12 +70,47 @@ dependencies = [ "generic-array", ] +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "cc" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + [[package]] name = "clap" version = "2.33.3" @@ -144,6 +191,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "generic-array" version = "0.14.4" @@ -191,6 +253,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "0.4.7" @@ -254,12 +325,64 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "pkg-config" version = "0.3.19" @@ -511,12 +634,39 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "typenum" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "u2f" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f285392366190c4d46823458f4543ac0f35174759c78e80c5baa39e1f7aa4f" +dependencies = [ + "base64 0.11.0", + "byteorder", + "bytes", + "chrono", + "openssl", + "serde", + "serde_derive", + "serde_json", + "time", +] + [[package]] name = "unicode-width" version = "0.1.8" @@ -529,6 +679,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -558,7 +714,7 @@ name = "webauthn-cli" version = "0.1.0" dependencies = [ "authenticator", - "base64", + "base64 0.13.0", "clap", "hex", "rand 0.8.4", @@ -566,6 +722,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "u2f", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f09276e..2a85a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ base64 = "0.13" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" hex = "0.4" +u2f = "0.2" diff --git a/src/digest.rs b/src/digest.rs new file mode 100644 index 0000000..afd058a --- /dev/null +++ b/src/digest.rs @@ -0,0 +1,7 @@ +use sha2::{Digest, Sha256}; + +pub fn sha256(input: &str) -> Vec { + let mut challenge = Sha256::default(); + Digest::update(&mut challenge, input.as_bytes()); + challenge.finalize().to_vec() +} \ No newline at end of file diff --git a/src/fido.rs b/src/fido.rs new file mode 100644 index 0000000..1b97e52 --- /dev/null +++ b/src/fido.rs @@ -0,0 +1,62 @@ +use std::thread; +use std::sync::mpsc::{channel, Sender}; +use serde::{Deserialize, Serialize}; +use authenticator::StatusUpdate; +use base64::URL_SAFE_NO_PAD; +use rand::Rng; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct U2fV2Challenge { + challenge: String, + version: String, + #[serde(rename = "appId")] + app_id: String, +} + +impl U2fV2Challenge { + pub fn new_random(app_id: S) -> Self where S: Into { + let mut rng = rand::thread_rng(); + let mut rand_bytes = [0_u8; 32]; + for i in 0..32 { + let b: u8 = rng.gen(); + rand_bytes[i] = b; + } + + let challenge = base64::encode_config(&rand_bytes, URL_SAFE_NO_PAD); + Self::new(challenge, app_id) + } + + pub fn new(challenge: S1, app_id: S2) -> Self where S1: Into, S2: Into { + Self { + challenge: challenge.into(), + version: "U2F_V2".into(), + app_id: app_id.into(), + } + } + + pub fn to_json(&self) -> String { + serde_json::to_string(&self).unwrap() + } +} + +pub fn start_status_updater() -> Sender { + let (status_tx, status_rx) = channel::(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::DeviceAvailable { dev_info }) => { + debugging!("STATUS: device available: {}", dev_info) + } + Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { + debugging!("STATUS: device unavailable: {}", dev_info) + } + Ok(StatusUpdate::Success { dev_info }) => { + debugging!("STATUS: success using device: {}", dev_info); + } + Err(_) => { + debugging!("STATUS: end"); + return; + } + } + }); + status_tx +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 4250d61..21cfda4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ -#[macro_use] extern crate rust_util; +#[macro_use] +extern crate rust_util; mod cmd; +mod fido; +mod digest; mod register; mod sign; -use clap::App; +use clap::{App, AppSettings}; use cmd::{Command, CommandError}; use cmd::DefaultCommandImpl; @@ -15,7 +18,9 @@ fn main() -> CommandError { ]; let mut app = App::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) - .about(env!("CARGO_PKG_DESCRIPTION")); + .about(env!("CARGO_PKG_DESCRIPTION")) + .long_about("Webauthn Cli is a command tool register and sign using FIDO security key") + .setting(AppSettings::ColoredHelp); app = DefaultCommandImpl::process_command(app); for command in &commands { app = app.subcommand(command.subcommand()); diff --git a/src/register.rs b/src/register.rs index 79c83d3..374de7b 100644 --- a/src/register.rs +++ b/src/register.rs @@ -2,44 +2,12 @@ use clap::{ArgMatches, SubCommand, App, Arg}; use crate::cmd::{Command, CommandError}; use authenticator::authenticatorservice::AuthenticatorService; use authenticator::statecallback::StateCallback; -use authenticator::{RegisterFlags, StatusUpdate}; -use sha2::{Digest, Sha256}; +use authenticator::RegisterFlags; use std::sync::mpsc::channel; -use std::thread; use rust_util::XResult; -use rand::Rng; -use base64::URL_SAFE_NO_PAD; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct U2fV2Challenge { - challenge: String, - version: String, - #[serde(rename = "appId")] - app_id: String, -} - -impl U2fV2Challenge { - fn new_random(app_id: S) -> Self where S: Into { - let mut rng = rand::thread_rng(); - let mut rand_bytes = [0_u8; 32]; - for i in 0..32 { - let b: u8 = rng.gen(); - rand_bytes[i] = b; - } - - let challenge = base64::encode_config(&rand_bytes, URL_SAFE_NO_PAD); - Self::new(challenge, app_id) - } - - fn new(challenge: S1, app_id: S2) -> Self where S1: Into, S2: Into { - Self { - challenge: challenge.into(), - version: "U2F_V2".into(), - app_id: app_id.into(), - } - } -} +use crate::fido; +use crate::digest; +use crate::fido::U2fV2Challenge; pub struct CommandImpl; @@ -49,48 +17,24 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("Register subcommand") .arg(Arg::with_name("app-id").long("app-id").default_value("https://example.com").help("App id")) + .arg(Arg::with_name("timeout").long("timeout").default_value("10").help("Timeout in seconds")) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { - let mut manager = AuthenticatorService::new()?; - manager.add_u2f_usb_hid_platform_transports(); - let app_id = sub_arg_matches.value_of("app-id").unwrap(); - let timeout_ms = 10000; + let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::() { + Ok(t) => (t * 1000) as u64, + Err(e) => return simple_error!("Timeout should be a number: {}", e), + }; let u2fv2_challenge = U2fV2Challenge::new_random(app_id); - let challenge_str = serde_json::to_string(&u2fv2_challenge).unwrap(); - - let mut challenge = Sha256::default(); - Digest::update(&mut challenge, challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().to_vec(); - - let mut application = Sha256::default(); - // application.update(app_id.as_bytes()); - Digest::update(&mut application, app_id.as_bytes()); - let app_bytes = application.finalize().to_vec(); + let chall_bytes = digest::sha256(&u2fv2_challenge.to_json()); + let app_bytes = digest::sha256(app_id); let flags = RegisterFlags::empty(); - let (status_tx, status_rx) = channel::(); - thread::spawn(move || loop { - match status_rx.recv() { - Ok(StatusUpdate::DeviceAvailable { dev_info }) => { - debugging!("STATUS: device available: {}", dev_info) - } - Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { - debugging!("STATUS: device unavailable: {}", dev_info) - } - Ok(StatusUpdate::Success { dev_info }) => { - debugging!("STATUS: success using device: {}", dev_info); - } - Err(_recv_error) => { - debugging!("STATUS: end"); - return; - } - } - }); + let status_tx = fido::start_status_updater(); let (register_tx, register_rx) = channel(); let callback = StateCallback::new(Box::new(move |rv| { @@ -99,6 +43,9 @@ impl Command for CommandImpl { information!("Start U2F register..."); information!("App id: {}", app_id); + debugging!("Wait timeout: {} ms", timeout_ms); + let mut manager = AuthenticatorService::new()?; + manager.add_u2f_usb_hid_platform_transports(); manager.register( flags, timeout_ms, @@ -112,8 +59,8 @@ impl Command for CommandImpl { let register_result = register_rx.recv()?; let (register_data, device_info) = register_result?; - success!("Register result: {}", base64::encode(®ister_data)); success!("Device info: {}", &device_info); + success!("Register result: {}", base64::encode(®ister_data)); let credential = u2f_get_key_handle_from_register_response(®ister_data).unwrap(); success!("Key handle: {}", base64::encode(&credential)); success!("Key handle: {}", hex::encode(&credential)); @@ -122,6 +69,18 @@ impl Command for CommandImpl { } } +// U2F raw message format specification (version 20170411) section 4.3 +// In case of success we need to send back the following reply +// (excluding ISO7816 success code) +// +------+--------------------+---------------------+------------+------------+------+ +// + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign | +// +------+--------------------+---------------------+------------+------------+------+ +// +// Where Sign is an ECDSA signature over the following structure: +// +------+-------------------+-----------------+------------+--------------------+ +// + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) | +// +------+-------------------+-----------------+------------+--------------------+ +// see https://github.com/google/OpenSK/blob/stable/src/ctap/ctap1.rs fn u2f_get_key_handle_from_register_response(register_response: &[u8]) -> XResult> { if register_response[0] != 0x05 { return simple_error!("Reserved byte not set correctly"); @@ -133,4 +92,16 @@ fn u2f_get_key_handle_from_register_response(register_response: &[u8]) -> XResul let _attestation = key_handle.split_off(key_handle_len); Ok(key_handle) +} + +#[test] +fn test() { + let app_id = "https://webencrypt.org"; + let client_data = base64::decode("eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6ImFHVnNiRzlmZDI5eWJHUSIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViZW5jcnlwdC5vcmciLCJjcm9zc09yaWdpbiI6ZmFsc2V9").unwrap(); + let register_data = base64::decode("BQSpAR+aliPCpz2u8c84Mv13bAYUcJnT9OgiXkCX9CTR/xhWqbJwoL9l6WRqvqwtG77NMvkDexcTKf9Mtf5+V1NCQKYfXWCP+IL2Lfbyng7mX0GV/etsHqlIiiaoEQo5g0Zetn+JimnLx5f259OZlEsvzB7Qs6swN5WRy57FRqREOPcwggE0MIHboAMCAQICCiBzHdQQUIQZ+ZgwCgYIKoZIzj0EAwIwFTETMBEGA1UEAxMKVTJGIElzc3VlcjAaFwswMDAxMDEwMDAwWhcLMDAwMTAxMDAwMFowFTETMBEGA1UEAxMKVTJGIERldmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCvum43LiwAMJacjIFh7SCIrl1e1ltYXeK50v2HWDRZ9pw38HSPmx2SjT+2hTY1YMVagbgXimYrb8+WWlZ6j9bqjFzAVMBMGCysGAQQBguUcAgEBBAQDAgUgMAoGCCqGSM49BAMCA0gAMEUCIQDBo6aOLxanIUYnBX9iu3KMngPnobpi0EZSTkVtLC8/cwIgC1945RGqGBKfbyNtkhMifZK05n7fU+gW37Bdnci5D94wRQIhAJHR1EsN/+Fb+74FyxSjBMaoD6p2edlqGPYEb4SiXwccAiAM48XFf/sWsgzS2YU4EeObztVLErUb1JaA2qHJtTUoig==").unwrap(); + let r = u2f::register::parse_registration(app_id.to_string(), client_data, register_data); + let rr = r.unwrap(); + println!("{}", hex::encode(rr.pub_key)); + println!("{}", hex::encode(rr.attestation_cert.unwrap())); + println!("{}", hex::encode(rr.key_handle)); } \ No newline at end of file diff --git a/src/sign.rs b/src/sign.rs index d143a08..0778042 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -1,11 +1,12 @@ use clap::{ArgMatches, SubCommand, App, Arg}; use crate::cmd::{Command, CommandError}; -use authenticator::{KeyHandle, AuthenticatorTransports, SignFlags, StatusUpdate}; +use authenticator::{KeyHandle, AuthenticatorTransports, SignFlags}; use std::sync::mpsc::channel; use authenticator::statecallback::StateCallback; use authenticator::authenticatorservice::AuthenticatorService; -use sha2::{Sha256, Digest}; -use std::thread; +use crate::fido; +use crate::digest; +use crate::fido::U2fV2Challenge; pub struct CommandImpl; @@ -14,21 +15,30 @@ impl Command for CommandImpl { fn subcommand<'a>(&self) -> App<'a, 'a> { SubCommand::with_name(self.name()).about("Sign subcommand") .arg(Arg::with_name("app-id").long("app-id").default_value("https://example.com").help("App id")) - .arg(Arg::with_name("key-handle").long("key-handle").takes_value(true).help("Key handle")) + .arg(Arg::with_name("timeout").long("timeout").default_value("10").help("Timeout in seconds")) + .arg(Arg::with_name("key-handle").long("key-handle").takes_value(true).multiple(true).help("Key handle")) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { let app_id = sub_arg_matches.value_of("app-id").unwrap(); - let key_handle = opt_value_result!( sub_arg_matches.value_of("key-handle"), "Key handle is required"); + let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::() { + Ok(t) => (t * 1000) as u64, + Err(e) => return simple_error!("Timeout should be a number: {}", e), + }; + let key_handles = opt_value_result!( sub_arg_matches.values_of("key-handle"), "Key handle is required"); - // opt_result!(hex::decode(key_handle),"{}", ""); - let credential = match hex::decode(key_handle) { - Ok(c) => c, - Err(e) => return simple_error!("Key handle decode failed: {}", e), - }; - let key_handle = KeyHandle { - credential, - transports: AuthenticatorTransports::empty(), - }; + let mut request_key_handles = vec![]; + for kh in key_handles { + match hex::decode(kh) { + Ok(k) => request_key_handles.push(KeyHandle { + credential: k, + transports: AuthenticatorTransports::empty(), + }), + Err(e) => warning!("Parse key handle: {}, failed: {}", kh, e), + } + } + if request_key_handles.is_empty() { + return simple_error!("No valid key handle provided"); + } let flags = SignFlags::empty(); let (sign_tx, sign_rx) = channel(); @@ -37,50 +47,23 @@ impl Command for CommandImpl { sign_tx.send(rv).unwrap(); })); - let mut manager = AuthenticatorService::new()?; - manager.add_u2f_usb_hid_platform_transports(); + let u2fv2_challenge = U2fV2Challenge::new_random(app_id); + let chall_bytes = digest::sha256(&u2fv2_challenge.to_json()); - let timeout_ms = 10000; + let app_bytes = digest::sha256(app_id); - // let u2fv2_challenge = U2fV2Challenge::new_random(app_id); - let challenge_str = "aaaa".to_owned();// serde_json::to_string(&u2fv2_challenge).unwrap(); - - let mut challenge = Sha256::default(); - Digest::update(&mut challenge, challenge_str.as_bytes()); - let chall_bytes = challenge.finalize().to_vec(); - - let mut application = Sha256::default(); - // application.update(app_id.as_bytes()); - Digest::update(&mut application, app_id.as_bytes()); - let app_bytes = application.finalize().to_vec(); - - let (status_tx, status_rx) = channel::(); - thread::spawn(move || loop { - match status_rx.recv() { - Ok(StatusUpdate::DeviceAvailable { dev_info }) => { - debugging!("STATUS: device available: {}", dev_info) - } - Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { - debugging!("STATUS: device unavailable: {}", dev_info) - } - Ok(StatusUpdate::Success { dev_info }) => { - debugging!("STATUS: success using device: {}", dev_info); - } - Err(_recv_error) => { - debugging!("STATUS: end"); - return; - } - } - }); + let status_tx = fido::start_status_updater(); information!("Start sign..."); information!("App id: {}", app_id); + let mut manager = AuthenticatorService::new()?; + manager.add_u2f_usb_hid_platform_transports(); if let Err(e) = manager.sign( flags, timeout_ms, chall_bytes, vec![app_bytes], - vec![key_handle], + request_key_handles, status_tx, callback, ) { @@ -92,11 +75,10 @@ impl Command for CommandImpl { .expect("Problem receiving, unable to continue"); let (_, handle_used, sign_data, device_info) = sign_result.expect("Sign failed"); - println!("Sign result: {}", base64::encode(&sign_data)); - println!("Key handle used: {}", base64::encode(&handle_used)); - println!("Key handle used: {}", hex::encode(&handle_used)); - println!("Device info: {}", &device_info); - println!("Done."); + success!("Device info: {}", &device_info); + success!("Sign result: {}", base64::encode(&sign_data)); + success!("Key handle used: {}", base64::encode(&handle_used)); + success!("Key handle used: {}", hex::encode(&handle_used)); Ok(()) }