Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
@@ -57,7 +57,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: test
|
||||
args: --release
|
||||
args: --all --release
|
||||
|
||||
- name: Run cargo build --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: build
|
||||
args: --all-features
|
||||
args: --all --all-features
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: test
|
||||
args: --release
|
||||
args: --all --release
|
||||
|
||||
- name: Run cargo build --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: build
|
||||
args: --all-features
|
||||
args: --all --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-features -- -D warnings
|
||||
args: --all --all-features -- -D warnings
|
||||
|
||||
# TODO: use actions-rs/audit-check
|
||||
security_audit:
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
||||
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.3] (2019-12-02)
|
||||
### Added
|
||||
- Initial `Readers` enumerator for detecting YubiKeys ([#51])
|
||||
- Certificate parsing ([#45])
|
||||
|
||||
### Changed
|
||||
- Use `Reader` to connect to `YubiKey` ([#51])
|
||||
- Convert `SlotId` and `AlgorithmId` into enums ([#44])
|
||||
- Use `secrecy` crate for storing `CachedPin` ([#43])
|
||||
- Change `CHUID` struct to hold complete CHUID value ([#42])
|
||||
- Eliminate all usages of `unsafe` ([#37], [#39])
|
||||
- Make anonymous CHUID struct public ([#36])
|
||||
- Have `sign_data` and `decrypt_data` return a `Buffer` ([#34])
|
||||
- `Ins` (APDU instruction codes) enum ([#33])
|
||||
- Factor `Response` into `apdu` module; improved debugging ([#32])
|
||||
|
||||
[0.0.3]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/53
|
||||
[#51]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/51
|
||||
[#45]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/45
|
||||
[#44]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/44
|
||||
[#43]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/43
|
||||
[#42]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/42
|
||||
[#39]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/39
|
||||
[#37]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/37
|
||||
[#36]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/36
|
||||
[#34]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/34
|
||||
[#33]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/33
|
||||
[#32]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/32
|
||||
|
||||
## [0.0.2] (2019-11-25)
|
||||
### Added
|
||||
- `untested` Cargo feature to mark untested functionality ([#30])
|
||||
|
||||
Generated
+1000
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "yubikey-piv"
|
||||
version = "0.0.2" # Also update html_root_url in lib.rs when bumping this
|
||||
version = "0.0.3" # Also update html_root_url in lib.rs when bumping this
|
||||
description = """
|
||||
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
||||
application providing general-purpose public-key signing and encryption
|
||||
@@ -13,7 +13,10 @@ license = "BSD-2-Clause"
|
||||
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
|
||||
readme = "README.md"
|
||||
categories = ["api-bindings", "cryptography", "hardware-support"]
|
||||
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
|
||||
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "cli"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[![crate][crate-image]][crate-link]
|
||||
[![Docs][docs-image]][docs-link]
|
||||
![Apache2/MIT licensed][license-image]
|
||||
[![2-Clause BSD Licensed][license-image]][license-link]
|
||||
![Rust Version][rustc-image]
|
||||
![Maintenance Status: Experimental][maintenance-image]
|
||||
[![Safety Dance][safety-image]][safety-link]
|
||||
@@ -192,6 +192,7 @@ or conditions.
|
||||
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
||||
[docs-link]: https://docs.rs/yubikey-piv/
|
||||
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
||||
[license-link]: https://github.com/iqlusioninc/yubikey-piv.rs/blob/develop/COPYING
|
||||
[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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 0.0.1 (2019-12-02)
|
||||
- Initial release
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "yubikey-cli"
|
||||
version = "0.0.1"
|
||||
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.3", path = ".." }
|
||||
+112
@@ -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-piv.rs/workflows/Rust/badge.svg?branch=develop&event=push
|
||||
[build-link]: https://github.com/iqlusioninc/yubikey-piv.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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -123,7 +123,7 @@
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
|
||||
html_root_url = "https://docs.rs/yubikey-piv/0.0.2"
|
||||
html_root_url = "https://docs.rs/yubikey-piv/0.0.3"
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
@@ -156,6 +156,7 @@ mod metadata;
|
||||
pub mod mgm;
|
||||
#[cfg(feature = "untested")]
|
||||
pub mod msroots;
|
||||
pub mod readers;
|
||||
#[cfg(feature = "untested")]
|
||||
mod serialization;
|
||||
#[cfg(feature = "untested")]
|
||||
@@ -163,9 +164,10 @@ pub mod settings;
|
||||
mod transaction;
|
||||
pub mod yubikey;
|
||||
|
||||
pub use self::{readers::Readers, yubikey::YubiKey};
|
||||
|
||||
#[cfg(feature = "untested")]
|
||||
pub use self::{key::Key, mgm::MgmKey};
|
||||
pub use yubikey::YubiKey;
|
||||
|
||||
/// Object identifiers
|
||||
pub type ObjectId = u32;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Support for enumerating available readers
|
||||
|
||||
use crate::{error::Error, yubikey::YubiKey};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::TryInto,
|
||||
ffi::CStr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// Iterator over connected readers
|
||||
pub type Iter<'ctx> = std::vec::IntoIter<Reader<'ctx>>;
|
||||
|
||||
/// Enumeration support for available readers
|
||||
pub struct Readers {
|
||||
/// PC/SC context
|
||||
ctx: Arc<Mutex<pcsc::Context>>,
|
||||
|
||||
/// Buffer for storing reader names
|
||||
reader_names: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Readers {
|
||||
/// Open a PC/SC context, which can be used to enumerate available PC/SC
|
||||
/// readers (which can be used to connect to YubiKeys).
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let ctx = pcsc::Context::establish(pcsc::Scope::System)?;
|
||||
let reader_names = vec![0u8; ctx.list_readers_len()?];
|
||||
Ok(Self {
|
||||
ctx: Arc::new(Mutex::new(ctx)),
|
||||
reader_names,
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over the available readers
|
||||
pub fn iter(&mut self) -> Result<Iter<'_>, Error> {
|
||||
let Self { ctx, reader_names } = self;
|
||||
|
||||
let reader_cstrs: Vec<_> = {
|
||||
let c = ctx.lock().unwrap();
|
||||
|
||||
// ensure PC/SC context is valid
|
||||
c.is_valid()?;
|
||||
|
||||
c.list_readers(reader_names)?.collect()
|
||||
};
|
||||
|
||||
let readers: Vec<_> = reader_cstrs
|
||||
.iter()
|
||||
.map(|name| Reader::new(name, Arc::clone(ctx)))
|
||||
.collect();
|
||||
|
||||
Ok(readers.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// An individual connected reader
|
||||
pub struct Reader<'ctx> {
|
||||
/// Name of this reader
|
||||
name: &'ctx CStr,
|
||||
|
||||
/// PC/SC context
|
||||
ctx: Arc<Mutex<pcsc::Context>>,
|
||||
}
|
||||
|
||||
impl<'ctx> Reader<'ctx> {
|
||||
/// Create a new reader from its name and context
|
||||
fn new(name: &'ctx CStr, ctx: Arc<Mutex<pcsc::Context>>) -> Self {
|
||||
// TODO(tarcieri): open devices, determine they're YubiKeys, get serial?
|
||||
Self { name, ctx }
|
||||
}
|
||||
|
||||
/// Get this reader's name
|
||||
pub fn name(&self) -> Cow<'_, str> {
|
||||
// TODO(tarcieri): is lossy ok here? try to avoid lossiness?
|
||||
self.name.to_string_lossy()
|
||||
}
|
||||
|
||||
/// Open a connection to this reader, returning a `YubiKey` if successful
|
||||
pub fn open(&self) -> Result<YubiKey, Error> {
|
||||
self.try_into()
|
||||
}
|
||||
|
||||
/// Connect to this reader, returning its `pcsc::Card`
|
||||
pub(crate) fn connect(&self) -> Result<pcsc::Card, Error> {
|
||||
let ctx = self.ctx.lock().unwrap();
|
||||
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
|
||||
}
|
||||
}
|
||||
+84
-88
@@ -42,14 +42,22 @@ use crate::{
|
||||
serialization::*,
|
||||
Buffer, ObjectId,
|
||||
};
|
||||
use crate::{consts::*, error::Error, transaction::Transaction};
|
||||
use crate::{
|
||||
consts::*,
|
||||
error::Error,
|
||||
readers::{Reader, Readers},
|
||||
transaction::Transaction,
|
||||
};
|
||||
#[cfg(feature = "untested")]
|
||||
use getrandom::getrandom;
|
||||
use log::{error, info, warn};
|
||||
use pcsc::{Card, Context};
|
||||
use pcsc::Card;
|
||||
#[cfg(feature = "untested")]
|
||||
use secrecy::ExposeSecret;
|
||||
use std::fmt::{self, Display};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
#[cfg(feature = "untested")]
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
@@ -130,96 +138,28 @@ pub struct YubiKey {
|
||||
}
|
||||
|
||||
impl YubiKey {
|
||||
/// Open a connection to a YubiKey, optionally giving the name
|
||||
/// (needed if e.g. there are multiple YubiKeys connected).
|
||||
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> {
|
||||
let context = Context::establish(pcsc::Scope::System)?;
|
||||
let mut card = Self::connect(&context, name)?;
|
||||
/// Open a connection to a YubiKey.
|
||||
///
|
||||
/// Returns an error if there is more than one YubiKey detected.
|
||||
///
|
||||
/// If you need to operate in environments with more than one YubiKey
|
||||
/// attached to the same system, use [`yubikey_piv::Readers`] to select
|
||||
/// from the available PC/SC readers connected.
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let mut readers = Readers::open()?;
|
||||
let mut reader_iter = readers.iter()?;
|
||||
|
||||
let mut is_neo = false;
|
||||
let version: Version;
|
||||
let serial: Serial;
|
||||
|
||||
{
|
||||
let txn = Transaction::new(&mut card)?;
|
||||
let mut atr_buf = [0; CB_ATR_MAX];
|
||||
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
|
||||
if atr == YKPIV_ATR_NEO_R3 {
|
||||
is_neo = true;
|
||||
if let Some(reader) = reader_iter.next() {
|
||||
if reader_iter.next().is_some() {
|
||||
error!("multiple YubiKeys detected!");
|
||||
return Err(Error::PcscError { inner: None });
|
||||
}
|
||||
|
||||
txn.select_application()?;
|
||||
|
||||
// now that the PIV application is selected, retrieve the version
|
||||
// and serial number. Previously the NEO/YK4 required switching
|
||||
// to the yk applet to retrieve the serial, YK5 implements this
|
||||
// as a PIV applet command. Unfortunately, this change requires
|
||||
// that we retrieve the version number first, so that get_serial
|
||||
// can determine how to get the serial number, which for the NEO/Yk4
|
||||
// will result in another selection of the PIV applet.
|
||||
|
||||
version = txn.get_version().map_err(|e| {
|
||||
warn!("failed to retrieve version: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
serial = txn.get_serial(version).map_err(|e| {
|
||||
warn!("failed to retrieve serial number: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
return reader.open();
|
||||
}
|
||||
|
||||
let yubikey = YubiKey {
|
||||
card,
|
||||
pin: None,
|
||||
is_neo,
|
||||
version,
|
||||
serial,
|
||||
};
|
||||
|
||||
Ok(yubikey)
|
||||
}
|
||||
|
||||
/// Connect to a YubiKey PC/SC card.
|
||||
fn connect(context: &Context, name: Option<&[u8]>) -> Result<Card, Error> {
|
||||
// ensure PC/SC context is valid
|
||||
context.is_valid()?;
|
||||
|
||||
let buffer_len = context.list_readers_len()?;
|
||||
let mut buffer = vec![0u8; buffer_len];
|
||||
|
||||
for reader in context.list_readers(&mut buffer)? {
|
||||
if let Some(wanted) = name {
|
||||
if reader.to_bytes() != wanted {
|
||||
warn!(
|
||||
"skipping reader '{}' since it doesn't match '{}'",
|
||||
reader.to_string_lossy(),
|
||||
String::from_utf8_lossy(wanted)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
info!("trying to connect to reader '{}'", reader.to_string_lossy());
|
||||
|
||||
match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1) {
|
||||
Ok(card) => {
|
||||
info!("connected to '{}' successfully", reader.to_string_lossy());
|
||||
return Ok(card);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"skipping '{}' due to connection error: {}",
|
||||
reader.to_string_lossy(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("error: no usable reader found");
|
||||
Err(Error::PcscError { inner: None })
|
||||
error!("no YubiKey detected!");
|
||||
Err(Error::GenericError)
|
||||
}
|
||||
|
||||
/// Reconnect to a YubiKey
|
||||
@@ -818,3 +758,59 @@ impl YubiKey {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(reader: &'a Reader<'_>) -> Result<Self, Error> {
|
||||
let mut card = reader.connect().map_err(|e| {
|
||||
error!("error connecting to reader '{}': {}", reader.name(), e);
|
||||
e
|
||||
})?;
|
||||
|
||||
info!("connected to reader: {}", reader.name());
|
||||
|
||||
let mut is_neo = false;
|
||||
let version: Version;
|
||||
let serial: Serial;
|
||||
|
||||
{
|
||||
let txn = Transaction::new(&mut card)?;
|
||||
let mut atr_buf = [0; CB_ATR_MAX];
|
||||
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
|
||||
if atr == YKPIV_ATR_NEO_R3 {
|
||||
is_neo = true;
|
||||
}
|
||||
|
||||
txn.select_application()?;
|
||||
|
||||
// now that the PIV application is selected, retrieve the version
|
||||
// and serial number. Previously the NEO/YK4 required switching
|
||||
// to the yk applet to retrieve the serial, YK5 implements this
|
||||
// as a PIV applet command. Unfortunately, this change requires
|
||||
// that we retrieve the version number first, so that get_serial
|
||||
// can determine how to get the serial number, which for the NEO/Yk4
|
||||
// will result in another selection of the PIV applet.
|
||||
|
||||
version = txn.get_version().map_err(|e| {
|
||||
warn!("failed to retrieve version: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
serial = txn.get_serial(version).map_err(|e| {
|
||||
warn!("failed to retrieve serial number: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
}
|
||||
|
||||
let yubikey = YubiKey {
|
||||
card,
|
||||
pin: None,
|
||||
is_neo,
|
||||
version,
|
||||
serial,
|
||||
};
|
||||
|
||||
Ok(yubikey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fn connect() {
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
}
|
||||
|
||||
let mut yubikey = YubiKey::open(None).unwrap();
|
||||
let mut yubikey = YubiKey::open().unwrap();
|
||||
dbg!(&yubikey.version());
|
||||
dbg!(&yubikey.serial());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user