Skip to content

Commit

Permalink
tests: Initial connect test and docs
Browse files Browse the repository at this point in the history
Adds an extremely basic initial test to ensure that we are able to
connect to a YubiKey.

The test is marked `#[ignore]` in the hope that we can eventually start
adding tests which run in CI, e.g. against a mock card.

This also includes a fix for calculating the APDU size, since the ones
we were sending originally were overly long.
  • Loading branch information
tarcieri committed Nov 25, 2019
1 parent 4039560 commit b407fb9
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 1,313 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ pcsc = "2"
sha-1 = "0.8"
subtle = "2"
zeroize = "1"

[dev-dependencies]
env_logger = "0.7"
68 changes: 52 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,68 @@ One small problem, it's not done yet... 😫

But it might be close?

## Minimum Supported Rust Version

- Rust **1.39+**

## 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!

## History

This library is a Rust translation of the [yubico-piv-tool][3] utility by
Yubico, which was originally written in C. It was mechanically translated
from C into Rust using [Corrode][4], and then subsequently heavily
refactored into safer, more idiomatic Rust§.
refactored into safer, more idiomatic Rust.

Note that while this project started as a fork of a [Yubico][5] project,
this fork is **NOT** an official Yubico project and is in no way supported or
endorsed by Yubico.

§ *NOTE*: This section is actually full of lies and notes aspirations/goals,
not history. That said, there's been a decent amount of work cleaning up the
mechanically translated code, and at ~5klocs it's not that much.

## Security Warning

No security audits of this crate have ever been performed, and it has not been
thoroughly assessed to ensure its operation is constant-time on common CPU
architectures.

USE AT YOUR OWN RISK!

## Requirements

- Rust 1.39+
## Testing

To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in
the default state (i.e. default PIN/PUK).

Tests which run live against a YubiKey device are marked as `#[ignore]` by
default in order to pass when running in a CI environment. To run these
tests locally, invoke the following command:

```
cargo test -- --ignored
```

