mirror of
https://github.com/solokeys/solo2-cli.git
synced 2026-06-20 13:16:16 -07:00
Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# This is a configuration file for the direnv tool
|
||||
# More info at <https://direnv.net>
|
||||
|
||||
export SOLO2_LOG=info
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+1400
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "solo2-cli"
|
||||
version = "0.0.0-unreleased"
|
||||
authors = ["SoloKeys Developers"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "solo2"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "solo2"
|
||||
required-features = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.14"
|
||||
anyhow = "1.0.40"
|
||||
clap = "2.33.3"
|
||||
hex = "0.4.3"
|
||||
hex-literal = "0.3.1"
|
||||
iso7816 = { git = "https://github.com/ycrypto/iso7816", branch = "main" }
|
||||
lazy_static = "1.4.0"
|
||||
# lpc55 = "0.1.0-alpha.1"
|
||||
lpc55 = { git = "https://github.com/lpc55/lpc55-host", branch = "main" }
|
||||
pcsc = "2.4.0"
|
||||
pretty_env_logger = { version = "0.4.0", optional = true }
|
||||
|
||||
[features]
|
||||
cli = ["pretty_env_logger"]
|
||||
@@ -0,0 +1,3 @@
|
||||
build:
|
||||
cargo build --release --features cli --bin solo2
|
||||
ls -sh target/release/solo2
|
||||
@@ -0,0 +1,4 @@
|
||||
# solo2 library and CLI
|
||||
|
||||
Example: `solo2 app management` selects the management app.
|
||||
If `SOLO2_LOG=info` is set in the environment, shows the serial number of the key.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
# More info at <https://github.com/Canop/bacon>
|
||||
|
||||
default_job = "check-bin"
|
||||
|
||||
[jobs]
|
||||
|
||||
[jobs.check-bin]
|
||||
command = ["cargo", "check", "--features", "cli", "--bin", "solo2", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-lib]
|
||||
command = ["cargo", "check", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = ["cargo", "clippy", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = ["cargo", "test", "--color", "always"]
|
||||
need_stdout = true
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use hex_literal::hex;
|
||||
|
||||
use crate::{Card, Result};
|
||||
use pcsc::{Context, Protocols, Scope, ShareMode};
|
||||
|
||||
pub mod management;
|
||||
|
||||
pub const SOLOKEYS_RID: &'static [u8] = &hex!("A000000847");
|
||||
|
||||
pub const MANAGEMENT_PIX: &'static [u8] = &hex!("00000001");
|
||||
pub const TESTER_PIX: &'static [u8] = &hex!("01000000");
|
||||
pub const PROVISIONER_PIX: &'static [u8] = &hex!("01000001");
|
||||
|
||||
pub trait App: Sized {
|
||||
const RID: &'static [u8];
|
||||
const PIX: &'static [u8];
|
||||
|
||||
fn aid() -> Vec<u8> {
|
||||
let mut aid: Vec<u8> = Default::default();
|
||||
aid.extend_from_slice(Self::RID);
|
||||
aid.extend_from_slice(Self::PIX);
|
||||
aid
|
||||
}
|
||||
|
||||
fn select(&mut self) -> Result<Vec<u8>> {
|
||||
// use iso7816::command::class::Class;
|
||||
info!("selecting app: {}", hex::encode(Self::aid()).to_uppercase());
|
||||
|
||||
self.card().call(
|
||||
// Class::
|
||||
0,
|
||||
iso7816::Instruction::Select.into(),
|
||||
0x04,
|
||||
0x00,
|
||||
Some(&Self::aid()),
|
||||
)
|
||||
}
|
||||
|
||||
fn card(&mut self) -> &mut Card;
|
||||
|
||||
fn connect() -> Result<Card> {
|
||||
let context = Context::establish(Scope::User)?;
|
||||
let l = context.list_readers_len()?;
|
||||
let mut buffer = Vec::with_capacity(l);
|
||||
buffer.resize(l, 0);
|
||||
|
||||
let readers = context.list_readers(&mut buffer)?.collect::<Vec<_>>();
|
||||
// TODO: select (by UUID)
|
||||
let reader = readers[0];
|
||||
|
||||
info!("connecting with reader: `{}`", &reader.to_string_lossy());
|
||||
let card = Card::from(context.connect(reader, ShareMode::Shared, Protocols::ANY)?);
|
||||
info!("...connected");
|
||||
|
||||
Ok(card)
|
||||
}
|
||||
|
||||
fn new() -> Result<Self>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::{Card, Result};
|
||||
|
||||
pub struct App {
|
||||
card: Card,
|
||||
}
|
||||
|
||||
impl super::App for App {
|
||||
const RID: &'static [u8] = super::SOLOKEYS_RID;
|
||||
const PIX: &'static [u8] = super::PROVISIONER_PIX;
|
||||
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
card: Self::connect()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn card(&mut self) -> &mut Card {
|
||||
&mut self.card
|
||||
}
|
||||
}
|
||||
|
||||
impl App {}
|
||||
@@ -0,0 +1,64 @@
|
||||
use clap::{self, crate_authors, crate_version, App, Arg, SubCommand};
|
||||
|
||||
const ABOUT: &str = "
|
||||
solo2 is the go-to tool to interact with a Solo 2 security key.
|
||||
|
||||
Use -h for short descriptions and --help for more details
|
||||
|
||||
Project homepage: https://github.com/solokeys/solo2-cli
|
||||
";
|
||||
pub fn cli() -> clap::App<'static, 'static> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref LONG_VERSION: String = long_version(None);
|
||||
}
|
||||
|
||||
let cli = App::new("solo2")
|
||||
.author(crate_authors!())
|
||||
.version(crate_version!())
|
||||
.long_version(LONG_VERSION.as_str())
|
||||
.about(ABOUT)
|
||||
.help_message("Prints help information. Use --help for more details.")
|
||||
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
|
||||
// apps
|
||||
.subcommand(
|
||||
SubCommand::with_name("app")
|
||||
.about("app interactions")
|
||||
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(SubCommand::with_name("management").about("management app")),
|
||||
)
|
||||
// inherited from lpc55-host
|
||||
.subcommand(
|
||||
SubCommand::with_name("provision")
|
||||
// .version(crate_version!())
|
||||
// .long_version(LONG_VERSION.as_str())
|
||||
.about("Run a sequence of bootloader commands defined in the config file.")
|
||||
.arg(
|
||||
Arg::with_name("CONFIG")
|
||||
.help("Configuration file containing settings")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("reboot")
|
||||
.version(crate_version!())
|
||||
.long_version(LONG_VERSION.as_str())
|
||||
.about("reboot device"),
|
||||
);
|
||||
|
||||
cli
|
||||
}
|
||||
|
||||
/// Return the "long" format of lpc55's version string.
|
||||
///
|
||||
/// If a revision hash is given, then it is used. If one isn't given, then
|
||||
/// the LPC55_BUILD_GIT_HASH env var is inspected for it. If that isn't set,
|
||||
/// then a revision hash is not included in the version string returned.
|
||||
pub fn long_version(revision_hash: Option<&str>) -> String {
|
||||
// Do we have a git hash?
|
||||
// (Yes, if ripgrep was built on a machine with `git` installed.)
|
||||
let hash = match revision_hash.or(option_env!("LPC55_BUILD_GIT_HASH")) {
|
||||
None => String::new(),
|
||||
Some(githash) => format!(" (rev {})", githash),
|
||||
};
|
||||
format!("{}{}", crate_version!(), hash)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
mod cli;
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use solo2;
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init_custom_env("SOLO2_LOG");
|
||||
info!("solo2 CLI startup");
|
||||
|
||||
let args = cli::cli().get_matches();
|
||||
|
||||
if let Err(err) = try_main(args) {
|
||||
eprintln!("Error: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_main(args: clap::ArgMatches<'_>) -> anyhow::Result<()> {
|
||||
if let Some(args) = args.subcommand_matches("app") {
|
||||
use solo2::apps::App;
|
||||
|
||||
if let Some(args) = args.subcommand_matches("management") {
|
||||
info!("interacting with management app");
|
||||
|
||||
let mut app = solo2::apps::management::App::new()?;
|
||||
let answer_to_select = app.select()?;
|
||||
info!("answer to select: {}", &hex::encode(answer_to_select));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(command) = args.subcommand_matches("provision") {
|
||||
let config_filename = command.value_of("CONFIG").unwrap();
|
||||
let config = lpc55::bootloader::provision::Config::try_from(config_filename)?;
|
||||
|
||||
// let bootloader = bootloader()?;
|
||||
// for cmd in config.provisions {
|
||||
// println!("cmd: {:?}", cmd);
|
||||
// bootloader.run_command(cmd)?;
|
||||
// }
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub type Error = anyhow::Error;
|
||||
pub type Result<T> = anyhow::Result<T>;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
pub struct Card {
|
||||
card: pcsc::Card,
|
||||
}
|
||||
|
||||
impl From<pcsc::Card> for Card {
|
||||
fn from(card: pcsc::Card) -> Self {
|
||||
Self { card }
|
||||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn call(
|
||||
&mut self,
|
||||
// cla: Into<u8>, ins: Into<u8>,
|
||||
// p1: Into<u8>, p2: Into<u8>,
|
||||
cla: u8,
|
||||
ins: u8,
|
||||
p1: u8,
|
||||
p2: u8,
|
||||
data: Option<&[u8]>,
|
||||
// ) -> iso7816::Result<Vec<u8>> {
|
||||
) -> crate::Result<Vec<u8>> {
|
||||
let data = data.unwrap_or(&[]);
|
||||
let mut send_buffer = Vec::<u8>::with_capacity(data.len() + 16);
|
||||
|
||||
send_buffer.push(cla);
|
||||
send_buffer.push(ins);
|
||||
send_buffer.push(p1);
|
||||
send_buffer.push(p2);
|
||||
|
||||
// TODO: checks, chain, ...
|
||||
if data.len() > 0 {
|
||||
send_buffer.push(data.len() as u8);
|
||||
send_buffer.extend_from_slice(data);
|
||||
}
|
||||
|
||||
let mut recv_buffer = Vec::<u8>::with_capacity(3072);
|
||||
recv_buffer.resize(3072, 0);
|
||||
|
||||
let l = self.card.transmit(&send_buffer, &mut recv_buffer)?.len();
|
||||
recv_buffer.resize(l, 0);
|
||||
|
||||
Ok(recv_buffer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod apps;
|
||||
pub mod error;
|
||||
pub use error::{Error, Result};
|
||||
pub mod iccd;
|
||||
pub use iccd::Card;
|
||||
Reference in New Issue
Block a user