Merge pull request #52 from iqlusioninc/cli

cli: Initial `yubikey-cli` utility with `list` command
This commit is contained in:
Tony Arcieri
2019-12-02 11:00:09 -08:00
committed by GitHub
12 changed files with 1478 additions and 9 deletions
+5 -5
View File
@@ -57,7 +57,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: test command: test
args: --release args: --all --release
- name: Run cargo build --all-features - name: Run cargo build --all-features
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -65,7 +65,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: build command: build
args: --all-features args: --all --all-features
test: test:
name: Test Suite name: Test Suite
@@ -94,7 +94,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: test command: test
args: --release args: --all --release
- name: Run cargo build --all-features - name: Run cargo build --all-features
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -102,7 +102,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: build command: build
args: --all-features args: --all --all-features
fmt: fmt:
name: Rustfmt name: Rustfmt
@@ -149,7 +149,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: --all-features -- -D warnings args: --all --all-features -- -D warnings
# TODO: use actions-rs/audit-check # TODO: use actions-rs/audit-check
security_audit: security_audit:
-1
View File
@@ -1,3 +1,2 @@
/target /target
**/*.rs.bk **/*.rs.bk
Cargo.lock
Generated
+1000
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -13,7 +13,10 @@ license = "BSD-2-Clause"
repository = "https://github.com/iqlusioninc/yubikey-piv.rs" repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
readme = "README.md" readme = "README.md"
categories = ["api-bindings", "cryptography", "hardware-support"] categories = ["api-bindings", "cryptography", "hardware-support"]
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"] keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
[workspace]
members = [".", "cli"]
[badges] [badges]
maintenance = { status = "experimental" } maintenance = { status = "experimental" }
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "yubikey-cli"
version = "0.0.0"
description = """
Command-line interface for performing encryption and signing using RSA and/or
ECC keys stored on YubiKey devices.
"""
authors = ["Tony Arcieri <bascule@gmail.com>"]
edition = "2018"
license = "BSD-2-Clause"
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
readme = "README.md"
categories = ["command-line-utilities", "cryptography", "hardware-support"]
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
[dependencies]
gumdrop = "0.7"
env_logger = "0.7"
lazy_static = "1"
termcolor = "1"
yubikey-piv = { version = "0.0.2", path = ".." }
+112
View File
@@ -0,0 +1,112 @@
<img src="https://raw.githubusercontent.com/tendermint/yubihsm-rs/develop/img/logo.png" width="150" height="110">
# yubikey-cli.rs
[![crate][crate-image]][crate-link]
[![Docs][docs-image]][docs-link]
![Apache2/MIT licensed][license-image]
![Rust Version][rustc-image]
![Maintenance Status: Experimental][maintenance-image]
[![Safety Dance][safety-image]][safety-link]
[![Build Status][build-image]][build-link]
[![Gitter Chat][gitter-image]][gitter-link]
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][PIV] CLI
utility with general-purpose public-key encryption and signing support.
[Documentation][docs-link]
## Minimum Supported Rust Version
- Rust **1.39+**
## Supported YubiKeys
- [YubiKey NEO] series (may be dropped in the future, see [#18])
- [YubiKey 4] series
- [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported
## Security Warning
No security audits of this crate have ever been performed. Presently it is in
an experimental stage and may still contain high-severity issues.
USE AT YOUR OWN RISK!
## Status
WIP. Check back later.
## Code of Conduct
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
## License
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you shall be licensed under the
[2-Clause BSD License][BSDL] as shown above, without any additional terms
or conditions.
[//]: # (badges)
[crate-image]: https://img.shields.io/crates/v/yubikey-cli.svg
[crate-link]: https://crates.io/crates/yubikey-cli
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
[docs-link]: https://docs.rs/yubikey-cli/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/
[build-image]: https://github.com/iqlusioninc/yubikey-cli.rs/workflows/Rust/badge.svg?branch=develop&event=push
[build-link]: https://github.com/iqlusioninc/yubikey-cli.rs/actions
[gitter-image]: https://badges.gitter.im/badge.svg
[gitter-link]: https://gitter.im/iqlusioninc/community
[//]: # (general links)
[PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/iqlusioninc/yubikey-cli.rs/blob/develop/CODE_OF_CONDUCT.md
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
+16
View File
@@ -0,0 +1,16 @@
//! `yubikey` command-line utility
#![forbid(unsafe_code)]
#![warn(
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
use gumdrop::Options;
use yubikey_cli::commands::YubikeyCli;
fn main() {
YubikeyCli::parse_args_default_or_exit().run();
}
+98
View File
@@ -0,0 +1,98 @@
//! Commands of the CLI application
pub mod list;
use self::list::ListCmd;
use crate::status;
use gumdrop::Options;
use std::env;
use std::process::exit;
use termcolor::ColorChoice;
/// The `yubikey` CLI utility
#[derive(Debug, Options)]
pub struct YubikeyCli {
/// Obtain help about the current command
#[options(short = "h", help = "print help message")]
pub help: bool,
/// Subcommand to execute.
#[options(command)]
pub command: Option<Commands>,
}
impl YubikeyCli {
/// Run the underlying command type or print usage info and exit
pub fn run(&self) {
// TODO(tarcieri): make this more configurable
status::set_color_choice(ColorChoice::Auto);
// Only show logs if `RUST_LOG` is set
if env::var("RUST_LOG").is_ok() {
env_logger::builder().format_timestamp(None).init();
}
match &self.command {
Some(cmd) => cmd.run(),
None => println!("{}", Commands::usage()),
}
}
}
/// Subcommands of this application
#[derive(Debug, Options)]
pub enum Commands {
/// `help` subcommand
#[options(help = "show help for a command")]
Help(HelpOpts),
/// `version` subcommand
#[options(help = "display version information")]
Version(VersionOpts),
/// `list` subcommand
#[options(help = "list detected readers")]
List(ListCmd),
}
impl Commands {
/// Run the given command
pub fn run(&self) {
match self {
Commands::Help(help) => help.run(),
Commands::Version(version) => version.run(),
Commands::List(list) => list.run(),
}
}
}
/// Help options
#[derive(Debug, Options)]
pub struct HelpOpts {
#[options(free, help = "subcommand to get help for")]
free: Vec<String>,
}
impl HelpOpts {
fn run(&self) {
if let Some(command) = self.free.first() {
if let Some(usage) = Commands::command_usage(command) {
println!("{}", usage);
exit(1);
}
}
println!("{}", Commands::usage());
}
}
/// Version options
#[derive(Debug, Options)]
pub struct VersionOpts {}
impl VersionOpts {
/// Display version information
pub fn run(&self) {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
}
+40
View File
@@ -0,0 +1,40 @@
//! List detected readers
use gumdrop::Options;
use std::process::exit;
use yubikey_piv::readers::Readers;
/// The `list` subcommand
#[derive(Debug, Options)]
pub struct ListCmd {}
impl ListCmd {
/// Run the `list` subcommand
pub fn run(&self) {
let mut readers = Readers::open().unwrap_or_else(|e| {
status_err!("couldn't open PC/SC context: {}", e);
exit(1);
});
let readers_iter = readers.iter().unwrap_or_else(|e| {
status_err!("couldn't enumerate PC/SC readers: {}", e);
exit(1);
});
if readers_iter.len() == 0 {
status_err!("no YubiKeys detected!");
exit(1);
}
for (i, reader) in readers_iter.enumerate() {
let name = reader.name();
let mut yubikey = match reader.open() {
Ok(yk) => yk,
Err(_) => continue,
};
let serial = yubikey.serial();
println!("{}: {} (serial: {})", i + 1, name, serial);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
//! `yubikey` command-line utility
#![forbid(unsafe_code)]
#![warn(
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
#[macro_use]
pub mod status;
pub mod commands;
+165
View File
@@ -0,0 +1,165 @@
//! Status messages
use lazy_static::lazy_static;
use std::io::{self, Write};
use std::sync::Mutex;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
/// Print a success status message (in green if colors are enabled)
#[macro_export]
macro_rules! status_ok {
($status:expr, $msg:expr) => {
$crate::status::Status::new()
.justified()
.bold()
.color(termcolor::Color::Green)
.status($status)
.print_stdout($msg);
};
($status:expr, $fmt:expr, $($arg:tt)+) => {
$crate::status_ok!($status, format!($fmt, $($arg)+));
};
}
/// Print a warning status message (in yellow if colors are enabled)
#[macro_export]
macro_rules! status_warn {
($msg:expr) => {
$crate::status::Status::new()
.bold()
.color(termcolor::Color::Yellow)
.status("warning:")
.print_stdout($msg);
};
($fmt:expr, $($arg:tt)+) => {
$crate::status_warn!(format!($fmt, $($arg)+));
};
}
/// Print an error message (in red if colors are enabled)
#[macro_export]
macro_rules! status_err {
($msg:expr) => {
$crate::status::Status::new()
.bold()
.color(termcolor::Color::Red)
.status("error:")
.print_stderr($msg);
};
($fmt:expr, $($arg:tt)+) => {
$crate::status_err!(format!($fmt, $($arg)+));
};
}
lazy_static! {
/// Color configuration
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
/// Standard output
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice());
/// Standard error
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice());
}
/// Obtain the color configuration.
///
/// Panics if no configuration has been provided.
fn get_color_choice() -> ColorChoice {
let choice = COLOR_CHOICE.lock().unwrap();
*choice
.as_ref()
.expect("terminal stream accessed before initialized!")
}
/// Set the color configuration.
///
/// Panics if the terminal has already been configured.
pub(super) fn set_color_choice(color_choice: ColorChoice) {
let mut choice = COLOR_CHOICE.lock().unwrap();
assert!(choice.is_none(), "terminal colors already configured!");
*choice = Some(color_choice);
}
/// Status message builder
#[derive(Clone, Debug, Default)]
pub struct Status {
/// Should the status be justified?
justified: bool,
/// Should colors be bold?
bold: bool,
/// Color in which status should be displayed
color: Option<Color>,
/// Prefix of the status message (e.g. `Success`)
status: Option<String>,
}
impl Status {
/// Create a new status message with default settings
pub fn new() -> Self {
Self::default()
}
/// Justify status on display
pub fn justified(mut self) -> Self {
self.justified = true;
self
}
/// Make colors bold
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
/// Set the colors used to display this message
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
/// Set a status message to display
pub fn status<S>(mut self, msg: S) -> Self
where
S: ToString,
{
self.status = Some(msg.to_string());
self
}
/// Print the given message to stdout
pub fn print_stdout(self, msg: impl AsRef<str>) {
self.print(&*STDOUT, msg)
.expect("error printing to stdout!")
}
/// Print the given message to stderr
pub fn print_stderr(self, msg: impl AsRef<str>) {
self.print(&*STDERR, msg)
.expect("error printing to stderr!")
}
/// Print the given message
fn print(self, stream: &StandardStream, msg: impl AsRef<str>) -> Result<(), io::Error> {
let mut s = stream.lock();
s.reset()?;
s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?;
if let Some(status) = self.status {
if self.justified {
write!(s, "{:>12}", status)?;
} else {
write!(s, "{}", status)?;
}
}
s.reset()?;
writeln!(s, " {}", msg.as_ref())?;
s.flush()?;
Ok(())
}
}
+3 -2
View File
@@ -164,9 +164,10 @@ pub mod settings;
mod transaction; mod transaction;
pub mod yubikey; pub mod yubikey;
pub use self::{readers::Readers, yubikey::YubiKey};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub use self::{key::Key, mgm::MgmKey, readers::Readers}; pub use self::{key::Key, mgm::MgmKey};
pub use yubikey::YubiKey;
/// Object identifiers /// Object identifiers
pub type ObjectId = u32; pub type ObjectId = u32;