This crate makes extensive use of the `log` facade to provide detailed
information about what is happening. If you'd like to print this logging
information while running the tests, set the `RUST_LOG` environment variable
to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`):

```
RUST_LOG=info cargo test -- --ignored
```

To trace every message sent to/from the card i.e. the raw
Application Protocol Data Unit (APDU) messages, use the `trace` log level:

```
running 1 test
[INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
[INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
[TRACE yubikey_piv::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8]
[TRACE yubikey_piv::transaction] <<< [97, 17, 79, 6, 0, 0, 16, 0, 1, 0, 121, 7, 79, 5, 160, 0, 0, 3, 8, 144, 0]
[TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0]
[TRACE yubikey_piv::transaction] <<< [5, 1, 2, 144, 0]
[TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0]
[TRACE yubikey_piv::transaction] <<< [0, 115, 0, 178, 144, 0]
test connect ... ok
```

APDU messages labeled `>>>` are being sent to the YubiKey's internal SmartCard,
and ones labeled `<<<` are the responses.

## Code of Conduct

Expand Down
93 changes: 54 additions & 39 deletions src/apdu.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,74 @@
//! Application Protocol Data Unit (APDU)
// Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/>
//
// Copyright (c) 2014-2016 Yubico AB
// 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.

use crate::{error::Error, response::Response, transaction::Transaction, Buffer};
use std::fmt::{self, Debug};
use zeroize::{Zeroize, Zeroizing};

/// Size of a serialized APDU (5 byte header + 255 bytes data)
pub const APDU_SIZE: usize = 260;
/// Maximum amount of command data that can be included in an APDU
const APDU_DATA_MAX: usize = 0xFF;

/// Application Protocol Data Unit (APDU).
///
/// These messages are packets used to communicate with the YubiKey using the
/// Chip Card Interface Device (CCID) protocol.
#[derive(Clone)]
pub(crate) struct APDU {
/// Instruction class - indicates the type of command, e.g. interindustry or proprietary
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
cla: u8,

/// Instruction code - indicates the specific command, e.g. "write data"
/// Instruction code: indicates the specific command (e.g. "write data")
ins: u8,

/// Instruction parameter 1 for the command, e.g. offset into file at which to write the data
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
p1: u8,

/// Instruction parameter 2 for the command
p2: u8,

/// Length of command - encodes the number of bytes of command data to follow
lc: u8,

/// Command data
data: [u8; 255],
/// Command data to be sent (`lc` is calculated as `data.len()`)
data: Vec<u8>,
}

impl APDU {
/// Create a new APDU with the given instruction code
pub fn new(ins: u8) -> Self {
let mut apdu = Self::default();
apdu.ins = ins;
apdu
Self {
cla: 0,
ins,
p1: 0,
p2: 0,
data: vec![],
}
}

/// Set this APDU's class
Expand All @@ -63,19 +94,18 @@ impl APDU {
///
/// Panics if the byte slice is more than 255 bytes!
pub fn data(&mut self, bytes: impl AsRef<[u8]>) -> &mut Self {
assert_eq!(self.lc, 0, "APDU command already set!");
assert!(self.data.is_empty(), "APDU command already set!");

let bytes = bytes.as_ref();

assert!(
bytes.len() <= self.data.len(),
"APDU command data too large: {}-bytes",
bytes.len()
bytes.len() <= APDU_DATA_MAX,
"APDU command data too long: {} (max: {})",
bytes.len(),
APDU_DATA_MAX
);

self.lc = bytes.len() as u8;
self.data[..bytes.len()].copy_from_slice(bytes);

self.data.extend_from_slice(bytes);
self
}

Expand All @@ -87,14 +117,13 @@ impl APDU {

/// Consume this APDU and return a self-zeroizing buffer
pub fn to_bytes(&self) -> Buffer {
let mut bytes = Vec::with_capacity(APDU_SIZE);
let mut bytes = Vec::with_capacity(5 + self.data.len());
bytes.push(self.cla);
bytes.push(self.ins);
bytes.push(self.p1);
bytes.push(self.p2);
bytes.push(self.lc);
bytes.push(self.data.len() as u8);
bytes.extend_from_slice(self.data.as_ref());

Zeroizing::new(bytes)
}
}
Expand All @@ -108,25 +137,12 @@ impl Debug for APDU {
self.ins,
self.p1,
self.p2,
self.lc,
&self.data[..]
self.data.len(),
self.data.as_slice()
)
}
}

impl Default for APDU {
fn default() -> Self {
Self {
cla: 0,
ins: 0,
p1: 0,
p2: 0,
lc: 0,
data: [0u8; 255],
}
}
}

impl Drop for APDU {
fn drop(&mut self) {
self.zeroize();
Expand All @@ -139,7 +155,6 @@ impl Zeroize for APDU {
self.ins.zeroize();
self.p1.zeroize();
self.p2.zeroize();
self.lc.zeroize();
self.data.zeroize();
}
}
31 changes: 18 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
//! [YubiKey][1] PIV: [Personal Identity Verification][2] support for
//! [Yubico][3] devices using the Chip Card Interface Device ([CCID][4])
//! protocol.
//! [Yubico][3] devices using the Personal Computer/Smart Card ([PC/SC][4])
//! interface as provided by the [`pcsc` crate][5].
//!
//! **PIV** is a [NIST][5] standard for both *signing* and *encryption*
//! **PIV** is a [NIST][6] standard for both *signing* and *encryption*
//! using SmartCards and SmartCard-based hardware tokens like YubiKeys.
//!
//! This library natively implements the CCID protocol used to manage and
//! utilize PIV encryption and signing keys which can be generated, imported,
//! and stored on YubiKey devices.
//!
//! See [Yubico's guide to PIV-enabled YubiKeys][6] for more information
//! See [Yubico's guide to PIV-enabled YubiKeys][7] for more information
//! on which devices support PIV and the available functionality.
//!
//! Supported algorithms:
Expand All @@ -25,26 +25,31 @@
//! This library is a work-in-progress translation and is not yet usable.
//! Check back later for updates.
//!
//! ## Minimum Supported Rust Version
//!
//! Rust 1.39+
//!
//! ## History
//!
//! This library is a Rust translation of the [yubico-piv-tool][7] utility by
//! This library is a Rust translation of the [yubico-piv-tool][8] utility by
//! Yubico, which was originally written in C. It was mechanically translated
//! from C into Rust using [Corrode][8], and then subsequently heavily
//! from C into Rust using [Corrode][9], and then subsequently heavily
//! refactored into safer, more idiomatic Rust.
//!
//! For more information on `yubico-piv-tool` and background information on how
//! the YubiKey implementation of PIV works in general, see the
//! [Yubico PIV Tool Command Line Guide][9].
//! [Yubico PIV Tool Command Line Guide][10].
//!
//! [1]: https://www.yubico.com/products/yubikey-hardware/
//! [2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf
//! [3]: https://www.yubico.com/
//! [4]: https://en.wikipedia.org/wiki/CCID_(protocol)
//! [5]: https://www.nist.gov/
//! [6]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
//! [7]: https://github.com/Yubico/yubico-piv-tool/
//! [8]: https://github.com/jameysharp/corrode
//! [9]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
//! [4]: https://en.wikipedia.org/wiki/PC/SC
//! [5]: https://github.com/bluetech/pcsc-rust
//! [6]: https://www.nist.gov/
//! [7]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
//! [8]: https://github.com/Yubico/yubico-piv-tool/
//! [9]: https://github.com/jameysharp/corrode
//! [10]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
// Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/>
Expand Down
26 changes: 19 additions & 7 deletions src/yubikey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl YubiKey {
Ok(yubikey)
}

/// Connect to a 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()?;
Expand All @@ -178,7 +178,19 @@ impl YubiKey {

info!("trying to connect to reader '{}'", reader.to_string_lossy());

return Ok(context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?);
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");
Expand Down Expand Up @@ -308,7 +320,7 @@ impl YubiKey {
/// Get YubiKey device serial number.
///
/// This always uses the cached version queried when the key is initialized.
pub fn get_serial(&mut self) -> Serial {
pub fn serial(&mut self) -> Serial {
self.serial
}

Expand Down Expand Up @@ -376,7 +388,7 @@ impl YubiKey {
/// Change the Personal Identification Number (PIN).
///
/// The default PIN code is 123456
pub unsafe fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
{
let txn = self.begin_transaction()?;
txn.change_pin(CHREF_ACT_CHANGE_PIN, current_pin, new_pin)?;
Expand All @@ -390,7 +402,7 @@ impl YubiKey {
}

/// Set PIN last changed
pub unsafe fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
let mut data = [0u8; CB_BUF_MAX];
let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
Expand Down Expand Up @@ -433,7 +445,7 @@ impl YubiKey {
/// The PUK is part of the PIV standard that the YubiKey follows.
///
/// The default PUK code is 12345678.
pub unsafe fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
let txn = self.begin_transaction()?;
txn.change_pin(CHREF_ACT_CHANGE_PUK, current_puk, new_puk)
}
Expand Down Expand Up @@ -507,7 +519,7 @@ impl YubiKey {

/// Unblock a Personal Identification Number (PIN) using a previously
/// configured PIN Unblocking Key (PUK).
pub unsafe fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
let txn = self.begin_transaction()?;
txn.change_pin(CHREF_ACT_UNBLOCK_PIN, puk, new_pin)
}
Expand Down
Loading

0 comments on commit b407fb9

Please sign in to comment.