Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
@@ -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,3 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.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/),
|
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).
|
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)
|
## [0.0.2] (2019-11-25)
|
||||||
### Added
|
### Added
|
||||||
- `untested` Cargo feature to mark untested functionality ([#30])
|
- `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]
|
[package]
|
||||||
name = "yubikey-piv"
|
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 = """
|
description = """
|
||||||
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
||||||
application providing general-purpose public-key signing and encryption
|
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"
|
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" }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[![crate][crate-image]][crate-link]
|
[![crate][crate-image]][crate-link]
|
||||||
[![Docs][docs-image]][docs-link]
|
[![Docs][docs-image]][docs-link]
|
||||||
![Apache2/MIT licensed][license-image]
|
[![2-Clause BSD Licensed][license-image]][license-link]
|
||||||
![Rust Version][rustc-image]
|
![Rust Version][rustc-image]
|
||||||
![Maintenance Status: Experimental][maintenance-image]
|
![Maintenance Status: Experimental][maintenance-image]
|
||||||
[![Safety Dance][safety-image]][safety-link]
|
[![Safety Dance][safety-image]][safety-link]
|
||||||
@@ -192,6 +192,7 @@ or conditions.
|
|||||||
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
||||||
[docs-link]: https://docs.rs/yubikey-piv/
|
[docs-link]: https://docs.rs/yubikey-piv/
|
||||||
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
[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
|
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
|
||||||
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-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-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(
|
#![doc(
|
||||||
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
|
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)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(
|
#![warn(
|
||||||
@@ -156,6 +156,7 @@ mod metadata;
|
|||||||
pub mod mgm;
|
pub mod mgm;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub mod msroots;
|
pub mod msroots;
|
||||||
|
pub mod readers;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
mod serialization;
|
mod serialization;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
@@ -163,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};
|
pub use self::{key::Key, mgm::MgmKey};
|
||||||
pub use yubikey::YubiKey;
|
|
||||||
|
|
||||||
/// Object identifiers
|
/// Object identifiers
|
||||||
pub type ObjectId = u32;
|
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::*,
|
serialization::*,
|
||||||
Buffer, ObjectId,
|
Buffer, ObjectId,
|
||||||
};
|
};
|
||||||
use crate::{consts::*, error::Error, transaction::Transaction};
|
use crate::{
|
||||||
|
consts::*,
|
||||||
|
error::Error,
|
||||||
|
readers::{Reader, Readers},
|
||||||
|
transaction::Transaction,
|
||||||
|
};
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use pcsc::{Card, Context};
|
use pcsc::Card;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
use std::fmt::{self, Display};
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
|
fmt::{self, Display},
|
||||||
|
};
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use std::{
|
use std::{
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
@@ -130,96 +138,28 @@ pub struct YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl YubiKey {
|
impl YubiKey {
|
||||||
/// Open a connection to a YubiKey, optionally giving the name
|
/// Open a connection to a YubiKey.
|
||||||
/// (needed if e.g. there are multiple YubiKeys connected).
|
///
|
||||||
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> {
|
/// Returns an error if there is more than one YubiKey detected.
|
||||||
let context = Context::establish(pcsc::Scope::System)?;
|
///
|
||||||
let mut card = Self::connect(&context, name)?;
|
/// 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;
|
if let Some(reader) = reader_iter.next() {
|
||||||
let version: Version;
|
if reader_iter.next().is_some() {
|
||||||
let serial: Serial;
|
error!("multiple YubiKeys detected!");
|
||||||
|
return Err(Error::PcscError { inner: None });
|
||||||
{
|
|
||||||
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()?;
|
return reader.open();
|
||||||
|
|
||||||
// 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 {
|
error!("no YubiKey detected!");
|
||||||
card,
|
Err(Error::GenericError)
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconnect to a YubiKey
|
/// 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();
|
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.version());
|
||||||
dbg!(&yubikey.serial());
|
dbg!(&yubikey.serial());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user