Compare commits

...

58 Commits

Author SHA1 Message Date
Tony Arcieri 77d9dd6e97 v0.0.2 2019-11-25 15:27:04 -08:00
Tony Arcieri aeb4e6c3fc Merge pull request #30 from tarcieri/untested-feature
Add `untested` Cargo feature for untested functionality
2019-11-25 15:16:56 -08:00
Tony Arcieri a23af7dc31 Add untested Cargo feature for untested functionality
This adds an `untested` feature to any functions which have not yet been
tested live against a YubiKey device (which is presently pretty much
everything).

This sets a clear expectation of what is presently supported, and
additionally documents the status in the README (and a series of GitHub
issues).

Adds a `cargo build --all-features` to GitHub Actions' `test` step in
order to make sure that `untested` functionality still compiles.
2019-11-25 15:04:32 -08:00
Tony Arcieri 9083194c3b Merge pull request #29 from tarcieri/document-project-status
Document project status in README.md and lib.rs
2019-11-25 13:57:41 -08:00
Tony Arcieri cf8f3c88cf Document project status in README.md and lib.rs
This commit adds quite a bit of documentation about the current status
of the project, including links to GitHub issues for the different Rust
modules which map to specific pieces of functionality.

The intent is to track the current status of the project in the
README.md as that's more up-to-date than the docs.rs documentation
(which depends on a crate release to get updated).
2019-11-25 13:42:22 -08:00
Tony Arcieri eb399cbecc Merge pull request #19 from tarcieri/tests
tests: Initial connect test and docs
2019-11-25 10:32:30 -08:00
Tony Arcieri fd77e9f844 tests: Initial connect test and docs
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.
2019-11-25 10:00:56 -08:00
Tony Arcieri 4039560d97 Merge pull request #17 from tarcieri/pcsc
Rewrite translated code to use the `pcsc` crate
2019-11-25 09:17:51 -08:00
Tony Arcieri 63d7a21c9d transaction: Fix fetch_object result slicing
Needs to match the original C code:

    memmove(data, data + 1 + offs, outlen);
2019-11-25 09:00:53 -08:00
Tony Arcieri 79b1142f21 Remove usages of YKPIV_OBJ_MAX_SIZE
...replacing them with `CB_BUF_MAX`.

Both constants are 3072, however `CB_BUF_MAX` is what the original code
was using.

See discussion here:

https://github.com/tarcieri/yubikey-piv.rs/pull/17#discussion_r350166104
2019-11-25 08:49:29 -08:00
Tony Arcieri 67ed32cbf9 msroots: Use clippy's suggested logic simplification
Also the same one @str4d made originally, guess I should've listened!

https://github.com/tarcieri/yubikey-piv.rs/pull/17#discussion_r349964456
2019-11-25 08:36:30 -08:00
Tony Arcieri c54f66acb4 transaction: Always require padded PIN for verify_pin
Callers of this function always pad up to `CB_PIN_MAX` with `0xFF`.

The logic being changed here was previously identical to the `_verify`
function in `ykpiv.c`:

https://github.com/Yubico/yubico-piv-tool/blob/8ba243f/lib/ykpiv.c#L1299

...but @str4d noticed this potentially allows a caller to send an
unpadded PIN, which may (or may not) be an issue.
2019-11-25 08:27:54 -08:00
Tony Arcieri 6e4819bad1 msroots: Match original C logic for MSROOTS tag matching 2019-11-25 08:26:05 -08:00
Tony Arcieri a9d7996aa6 metadata: Re-add check that we're not at end-of-buffer
It seems like given we're inside a while loop which also has this
conditional, the original code should've been fine, but this change
makes it closer to the original C code.
2019-11-25 08:22:12 -08:00
Tony Arcieri 9367218c7d Apply suggestions from code review
More of @str4d's suggested changes

Co-Authored-By: str4d <thestr4d@gmail.com>
2019-11-25 07:38:33 -08:00
Tony Arcieri e18828d048 Apply suggestions from code review
@str4d's suggested fixes

Co-Authored-By: str4d <thestr4d@gmail.com>
2019-11-25 07:19:20 -08:00
Tony Arcieri ebbf043bc9 Rewrite translated code to use the pcsc crate
This commit contains a "big bang" refactor/rewrite which does the
following:

- Replaces all `SCard*` FFI calls with the `pcsc` crate, which provides
  a safe, portable PC/SC API across Windows, macOS, and Linux
- Refactors the `util` module into modules representing the various
  device functions and concepts, e.g. `certificate`, `key`, `mgm`
- Replaces all usage of `libc` with `std` functionality, and in many
  places rewriting functionality to use safe code.
- Removes `ykpiv_` from all function names, and `Piv*` from type names.

In 20/20 hindsight I wish I had done this commit more incrementally so
as to make it easier to review. Que sera sera.

However, realistically we need to test all functionality on the device
to ensure that it actually works. Going forward I would like to put
pretty much all of the current code behind an `untested` cargo feature,
and then remove it for each bit of functionality we test.
2019-11-24 16:36:43 -08:00
Tony Arcieri 96cd5d080b Merge pull request #15 from tarcieri/apdu-cleanups
Clean up APDU construction with builder API
2019-11-21 09:18:30 -08:00
Tony Arcieri bd485eb912 Clean up APDU construction with builder API
Changes the `APDU` struct into a builder for serialized APDU messages.

This makes APDU construction safer and more idiomatic, and also caught a
few bugs in the process (missing templ from the C translation).
2019-11-21 09:05:32 -08:00
Tony Arcieri 64bf135f6c Merge pull request #14 from tarcieri/yubikey-struct-methods
Factor `yubikey` module fns into struct methods
2019-11-21 08:37:31 -08:00
Tony Arcieri b5bee1aa2f Factor yubikey module fns into struct methods
Moves all of the functions in the `yubikey` module into an
`impl YubiKey` block, and changes the receiver to `&mut self` making
them methods.
2019-11-21 08:20:08 -08:00
Tony Arcieri 7b1d98f695 Merge pull request #13 from tarcieri/rename-errorkind
Rename ErrorKind to Error
2019-11-21 07:50:13 -08:00
Tony Arcieri f372cfc2a7 Rename ErrorKind to Error
There was originally another `Error` type from the translation. Now that
it's gone, and we don't presently have a type just named `Error`, this
renames the current `ErrorKind` type now that the original was deleted.
2019-11-21 07:41:29 -08:00
Tony Arcieri 9b6fb7a39c Merge pull request #12 from str4d/internal-cleanups
Internal cleanups
2019-11-21 06:50:14 -08:00
Jack Grigg d01d2dec84 Minor internal cleanups 2019-11-21 13:15:57 +00:00
Jack Grigg 7412c02892 Remove dead code from internals 2019-11-21 13:12:46 +00:00
Jack Grigg 6e24660a80 Clean up internal::setting_get_bool 2019-11-21 13:10:23 +00:00
Tony Arcieri ad21eaea81 Merge pull request #10 from str4d/3des
Use des crate for 3DES operations
2019-11-20 17:07:02 -08:00
Jack Grigg a71389a820 Remove completed TODO 2019-11-21 00:48:48 +00:00
Jack Grigg 35cc1bbf72 Address clippy lints 2019-11-21 00:44:49 +00:00
Jack Grigg 86fde50c2d Use des crate for 3DES operations 2019-11-21 00:37:16 +00:00
Tony Arcieri 634740d751 Merge pull request #9 from str4d/pbkdf2
Replace PKCS5_PBKDF2_HMAC_SHA1 with crates
2019-11-20 13:44:26 -08:00
Jack Grigg c5a486cb4b Replace PKCS5_PBKDF2_HMAC_SHA1 with crates
Also tidies up ykpiv_util_get_derived_mgm (which was the only consumer
of the function) and fixes some porting bugs.
2019-11-20 21:20:01 +00:00
Tony Arcieri 87c00a9b61 Merge pull request #8 from str4d/getrandom
Replace RAND_bytes with getrandom crate
2019-11-20 13:17:06 -08:00
Jack Grigg c0bbf9aa06 Replace RAND_bytes with getrandom crate 2019-11-20 21:02:28 +00:00
Tony Arcieri ffdb114ae5 Merge pull request #7 from tarcieri/log
Use `log` crate for logging
2019-11-20 11:42:21 -08:00
Tony Arcieri c3d5df1643 Use log crate for logging
Switches all of the previous `state->verbose`-gated `eprintln!` calls to
use macros from the `log` crate, trying to map them onto the previous
verbosity levels, more or less following this mapping:

0. off
1. error/info/warn (depending on context)
2. trace

This additionally includes a bunch of logic/branch reformatting (and
occasional missed constants), since getting rid of all the gating on
verbose provided ample opportunities to clean up the code. Hopefully I
didn't break too much in the process!
2019-11-20 11:34:07 -08:00
Tony Arcieri f25eed1a86 Merge pull request #6 from str4d/1-replace-ok-with-result
Replace ErrorKind::Ok with Result
2019-11-20 07:11:05 -08:00
Jack Grigg 683e463824 Silence _ykpiv_end_transaction "unused Result" clippy warnings
These calls will be replaced when the pcsc crate is introduced.
2019-11-20 12:38:48 +00:00
Jack Grigg ce55e08af8 Explicitly ignore _cache_pin errors
The only error that _cache_pin can return is a memory allocation failure
which will likely be removed during the refactor.
2019-11-20 12:35:38 +00:00
Jack Grigg 88ec6bcb32 Remove redundant Result from ykpiv_disconnect 2019-11-20 12:32:19 +00:00
Jack Grigg b23ed1d48a Pass response to ykpiv_auth_verifyresponse by value 2019-11-20 12:32:04 +00:00
Jack Grigg 6324f7a75d Document tries field of ErrorKind::WrongPin 2019-11-20 12:26:33 +00:00
Jack Grigg 9252765940 Fix bug in ykpiv_util_block_puk
Introduced in b750b9cbbb.
2019-11-20 12:25:35 +00:00
Jack Grigg a43bddb531 Pointers -> refs in ykpiv_auth_verifyresponse 2019-11-20 12:21:42 +00:00
Jack Grigg 71a334a9b8 fn ykpiv_auth_getchallenge() -> Result<[u8; 8], ErrorKind> 2019-11-20 12:16:58 +00:00
Jack Grigg b750b9cbbb Convert tries pointers into Result elements 2019-11-20 12:07:06 +00:00
Jack Grigg 31ef465571 fn _ykpiv_get_version() -> Result<Version, ErrorKind> 2019-11-20 11:44:13 +00:00
Jack Grigg 90bdda85cb fn _ykpiv_get_serial() -> Result<u32, ErrorKind> 2019-11-20 11:39:58 +00:00
Jack Grigg c394511c60 Convert APDU pointer into mutable reference 2019-11-20 11:31:23 +00:00
Jack Grigg 4e710da32c Remove ErrorKind::Ok 2019-11-20 11:17:17 +00:00
Jack Grigg 7add9bfa41 Convert remaining APIs to Result<(), ErrorKind> 2019-11-20 11:16:44 +00:00
Jack Grigg 6c03ea89ec Return Result<(), ErrorKind> from most internal APIs
Started with _ykpiv_begin_transaction and kept going incrementally until
it compiled again.
2019-11-20 11:09:59 +00:00
Jack Grigg 5733d0b0af Convert Yubikey pointers into mutable references 2019-11-20 01:07:15 +00:00
Jack Grigg 943dd6f146 Return Result<(), ErrorKind> from most APIs
This commit modifies all public APIs where doing so wouldn't require
modifying internal functions.
2019-11-20 01:06:54 +00:00
Tony Arcieri 65ec5aad63 Merge pull request #3 from tarcieri/gitter-badge
README.md: Update Gitter badge URLs
2019-11-19 08:36:45 -08:00
Tony Arcieri 012d164e12 README.md: Update Gitter badge URLs 2019-11-18 20:38:26 -08:00
Tony Arcieri 9bcd85bce0 Merge pull request #2 from tarcieri/v0.0.1
v0.0.1
2019-11-18 19:20:58 -08:00
29 changed files with 4215 additions and 6597 deletions
+53 -3
View File
@@ -21,20 +21,59 @@ jobs:
toolchain: stable
override: true
- name: Install libpcsclite-dev
run: sudo apt-get install libpcsclite-dev
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: check
# Need to install `libpscslite-dev` on Linux
linux:
name: Test Suite
strategy:
matrix:
toolchain:
- 1.39.0
- stable
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v1
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
override: true
- name: Install libpcsclite-dev
run: sudo apt-get install libpcsclite-dev
- name: Run cargo test
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: test
args: --release
- name: Run cargo build --all-features
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: build
args: --all-features
test:
name: Test Suite
strategy:
matrix:
platform:
- ubuntu-latest
- macos-latest
# TODO: support Windows after eliminating C legacy
# - windows-latest
- windows-latest
toolchain:
- 1.39.0
- stable
@@ -57,6 +96,14 @@ jobs:
command: test
args: --release
- name: Run cargo build --all-features
uses: actions-rs/cargo@v1
env:
RUSTFLAGS: -D warnings
with:
command: build
args: --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
@@ -92,6 +139,9 @@ jobs:
toolchain: stable
override: true
- name: Install libpcsclite-dev
run: sudo apt-get install libpcsclite-dev
- name: Install clippy
run: rustup component add clippy
+27
View File
@@ -4,5 +4,32 @@ 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.2] (2019-11-25)
### Added
- `untested` Cargo feature to mark untested functionality ([#30])
- Initial connect test and docs ([#19])
- Clean up APDU construction with builder API ([#15])
### Changed
- Rewrite translated code to use the `pcsc` crate ([#17])
- Rename ErrorKind to Error ([#13])
- Use `des` crate for 3DES operations ([#10])
- Replace `PKCS5_PBKDF2_HMAC_SHA1` with `pbkdf2` et al crates ([#9])
- Replace `RAND_bytes` with `getrandom` crate ([#8])
- Use `log` crate for logging ([#7])
- Replace `ErrorKind::Ok` with `Result` ([#6])
[0.0.2]: https://github.com/tarcieri/yubikey-piv.rs/pull/31
[#30]: https://github.com/tarcieri/yubikey-piv.rs/pull/30
[#19]: https://github.com/tarcieri/yubikey-piv.rs/pull/19
[#17]: https://github.com/tarcieri/yubikey-piv.rs/pull/17
[#15]: https://github.com/tarcieri/yubikey-piv.rs/pull/15
[#13]: https://github.com/tarcieri/yubikey-piv.rs/pull/13
[#10]: https://github.com/tarcieri/yubikey-piv.rs/pull/10
[#9]: https://github.com/tarcieri/yubikey-piv.rs/pull/9
[#8]: https://github.com/tarcieri/yubikey-piv.rs/pull/8
[#7]: https://github.com/tarcieri/yubikey-piv.rs/pull/7
[#6]: https://github.com/tarcieri/yubikey-piv.rs/pull/6
## 0.0.1 (2019-11-18)
- It typechecks, ship it!
+22 -4
View File
@@ -1,13 +1,12 @@
[package]
name = "yubikey-piv"
version = "0.0.1" # Also update html_root_url in lib.rs when bumping this
version = "0.0.2" # Also update html_root_url in lib.rs when bumping this
description = """
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
CCID application providing general-purpose public-key signing and encryption
application providing general-purpose public-key signing and encryption
with hardware-backed private keys for RSA (2048/1024) and ECC (P-256/P-384)
algorithms (e.g, PKCS#1v1.5, ECDSA)
"""
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
edition = "2018"
license = "BSD-2-Clause"
@@ -16,6 +15,25 @@ readme = "README.md"
categories = ["api-bindings", "cryptography", "hardware-support"]
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
[badges]
maintenance = { status = "experimental" }
[dependencies]
libc = "0.2"
des = "0.3"
getrandom = "0.1"
hmac = "0.7"
log = "0.4"
pbkdf2 = "0.3"
pcsc = "2"
sha-1 = "0.8"
subtle = "2"
zeroize = "1"
[dev-dependencies]
env_logger = "0.7"
[features]
untested = []
[package.metadata.docs.rs]
all-features = true
+129 -35
View File
@@ -10,7 +10,7 @@
[![Build Status][build-image]][build-link]
[![Gitter Chat][gitter-image]][gitter-link]
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][1] driver
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][PIV] driver
with general-purpose public-key encryption and signing support.
[Documentation][docs-link]
@@ -22,52 +22,129 @@ to store a number of RSA (2048/1024) and ECC (NIST P-256/P-384) private keys
with configurable access control policies. Both the signing (RSASSA/ECDSA) and
encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type.
See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
on which devices support PIV and the available functionality.
If you've been wanting to use Rust to sign and/or encrypt stuff using a
private key generated and stored on a Yubikey (with option PIN-based access),
this is the crate you've been after!
One small problem, it's not done yet... 😫
But it might be close?
## History
This library is a Rust translation of the [yubico-piv-tool][2] utility by
Yubico, which was originally written in C. It was mechanically translated
from C into Rust using [Corrode][3], and then subsequently heavily
refactored into safer, more idiomatic Rust§.
Note that while this project started as a fork of a [Yubico][4] project,
Note that while this project started as a fork of a [Yubico] 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.
## 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, and it has not been
thoroughly assessed to ensure its operation is constant-time on common CPU
architectures.
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!
## Requirements
## Status
- Rust 1.39+
This project is a largely incomplete work-in-progress. So far the only
functionality which has actually been tested is connecting to Yubikeys.
If you're interested helping test functionality, the table below documents
the current status of the project and relevant GitHub issues for various
functions of the YubiKey:
| | Module | Issue | Description |
|----|---------------|-------|-------------|
| 🚧 | `yubikey` | [#20] | Core functionality: auth, keys, PIN/PUK, encrypt, sign, attest |
| ⚠️ | `cccid` | [#21] | Cardholder Capability Container (CCC) IDs |
| ⚠️ | `certificate` | [#22] | Certificates for stored keys |
| ⚠️ | `chuid` | [#23] | Cardholder Unique Identifier (CHUID) |
| ⚠️ | `config` | [#24] | Support for reading on-key configuration |
| ⚠️ | `container` | [#25] | MS Container Map Records |
| ⚠️ | `key` | [#26] | Crypto key management: list, generate, import |
| ⚠️ | `mgm` | [#26] | Management Key (MGM) support: set, get, derive
| ⚠️ | `msroots` | [#28] | `msroots` file: PKCS#7 formatted certificate store for enterprise trusted roots |
Legend:
| | Description |
|----|------------------------------------|
| 🚧 | Testing and validation in progress |
| ⚠️ | Untested support |
NOTE: Commands marked ⚠️ are disabled by default as they have have not been properly tested and may contain bugs or
not work at all. USE AT YOUR OWN RISK!
Enable the `untested` feature in your `Cargo.toml` to enable features marked ⚠️
above.
## 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.
## History
This library is a Rust translation of the [yubico-piv-tool] utility by
Yubico, which was originally written in C. It was mechanically translated
from C into Rust using [Corrode], and then subsequently heavily
refactored into safer, more idiomatic Rust.
## Code of Conduct
We abide by the [Contributor Covenant][5] and ask that you do as well.
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
For more information, please see [CODE_OF_CONDUCT.md][6].
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
## License
**yubikey-piv.rs** is a fork of and originally a mechanical translation from
Yubico's [`yubico-piv-tool`][2], a C library/CLI program. The original library
was licensed under a [2-Clause BSD License][5], which this library inherits
Yubico's [yubico-piv-tool], a C library/CLI program. The original library
was licensed under a [2-Clause BSD License][BSDL], which this library inherits
as a derived work.
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
@@ -101,7 +178,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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][5] as shown above, without any additional terms
[2-Clause BSD License][BSDL] as shown above, without any additional terms
or conditions.
[//]: # (badges)
@@ -115,15 +192,32 @@ or conditions.
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[build-image]: https://github.com/tarcieri/yubikey-piv.rs/workflows/Rust/badge.svg
[build-link]: https://github.com/tarcieri/yubikey-piv.rs/actions
[gitter-image]: https://badges.gitter.im/tarcieri/yubihsm-piv.rs.svg
[gitter-link]: https://gitter.im/tarcieri/community
[gitter-image]: https://badges.gitter.im/yubihsm-piv-rs.svg
[gitter-link]: https://gitter.im/yubikey-piv-rs/community
[//]: # (general links)
[1]: https://piv.idmanagement.gov/
[2]: https://github.com/Yubico/yubico-piv-tool/
[3]: https://github.com/jameysharp/corrode
[4]: https://www.yubico.com/
[5]: https://contributor-covenant.org/
[6]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
[7]: https://opensource.org/licenses/BSD-2-Clause
[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/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
[//]: # (github issues)
[#18]: https://github.com/tarcieri/yubikey-piv.rs/issues/18
[#20]: https://github.com/tarcieri/yubikey-piv.rs/issues/20
[#21]: https://github.com/tarcieri/yubikey-piv.rs/issues/21
[#22]: https://github.com/tarcieri/yubikey-piv.rs/issues/22
[#23]: https://github.com/tarcieri/yubikey-piv.rs/issues/23
[#24]: https://github.com/tarcieri/yubikey-piv.rs/issues/24
[#25]: https://github.com/tarcieri/yubikey-piv.rs/issues/25
[#26]: https://github.com/tarcieri/yubikey-piv.rs/issues/26
[#27]: https://github.com/tarcieri/yubikey-piv.rs/issues/27
[#28]: https://github.com/tarcieri/yubikey-piv.rs/issues/28
+127 -32
View File
@@ -1,56 +1,152 @@
//! Application Protocol Data Unit (APDU)
use zeroize::Zeroize;
// 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};
/// 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.
/// These messages are packets used to communicate with the YubiKey.
#[derive(Clone)]
pub struct APDU {
/// Instruction class - indicates the type of command, e.g. interindustry or proprietary
pub cla: u8,
pub(crate) struct APDU {
/// 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"
pub ins: u8,
/// 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
pub p1: u8,
/// 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
pub p2: u8,
p2: u8,
/// Length of command - encodes the number of bytes of command data to follow
pub lc: u8,
/// Command data
pub data: [u8; 255],
/// Command data to be sent (`lc` is calculated as `data.len()`)
data: Vec<u8>,
}
impl APDU {
/// Get a const pointer to this APDU
// TODO(tarcieri): eliminate pointers and use all safe references
pub(crate) fn as_ptr(&self) -> *const APDU {
/// Create a new APDU with the given instruction code
pub fn new(ins: u8) -> Self {
Self {
cla: 0,
ins,
p1: 0,
p2: 0,
data: vec![],
}
}
/// Set this APDU's class
#[cfg(feature = "untested")]
pub fn cla(&mut self, value: u8) -> &mut Self {
self.cla = value;
self
}
/// Get a mut pointer to this APDU
// TODO(tarcieri): eliminate pointers and use all safe references
pub(crate) fn as_mut_ptr(&mut self) -> *mut APDU {
/// Set this APDU's first parameter only
pub fn p1(&mut self, value: u8) -> &mut Self {
self.p1 = value;
self
}
/// Set both parameters for this APDU
#[cfg(feature = "untested")]
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
self.p1 = p1;
self.p2 = p2;
self
}
/// Set the command data for this APDU.
///
/// Panics if the byte slice is more than 255 bytes!
pub fn data(&mut self, bytes: impl AsRef<[u8]>) -> &mut Self {
assert!(self.data.is_empty(), "APDU command already set!");
let bytes = bytes.as_ref();
assert!(
bytes.len() <= APDU_DATA_MAX,
"APDU command data too long: {} (max: {})",
bytes.len(),
APDU_DATA_MAX
);
self.data.extend_from_slice(bytes);
self
}
/// Transmit this APDU using the given card transaction
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> {
let response_bytes = txn.transmit(&self.to_bytes(), recv_len)?;
Ok(Response::from_bytes(response_bytes))
}
/// Consume this APDU and return a self-zeroizing buffer
pub fn to_bytes(&self) -> Buffer {
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.data.len() as u8);
bytes.extend_from_slice(self.data.as_ref());
Zeroizing::new(bytes)
}
}
impl Default for APDU {
fn default() -> Self {
Self {
cla: 0,
ins: 0,
p1: 0,
p2: 0,
lc: 0,
data: [0u8; 255],
impl Debug for APDU {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"APDU {{ cla: {}, ins: {}, p1: {}, p2: {}, lc: {}, data: {:?} }}",
self.cla,
self.ins,
self.p1,
self.p2,
self.data.len(),
self.data.as_slice()
)
}
}
impl Drop for APDU {
fn drop(&mut self) {
self.zeroize();
}
}
@@ -60,7 +156,6 @@ impl Zeroize for APDU {
self.ins.zeroize();
self.p1.zeroize();
self.p2.zeroize();
self.lc.zeroize();
self.data.zeroize();
}
}
+85
View File
@@ -0,0 +1,85 @@
//! Cardholder Capability Container (CCC) ID Support
// 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::{consts::*, error::Error, yubikey::YubiKey};
use getrandom::getrandom;
/// Cardholder Capability Container (CCC) Template
///
/// f0: Card Identifier
///
/// - 0xa000000116 == GSC-IS RID
/// - 0xff == Manufacturer ID (dummy)
/// - 0x02 == Card type (javaCard)
/// - next 14 bytes: card ID
const CCC_TMPL: &[u8] = &[
0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4,
0x01, 0x00, 0xf5, 0x01, 0x10, 0xf6, 0x00, 0xf7, 0x00, 0xfa, 0x00, 0xfb, 0x00, 0xfc, 0x00, 0xfd,
0x00, 0xfe, 0x00,
];
/// Cardholder Capability Container (CCC) Identifier
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct CCCID([u8; YKPIV_CCCID_SIZE]);
impl CCCID {
/// Generate a random CCCID
pub fn generate() -> Result<Self, Error> {
let mut id = [0u8; YKPIV_CCCID_SIZE];
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
Ok(CCCID(id))
}
/// Get Cardholder Capability Container (CCC) ID
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> {
let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(YKPIV_OBJ_CAPABILITY)?;
if response.len() != CCC_TMPL.len() {
return Err(Error::GenericError);
}
let mut cccid = [0u8; YKPIV_CCCID_SIZE];
cccid.copy_from_slice(&response[CCC_ID_OFFS..(CCC_ID_OFFS + YKPIV_CCCID_SIZE)]);
Ok(CCCID(cccid))
}
/// Get Cardholder Capability Container (CCC) ID
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
let mut buf = CCC_TMPL.to_vec();
buf[CCC_ID_OFFS..(CCC_ID_OFFS + self.0.len())].copy_from_slice(&self.0);
let txn = yubikey.begin_transaction()?;
txn.save_object(YKPIV_OBJ_CAPABILITY, &buf)
}
}
+186
View File
@@ -0,0 +1,186 @@
//! YubiKey Certificates
// 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::{
consts::*,
error::Error,
key::{self, SlotId},
serialization::*,
transaction::Transaction,
yubikey::YubiKey,
Buffer,
};
use log::error;
use std::ptr;
use zeroize::Zeroizing;
/// Certificates
#[derive(Clone, Debug)]
pub struct Certificate(Buffer);
impl Certificate {
/// Read a certificate from the given slot in the YubiKey
pub fn read(yubikey: &mut YubiKey, slot: SlotId) -> Result<Self, Error> {
let txn = yubikey.begin_transaction()?;
let buf = read_certificate(&txn, slot)?;
if buf.is_empty() {
return Err(Error::InvalidObject);
}
Ok(Certificate(buf))
}
/// Write this certificate into the YubiKey in the given slot
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> {
let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, Some(&self.0), certinfo, max_size)
}
/// Delete a certificate located at the given slot of the given YubiKey
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<(), Error> {
let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, None, 0, max_size)
}
/// Initialize a local certificate struct from the given bytebuffer
pub fn new(cert: impl Into<Buffer>) -> Result<Self, Error> {
let cert = cert.into();
if cert.is_empty() {
error!("certificate cannot be empty");
return Err(Error::SizeError);
}
Ok(Certificate(cert))
}
/// Extract the inner buffer
pub fn into_buffer(self) -> Buffer {
self.0
}
}
impl AsRef<[u8]> for Certificate {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Read certificate
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> {
let mut len: usize = 0;
let object_id = key::slot_object(slot)?;
let mut buf = match txn.fetch_object(object_id) {
Ok(b) => b,
Err(_) => {
// TODO(tarcieri): is this really ok?
return Ok(Zeroizing::new(vec![]));
}
};
if buf.len() < CB_OBJ_TAG_MIN {
// TODO(tarcieri): is this really ok?
return Ok(Zeroizing::new(vec![]));
}
if buf[0] == TAG_CERT {
let offset = 1 + get_length(&buf[1..], &mut len);
if len > buf.len() - offset {
// TODO(tarcieri): is this really ok?
return Ok(Zeroizing::new(vec![]));
}
unsafe {
ptr::copy(buf.as_ptr().add(offset), buf.as_mut_ptr(), len);
}
buf.truncate(len);
}
Ok(buf)
}
/// Write certificate
pub(crate) fn write_certificate(
txn: &Transaction<'_>,
slot: SlotId,
data: Option<&[u8]>,
certinfo: u8,
max_size: usize,
) -> Result<(), Error> {
let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = 0;
let object_id = key::slot_object(slot)?;
if data.is_none() {
return txn.save_object(object_id, &[]);
}
let data = data.unwrap();
let mut req_len = 1 /* cert tag */ + 3 /* compression tag + data*/ + 2 /* lrc */;
req_len += set_length(&mut buf, data.len());
req_len += data.len();
if req_len < data.len() || req_len > max_size {
return Err(Error::SizeError);
}
buf[offset] = TAG_CERT;
offset += 1;
offset += set_length(&mut buf[offset..], data.len());
buf[offset..(offset + data.len())].copy_from_slice(&data);
offset += data.len();
// write compression info and LRC trailer
buf[offset] = TAG_CERT_COMPRESS;
buf[offset + 1] = 0x01;
buf[offset + 2] = if certinfo == YKPIV_CERTINFO_GZIP {
0x01
} else {
0x00
};
buf[offset + 3] = TAG_CERT_LRC;
buf[offset + 4] = 00;
offset += 5;
txn.save_object(object_id, &buf[..offset])
}
+92
View File
@@ -0,0 +1,92 @@
//! Cardholder Unique Identifier (CHUID) Support
// 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::{consts::*, error::Error, yubikey::YubiKey};
use getrandom::getrandom;
/// Cardholder Unique Identifier (CHUID) Template
///
/// Format defined in SP-800-73-4, Appendix A, Table 9
///
/// FASC-N containing S9999F9999F999999F0F1F0000000000300001E encoded in
/// 4-bit BCD with 1 bit parity. run through the tools/fasc.pl script to get
/// bytes. This CHUID has an expiry of 2030-01-01.
///
/// Defined fields:
///
/// - 0x30: FASC-N (hard-coded)
/// - 0x34: Card UUID / GUID (settable)
/// - 0x35: Exp. Date (hard-coded)
/// - 0x3e: Signature (hard-coded, empty)
/// - 0xfe: Error Detection Code (hard-coded)
const CHUID_TMPL: &[u8] = &[
0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58,
0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x08, 0x32,
0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
];
/// Cardholder Unique Identifier (CHUID)
#[derive(Copy, Clone, Debug)]
pub struct CHUID([u8; YKPIV_CARDID_SIZE]);
impl CHUID {
/// Generate a random Cardholder Unique Identifier (CHUID)
pub fn generate() -> Result<Self, Error> {
let mut id = [0u8; YKPIV_CARDID_SIZE];
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
Ok(CHUID(id))
}
/// Get Cardholder Unique Identifier (CHUID)
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> {
let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(YKPIV_OBJ_CHUID)?;
if response.len() != CHUID_TMPL.len() {
return Err(Error::GenericError);
}
let mut cardid = [0u8; YKPIV_CARDID_SIZE];
cardid.copy_from_slice(&response[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + YKPIV_CARDID_SIZE)]);
Ok(CHUID(cardid))
}
/// Set Cardholder Unique Identifier (CHUID)
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
let mut buf = CHUID_TMPL.to_vec();
buf[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + self.0.len())].copy_from_slice(&self.0);
let txn = yubikey.begin_transaction()?;
txn.save_object(YKPIV_OBJ_CHUID, &buf)
}
}
+127
View File
@@ -0,0 +1,127 @@
//! YubiKey Configuration Values
// 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::{consts::*, error::Error, metadata, mgm::MgmType, yubikey::YubiKey};
use log::error;
use std::convert::TryInto;
/// Config
#[derive(Copy, Clone)]
pub struct Config {
/// Protected data available
protected_data_available: bool,
/// PUK blocked
puk_blocked: bool,
/// No block on upgrade
puk_noblock_on_upgrade: bool,
/// PIN last changed
pin_last_changed: u32,
/// MGM type
mgm_type: MgmType,
}
impl Config {
/// Get YubiKey config
pub fn get(yubikey: &mut YubiKey) -> Result<Config, Error> {
let mut config = Config {
protected_data_available: false,
puk_blocked: false,
puk_noblock_on_upgrade: false,
pin_last_changed: 0,
mgm_type: MgmType::Manual,
};
let txn = yubikey.begin_transaction()?;
if let Ok(data) = metadata::read(&txn, TAG_ADMIN) {
if let Ok(item) = metadata::get_item(&data, TAG_ADMIN_FLAGS_1) {
if item.is_empty() {
error!("empty response for admin flags metadata item! ignoring");
} else {
if item[0] & ADMIN_FLAGS_1_PUK_BLOCKED != 0 {
config.puk_blocked = true;
}
if item[0] & ADMIN_FLAGS_1_PROTECTED_MGM != 0 {
config.mgm_type = MgmType::Protected;
}
}
}
if metadata::get_item(&data, TAG_ADMIN_SALT).is_ok() {
if config.mgm_type != MgmType::Manual {
error!("conflicting types of MGM key administration configured");
} else {
config.mgm_type = MgmType::Derived;
}
}
if let Ok(item) = metadata::get_item(&data, TAG_ADMIN_TIMESTAMP) {
if item.len() != CB_ADMIN_TIMESTAMP {
error!("pin timestamp in admin metadata is an invalid size");
} else {
// TODO(tarcieri): double check this is little endian
config.pin_last_changed = u32::from_le_bytes(item.try_into().unwrap());
}
}
}
if let Ok(data) = metadata::read(&txn, TAG_PROTECTED) {
config.protected_data_available = true;
if let Ok(item) = metadata::get_item(&data, TAG_PROTECTED_FLAGS_1) {
if item.is_empty() {
error!("empty response for protected flags metadata item! ignoring");
} else if item[0] & PROTECTED_FLAGS_1_PUK_NOBLOCK != 0 {
config.puk_noblock_on_upgrade = true;
}
}
if metadata::get_item(&data, TAG_PROTECTED_MGM).is_ok() {
if config.mgm_type != MgmType::Protected {
error!(
"conflicting types of mgm key administration configured: protected MGM exists"
);
}
// Always favor protected MGM
config.mgm_type = MgmType::Protected;
}
}
Ok(config)
}
}
+29 -5
View File
@@ -36,14 +36,32 @@
pub const szLOG_SOURCE: &str = "yubikey-piv.rs";
pub const CB_OBJ_MAX: usize = 3063;
pub const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
pub const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
pub const CB_ADMIN_TIMESTAMP: usize = 0x04;
pub const CB_ADMIN_SALT: usize = 16;
pub const CB_ATR_MAX: usize = 33;
pub const CB_BUF_MAX_NEO: usize = 2048;
pub const CB_BUF_MAX_YK4: usize = 3072;
pub const CB_BUF_MAX: usize = CB_BUF_MAX_YK4;
pub const CB_ECC_POINTP256: usize = 65;
pub const CB_ECC_POINTP384: usize = 97;
pub const CB_OBJ_MAX_YK4: usize = CB_BUF_MAX_YK4 - 9;
pub const CB_OBJ_MAX: usize = CB_OBJ_MAX_YK4;
pub const CB_OBJ_MAX_NEO: usize = CB_BUF_MAX_NEO - 9;
pub const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
pub const CB_OBJ_TAG_MAX: usize = (CB_OBJ_TAG_MIN + 2); // 1 byte tag + 3 bytes len
pub const CB_PAGE: usize = 4096;
pub const CB_PIN_MAX: usize = 8;
pub const CB_ECC_POINTP256: usize = 65;
pub const CB_ECC_POINTP384: usize = 97;
pub const CCC_ID_OFFS: usize = 9;
pub const CHUID_GUID_OFFS: usize = 29;
@@ -51,6 +69,9 @@ pub const CHREF_ACT_CHANGE_PIN: i32 = 0;
pub const CHREF_ACT_UNBLOCK_PIN: i32 = 1;
pub const CHREF_ACT_CHANGE_PUK: i32 = 2;
pub const CONTAINER_NAME_LEN: usize = 40;
pub const CONTAINER_REC_LEN: usize = (2 * CONTAINER_NAME_LEN) + 27; // 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20
pub const DES_TYPE_3DES: u8 = 1;
pub const DES_LEN_DES: usize = 8;
@@ -65,6 +86,10 @@ pub const DEVTYPE_NEOr3: u32 = (DEVTYPE_NEO | 0x0000_7233); //"r3"
pub const DEVTYPE_YK4: u32 = (DEVTYPE_YK | 0x0000_0034); // "4"
pub const DEVYTPE_YK5: u32 = (DEVTYPE_YK | 0x0000_0035); // "5"
pub const ITER_MGM_PBKDF2: usize = 10000;
pub const PROTECTED_FLAGS_1_PUK_NOBLOCK: u8 = 0x01;
// sw is status words, see NIST special publication 800-73-4, section 5.6
pub const SW_SUCCESS: i32 = 0x9000;
@@ -122,6 +147,7 @@ pub const YKPIV_INS_SELECT_APPLICATION: u8 = 0xa4;
pub const YKPIV_INS_GET_RESPONSE_APDU: u8 = 0xc0;
// Yubico vendor specific instructions
// <https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html>
pub const YKPIV_INS_SET_MGMKEY: u8 = 0xff;
pub const YKPIV_INS_IMPORT_KEY: u8 = 0xfe;
pub const YKPIV_INS_GET_VERSION: u8 = 0xfd;
@@ -203,8 +229,6 @@ pub const YKPIV_OBJ_MSROOTS3: u32 = 0x005f_ff13;
pub const YKPIV_OBJ_MSROOTS4: u32 = 0x005f_ff14;
pub const YKPIV_OBJ_MSROOTS5: u32 = 0x005f_ff15;
pub const YKPIV_OBJ_MAX_SIZE: usize = 3072;
pub const YKPIV_PINPOLICY_TAG: u8 = 0xaa;
pub const YKPIV_PINPOLICY_DEFAULT: u8 = 0;
pub const YKPIV_PINPOLICY_NEVER: u8 = 1;
+227
View File
@@ -0,0 +1,227 @@
//! MS Container Map Records
//!
//! These appear(?) to be defined in Microsoft's Smart Card Minidriver Specification:
//! <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
// 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::{consts::*, error::Error, key::SlotId, serialization::*, yubikey::YubiKey};
use log::error;
use std::{
convert::{TryFrom, TryInto},
fmt::{self, Debug},
};
/// MS Container Map(?) Records
#[derive(Copy, Clone)]
pub struct Container {
/// Container name
pub name: [u16; CONTAINER_NAME_LEN],
/// Card slot
pub slot: SlotId,
/// Key spec
pub key_spec: u8,
/// Key size in bits
pub key_size_bits: u16,
/// Flags
pub flags: u8,
/// PIN ID
pub pin_id: u8,
/// Associated ECHD(?) container (typo of "ecdh" perhaps?)
pub associated_echd_container: u8,
/// Cert fingerprint
pub cert_fingerprint: [u8; 20],
}
impl Container {
/// Read MS Container Map records
pub fn read_mscmap(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(YKPIV_OBJ_MSCMAP)?;
let mut containers = vec![];
if response.len() < CB_OBJ_TAG_MIN {
// TODO(tarcieri): is this really OK?
return Ok(containers);
}
if response[0] != TAG_MSCMAP {
// TODO(tarcieri): yubico-piv-tool returned success here? should we?
return Err(Error::InvalidObject);
}
let mut len = 0;
let offset = 1 + get_length(&response[1..], &mut len);
if len > response.len() - offset {
// TODO(tarcieri): is this really OK?
return Ok(containers);
}
for chunk in response[offset..(offset + len)].chunks_exact(CONTAINER_REC_LEN) {
containers.push(Container::new(chunk)?);
}
Ok(containers)
}
/// Write MS Container Map records.
pub fn write_mscmap(yubikey: &mut YubiKey, containers: &[Self]) -> Result<(), Error> {
let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = 0;
let n_containers = containers.len();
let data_len = n_containers * CONTAINER_REC_LEN;
let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
if n_containers == 0 {
return txn.save_object(YKPIV_OBJ_MSCMAP, &[]);
}
let req_len = 1 + set_length(&mut buf, data_len) + data_len;
if req_len > max_size {
return Err(Error::SizeError);
}
buf[offset] = TAG_MSCMAP;
offset += 1;
offset += set_length(&mut buf[offset..], data_len);
for (i, chunk) in buf[..data_len]
.chunks_exact_mut(CONTAINER_REC_LEN)
.enumerate()
{
chunk.copy_from_slice(&containers[i].to_bytes());
}
offset += data_len;
txn.save_object(YKPIV_OBJ_MSCMAP, &buf[..offset])
}
/// Parse a container record from a byte slice
pub fn new(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() != CONTAINER_REC_LEN {
error!(
"couldn't parse PIV container: expected {}-bytes, got {}-bytes",
CONTAINER_REC_LEN,
bytes.len()
);
return Err(Error::ParseError);
}
let mut name = [0u16; CONTAINER_NAME_LEN];
let name_bytes_len = CONTAINER_NAME_LEN * 2;
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
name[i] = u16::from_le_bytes(chunk.try_into().unwrap());
}
let mut cert_fingerprint = [0u8; 20];
cert_fingerprint.copy_from_slice(&bytes[(bytes.len() - 20)..]);
Ok(Container {
name,
slot: bytes[name_bytes_len],
key_spec: bytes[name_bytes_len + 1],
key_size_bits: u16::from_le_bytes(
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
.try_into()
.unwrap(),
),
flags: bytes[name_bytes_len + 4],
pin_id: bytes[name_bytes_len + 5],
associated_echd_container: bytes[name_bytes_len + 6],
cert_fingerprint,
})
}
/// Parse the container name as a UTF-16 string
pub fn parse_name(&self) -> Result<String, Error> {
String::from_utf16(&self.name).map_err(|_| Error::ParseError)
}
/// Serialize a container record as a byte size
pub fn to_bytes(&self) -> [u8; CONTAINER_REC_LEN] {
let mut bytes = Vec::with_capacity(CONTAINER_REC_LEN);
for i in 0..CONTAINER_NAME_LEN {
bytes.extend_from_slice(&self.name[i].to_le_bytes());
}
bytes.push(self.slot);
bytes.push(self.key_spec);
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
bytes.push(self.flags);
bytes.push(self.pin_id);
bytes.push(self.associated_echd_container);
bytes.extend_from_slice(&self.cert_fingerprint);
// TODO(tarcieri): use TryInto here when const generics are available
let mut result = [0u8; CONTAINER_REC_LEN];
result.copy_from_slice(&bytes);
result
}
}
impl Debug for Container {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"PivContainer {{ name: {:?}, slot: {}, key_spec: {}, key_size_bits: {}, \
flags: {}, pin_id: {}, associated_echd_container: {}, cert_fingerprint: {:?} }}",
&self.name[..],
self.slot,
self.key_spec,
self.key_size_bits,
self.flags,
self.pin_id,
self.associated_echd_container,
&self.cert_fingerprint[..]
)
}
}
impl<'a> TryFrom<&'a [u8]> for Container {
type Error = Error;
fn try_from(bytes: &'a [u8]) -> Result<Self, Error> {
Self::new(bytes)
}
}
+58 -54
View File
@@ -30,20 +30,19 @@
// (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 std::fmt;
use std::fmt::{self, Display};
/// Kinds of errors
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
/// OK
// TODO(tarcieri): replace this with proper result types
Ok,
pub enum Error {
/// Memory error
MemoryError,
/// PCSC error
PcscError,
PcscError {
/// Original PC/SC error
inner: Option<pcsc::Error>,
},
/// Size error
SizeError,
@@ -67,7 +66,10 @@ pub enum ErrorKind {
ParseError,
/// Wrong PIN
WrongPin,
WrongPin {
/// Number of tries remaining
tries: u32,
},
/// Invalid object
InvalidObject,
@@ -88,73 +90,75 @@ pub enum ErrorKind {
NotSupported,
}
impl ErrorKind {
impl Error {
/// Name of the error.
///
/// These names map to the legacy names from the Yubico C library, to
/// assist in web searches for relevant information for these errors.
pub fn name(self) -> &'static str {
match self {
ErrorKind::Ok => "YKPIV_OK",
ErrorKind::MemoryError => "YKPIV_MEMORY_ERROR",
ErrorKind::PcscError => "YKPIV_PCSC_ERROR",
ErrorKind::SizeError => "YKPIV_SIZE_ERROR",
ErrorKind::AppletError => "YKPIV_APPLET_ERROR",
ErrorKind::AuthenticationError => "YKPIV_AUTHENTICATION_ERROR",
ErrorKind::RandomnessError => "YKPIV_RANDOMNESS_ERROR",
ErrorKind::GenericError => "YKPIV_GENERIC_ERROR",
ErrorKind::KeyError => "YKPIV_KEY_ERROR",
ErrorKind::ParseError => "YKPIV_PARSE_ERROR",
ErrorKind::WrongPin => "YKPIV_WRONG_PIN",
ErrorKind::InvalidObject => "YKPIV_INVALID_OBJECT",
ErrorKind::AlgorithmError => "YKPIV_ALGORITHM_ERROR",
ErrorKind::PinLocked => "YKPIV_PIN_LOCKED",
ErrorKind::ArgumentError => "YKPIV_ARGUMENT_ERROR",
ErrorKind::RangeError => "YKPIV_RANGE_ERROR",
ErrorKind::NotSupported => "YKPIV_NOT_SUPPORTED",
Error::MemoryError => "YKPIV_MEMORY_ERROR",
Error::PcscError { .. } => "YKPIV_PCSC_ERROR",
Error::SizeError => "YKPIV_SIZE_ERROR",
Error::AppletError => "YKPIV_APPLET_ERROR",
Error::AuthenticationError => "YKPIV_AUTHENTICATION_ERROR",
Error::RandomnessError => "YKPIV_RANDOMNESS_ERROR",
Error::GenericError => "YKPIV_GENERIC_ERROR",
Error::KeyError => "YKPIV_KEY_ERROR",
Error::ParseError => "YKPIV_PARSE_ERROR",
Error::WrongPin { .. } => "YKPIV_WRONG_PIN",
Error::InvalidObject => "YKPIV_INVALID_OBJECT",
Error::AlgorithmError => "YKPIV_ALGORITHM_ERROR",
Error::PinLocked => "YKPIV_PIN_LOCKED",
Error::ArgumentError => "YKPIV_ARGUMENT_ERROR",
Error::RangeError => "YKPIV_RANGE_ERROR",
Error::NotSupported => "YKPIV_NOT_SUPPORTED",
}
}
/// Error message
pub fn msg(self) -> &'static str {
match self {
ErrorKind::Ok => "OK",
ErrorKind::MemoryError => "memory error",
ErrorKind::PcscError => "PCSC error",
ErrorKind::SizeError => "size error",
ErrorKind::AppletError => "applet error",
ErrorKind::AuthenticationError => "authentication error",
ErrorKind::RandomnessError => "randomness error",
ErrorKind::GenericError => "generic error",
ErrorKind::KeyError => "key error",
ErrorKind::ParseError => "parse error",
ErrorKind::WrongPin => "wrong pin",
ErrorKind::InvalidObject => "invalid object",
ErrorKind::AlgorithmError => "algorithm error",
ErrorKind::PinLocked => "PIN locked",
ErrorKind::ArgumentError => "argument error",
ErrorKind::RangeError => "range error",
ErrorKind::NotSupported => "not supported",
Error::MemoryError => "memory error",
Error::PcscError { .. } => "PCSC error",
Error::SizeError => "size error",
Error::AppletError => "applet error",
Error::AuthenticationError => "authentication error",
Error::RandomnessError => "randomness error",
Error::GenericError => "generic error",
Error::KeyError => "key error",
Error::ParseError => "parse error",
Error::WrongPin { .. } => "wrong pin",
Error::InvalidObject => "invalid object",
Error::AlgorithmError => "algorithm error",
Error::PinLocked => "PIN locked",
Error::ArgumentError => "argument error",
Error::RangeError => "range error",
Error::NotSupported => "not supported",
}
}
}
impl fmt::Display for ErrorKind {
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.msg())
}
}
impl std::error::Error for ErrorKind {}
/// Get a string representation of this error
// TODO(tarcieri): completely replace this with `Display`
pub fn ykpiv_strerror(err: ErrorKind) -> &'static str {
err.msg()
impl From<pcsc::Error> for Error {
fn from(err: pcsc::Error) -> Error {
Error::PcscError { inner: Some(err) }
}
}
/// Get the name of this error
// TODO(tarcieri): completely replace this with debug
pub fn ykpiv_strerror_name(err: ErrorKind) -> &'static str {
err.name()
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
#[allow(trivial_casts)] // why doesn't this work without the cast???
Error::PcscError { inner } => inner
.as_ref()
.map(|err| err as &(dyn std::error::Error + 'static)),
_ => None,
}
}
}
-427
View File
@@ -1,427 +0,0 @@
//! Internal functions (mostly 3DES)
// TODO(tarcieri): replace OpenSSL extern "C" invocations with `des` crate
// - crate: https://crates.io/crates/des
// - docs: https://docs.rs/des/0
// 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.
// TODO(tarcieri): investigate and remove dead code
#![allow(non_upper_case_globals, dead_code)]
#![allow(clippy::missing_safety_doc)]
use crate::consts::*;
use libc::{
c_char, c_int, fclose, feof, fgets, fopen, free, getenv, malloc, memcpy, memset, sscanf,
strcasecmp, strcmp,
};
use std::{
ffi::{CStr, CString},
mem,
os::raw::c_void,
};
extern "C" {
fn DES_ecb3_encrypt(
input: *mut [u8; 8],
output: *mut [u8; 8],
ks1: *mut DesSubKey,
ks2: *mut DesSubKey,
ks3: *mut DesSubKey,
enc: i32,
);
fn DES_is_weak_key(key: *mut [u8; 8]) -> i32;
fn DES_set_key_unchecked(key: *mut [u8; 8], schedule: *mut DesSubKey);
fn PKCS5_PBKDF2_HMAC_SHA1(
pass: *const u8,
passlen: i32,
salt: *const u8,
saltlen: i32,
iter: i32,
keylen: i32,
out: *mut u8,
) -> i32;
fn RAND_bytes(buf: *mut u8, num: i32) -> i32;
}
/// DES-related errors
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum DesErrorKind {
/// Ok
Ok = 0,
/// Invalid parameter
InvalidParameter = -1,
/// Buffer too small
BufferTooSmall = -2,
/// Memory error
MemoryError = -3,
/// General error
GeneralError = -4,
}
/// 3DES subkeys
#[derive(Copy, Clone)]
#[repr(C)]
pub struct DesSubKey([u8; 16]);
/// 3DES keys
#[derive(Copy, Clone)]
pub struct DesKey {
/// subkey 1
pub ks1: DesSubKey,
/// subkey 2
pub ks2: DesSubKey,
/// subkey 3
pub ks3: DesSubKey,
}
/// Import DES key
pub unsafe fn des_import_key(
key_type: i32,
keyraw: *const u8,
keyrawlen: usize,
key: *mut *mut DesKey,
) -> DesErrorKind {
let mut key_tmp = [0u8; 8];
let cb_expectedkey: usize;
let cb_keysize: usize;
if key_type != DES_TYPE_3DES as i32 {
return DesErrorKind::InvalidParameter;
}
cb_expectedkey = (8i32 * 3i32) as (usize);
cb_keysize = 8usize;
if cb_keysize > 8 {
return DesErrorKind::MemoryError;
}
if key.is_null() || keyraw.is_null() || keyrawlen != cb_expectedkey {
return DesErrorKind::InvalidParameter;
}
*key = malloc(mem::size_of::<DesKey>()) as (*mut DesKey);
if (*key).is_null() {
return DesErrorKind::MemoryError;
}
memset(*key as (*mut c_void), 0i32, mem::size_of::<DesKey>());
memcpy(
key_tmp.as_mut_ptr() as (*mut c_void),
keyraw as (*const c_void),
cb_keysize,
);
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks1);
memcpy(
key_tmp.as_mut_ptr() as (*mut c_void),
keyraw.add(cb_keysize) as (*const c_void),
cb_keysize,
);
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks2);
memcpy(
key_tmp.as_mut_ptr() as (*mut c_void),
keyraw.add(2usize.wrapping_mul(cb_keysize)) as (*const c_void),
cb_keysize,
);
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks3);
DesErrorKind::Ok
}
/// Destroy DES key
pub unsafe fn des_destroy_key(key: *mut DesKey) -> DesErrorKind {
if !key.is_null() {
free(key as (*mut c_void));
}
DesErrorKind::Ok
}
/// Encrypt with DES key
pub unsafe fn des_encrypt(
key: *mut DesKey,
input: *const u8,
inputlen: usize,
out: *mut u8,
outlen: *mut usize,
) -> DesErrorKind {
if key.is_null() || outlen.is_null() || *outlen < inputlen || input.is_null() || out.is_null() {
return DesErrorKind::InvalidParameter;
}
DES_ecb3_encrypt(
input as *mut [u8; 8],
out as *mut [u8; 8],
&mut (*key).ks1,
&mut (*key).ks2,
&mut (*key).ks3,
1,
);
DesErrorKind::Ok
}
/// Decrypt with DES key
pub unsafe fn des_decrypt(
key: *mut DesKey,
in_: *const u8,
inlen: usize,
out: *mut u8,
outlen: *mut usize,
) -> DesErrorKind {
if key.is_null() || outlen.is_null() || *outlen < inlen || in_.is_null() || out.is_null() {
return DesErrorKind::InvalidParameter;
}
DES_ecb3_encrypt(
in_ as *mut [u8; 8],
out as *mut [u8; 8],
&mut (*key).ks1,
&mut (*key).ks2,
&mut (*key).ks3,
0,
);
DesErrorKind::Ok
}
/// Is the given DES key weak?
pub unsafe fn yk_des_is_weak_key(key: *const u8, _cb_key: usize) -> bool {
DES_is_weak_key(key as (*mut [u8; 8])) != 0
}
/// PRNG errors/results
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum PRngErrorKind {
/// Ok
Ok = 0,
/// General error
GeneralError = -1,
}
/// Generate bytes with the PRNG
pub unsafe fn _ykpiv_prng_generate(buffer: *mut u8, cb_req: usize) -> PRngErrorKind {
if RAND_bytes(buffer, cb_req as (i32)) != -1 {
PRngErrorKind::Ok
} else {
PRngErrorKind::GeneralError
}
}
/// PKCS#5 error types
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum Pkcs5ErrorKind {
/// OK
Ok = 0,
/// General error
GeneralError = -1,
}
/// Decrypt a PKCS#5 key
pub unsafe fn pkcs5_pbkdf2_sha1(
password: *const u8,
cb_password: usize,
salt: *const u8,
cb_salt: usize,
iterations: usize,
key: *const u8,
cb_key: usize,
) -> Pkcs5ErrorKind {
PKCS5_PBKDF2_HMAC_SHA1(
password,
cb_password as (i32),
salt,
cb_salt as (i32),
iterations as (i32),
cb_key as (i32),
key as (*mut u8),
);
Pkcs5ErrorKind::Ok
}
/// Strip whitespace
// TODO(tarcieri): implement this
pub unsafe fn _strip_ws(sz: *mut c_char) -> *mut c_char {
sz
}
/// Source of how a setting was configured
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SettingSource {
/// User-specified setting
User,
/// Admin-specified setting
Admin,
/// Default setting
Default,
}
/// Setting booleans
#[derive(Copy, Clone, Debug)]
pub struct SettingBool {
/// Boolean value
pub value: bool,
/// Source of the configuration setting (user/admin/default)
pub source: SettingSource,
}
/// Get a boolean config value
pub unsafe fn _get_bool_config(sz_setting: *const c_char) -> SettingBool {
let mut setting: SettingBool = SettingBool {
value: false,
source: SettingSource::Default,
};
let mut sz_line = [0u8; 256];
let mut psz_name: *mut c_char;
let mut psz_value: *mut c_char;
let mut sz_name = [0u8; 256];
let mut sz_value = [0u8; 256];
let pf = fopen(
b"/etc/yubico/yubikeypiv.conf\0".as_ptr() as *const c_char,
b"r\0".as_ptr() as *const c_char,
);
if pf.is_null() {
return setting;
}
while feof(pf) == 0 {
if fgets(
sz_line.as_mut_ptr() as *mut c_char,
sz_line.len() as c_int,
pf,
)
.is_null()
{
continue;
}
if sz_line[0] == b'#' {
continue;
}
if sz_line[0] == b'\r' {
continue;
}
if sz_line[0] == b'\n' {
continue;
}
if sscanf(
sz_line.as_ptr() as *const c_char,
b"%255[^=]=%255s\0".as_ptr() as *const c_char,
sz_name.as_mut_ptr(),
sz_value.as_mut_ptr(),
) != 2
{
continue;
}
psz_name = _strip_ws(sz_name.as_mut_ptr() as *mut c_char);
if strcasecmp(psz_name, sz_setting) != 0 {
continue;
}
psz_value = _strip_ws(sz_value.as_mut_ptr() as *mut c_char);
setting.source = SettingSource::Admin;
setting.value = strcmp(psz_value, b"1\0".as_ptr() as *const c_char) == 0
|| strcasecmp(psz_value, b"true\0".as_ptr() as *const c_char) == 0;
}
fclose(pf);
setting
}
/// Get a setting boolean from an environment variable
pub unsafe fn _get_bool_env(sz_setting: *const c_char) -> SettingBool {
let mut setting: SettingBool = SettingBool {
value: false,
source: SettingSource::Default,
};
let sz_name = CString::new(format!(
"YUBIKEY_PIV_{}",
CStr::from_ptr(sz_setting).to_string_lossy()
))
.unwrap();
let psz_value = getenv(sz_name.as_ptr());
if !psz_value.is_null() {
setting.source = SettingSource::User;
setting.value = strcmp(psz_value, b"1\0".as_ptr() as *const c_char) == 0
|| strcasecmp(psz_value, b"true\0".as_ptr() as *const c_char) == 0;
}
setting
}
/// Get a setting boolean
pub unsafe fn setting_get_bool(sz_setting: *const c_char, def: bool) -> SettingBool {
let mut setting = _get_bool_config(sz_setting);
if setting.source == SettingSource::Default {
setting = _get_bool_env(sz_setting);
}
if setting.source == SettingSource::Default {
setting.value = def;
}
setting
}
+375
View File
@@ -0,0 +1,375 @@
//! PIV cryptographic keys stored in a YubiKey.
//!
//! Supported algorithms:
//!
//! - **Encryption**: `RSA1024`, `RSA2048`, `ECCP256`, `ECCP384`
//! - **Signatures**:
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
//! - ECDSA: `ECCP256`, `ECCP384`
// 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::{
certificate::{self, Certificate},
consts::*,
error::Error,
response::StatusWords,
serialization::*,
settings,
yubikey::YubiKey,
AlgorithmId, ObjectId,
};
use log::{debug, error, warn};
/// Slot identifiers.
/// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html>
// TODO(tarcieri): replace these with enums
pub type SlotId = u8;
/// Get the [`ObjectId`] that corresponds to a given [`SlotId`]
// TODO(tarcieri): factor this into a slot ID enum
pub(crate) fn slot_object(slot: SlotId) -> Result<ObjectId, Error> {
let id = match slot {
YKPIV_KEY_AUTHENTICATION => YKPIV_OBJ_AUTHENTICATION,
YKPIV_KEY_SIGNATURE => YKPIV_OBJ_SIGNATURE,
YKPIV_KEY_KEYMGM => YKPIV_OBJ_KEY_MANAGEMENT,
YKPIV_KEY_CARDAUTH => YKPIV_OBJ_CARD_AUTH,
YKPIV_KEY_ATTESTATION => YKPIV_OBJ_ATTESTATION,
slot if slot >= YKPIV_KEY_RETIRED1 && (slot <= YKPIV_KEY_RETIRED20) => {
YKPIV_OBJ_RETIRED1 + (slot - YKPIV_KEY_RETIRED1) as u32
}
_ => return Err(Error::InvalidObject),
};
Ok(id)
}
/// Personal Identity Verification (PIV) key slots
pub const SLOTS: [u8; 24] = [
YKPIV_KEY_AUTHENTICATION,
YKPIV_KEY_SIGNATURE,
YKPIV_KEY_KEYMGM,
YKPIV_KEY_RETIRED1,
YKPIV_KEY_RETIRED2,
YKPIV_KEY_RETIRED3,
YKPIV_KEY_RETIRED4,
YKPIV_KEY_RETIRED5,
YKPIV_KEY_RETIRED6,
YKPIV_KEY_RETIRED7,
YKPIV_KEY_RETIRED8,
YKPIV_KEY_RETIRED9,
YKPIV_KEY_RETIRED10,
YKPIV_KEY_RETIRED11,
YKPIV_KEY_RETIRED12,
YKPIV_KEY_RETIRED13,
YKPIV_KEY_RETIRED14,
YKPIV_KEY_RETIRED15,
YKPIV_KEY_RETIRED16,
YKPIV_KEY_RETIRED17,
YKPIV_KEY_RETIRED18,
YKPIV_KEY_RETIRED19,
YKPIV_KEY_RETIRED20,
YKPIV_KEY_CARDAUTH,
];
/// PIV cryptographic keys stored in a YubiKey
#[derive(Clone, Debug)]
pub struct Key {
/// Card slot
slot: SlotId,
/// Cert
cert: Certificate,
}
impl Key {
/// List Personal Identity Verification (PIV) keys stored in a YubiKey
pub fn list(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
let mut keys = vec![];
let txn = yubikey.begin_transaction()?;
for slot in SLOTS.iter().cloned() {
let buf = match certificate::read_certificate(&txn, slot) {
Ok(b) => b,
Err(e) => {
debug!("error reading certificate in slot {}: {}", slot, e);
continue;
}
};
let cert = Certificate::new(buf)?;
keys.push(Key { slot, cert });
}
Ok(keys)
}
/// Get the slot ID for this key
pub fn slot(&self) -> SlotId {
self.slot
}
/// Get the certificate for this key
pub fn certificate(&self) -> &Certificate {
&self.cert
}
}
// Keygen messages
// TODO(tarcieri): extract these into an I18N-handling type?
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
const SZ_ROCA_ALLOW_USER: &str =
"was permitted by an end-user configuration setting, but is not recommended.";
const SZ_ROCA_ALLOW_ADMIN: &str =
"was permitted by an administrator configuration setting, but is not recommended.";
const SZ_ROCA_BLOCK_USER: &str = "was blocked due to an end-user configuration setting.";
const SZ_ROCA_BLOCK_ADMIN: &str = "was blocked due to an administrator configuration setting.";
const SZ_ROCA_DEFAULT: &str = "was permitted by default, but is not recommended. The default behavior will change in a future Yubico release.";
/// Information about a generated key
// TODO(tarcieri): this could use some more work
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GeneratedKey {
/// RSA keys
Rsa {
/// RSA algorithm
algorithm: AlgorithmId,
/// Modulus
modulus: Vec<u8>,
/// Exponent
exp: Vec<u8>,
},
/// ECC keys
Ecc {
/// ECC algorithm
algorithm: AlgorithmId,
/// Public curve point (i.e. public key)
point: Vec<u8>,
},
}
impl GeneratedKey {
/// Get the algorithm
pub fn algorithm(&self) -> AlgorithmId {
*match self {
GeneratedKey::Rsa { algorithm, .. } => algorithm,
GeneratedKey::Ecc { algorithm, .. } => algorithm,
}
}
}
/// Generate key
#[allow(clippy::cognitive_complexity)]
pub fn generate(
yubikey: &mut YubiKey,
slot: SlotId,
algorithm: AlgorithmId,
pin_policy: u8,
touch_policy: u8,
) -> Result<GeneratedKey, Error> {
let mut in_data = [0u8; 11];
let mut templ = [0, YKPIV_INS_GENERATE_ASYMMETRIC, 0, 0];
let setting_roca: settings::BoolValue;
if yubikey.device_model() == DEVTYPE_YK4
&& (algorithm == YKPIV_ALGO_RSA1024 || algorithm == YKPIV_ALGO_RSA2048)
&& yubikey.version.major == 4
&& (yubikey.version.minor < 3 || yubikey.version.minor == 3 && (yubikey.version.patch < 5))
{
setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true);
let psz_msg = match setting_roca.source {
settings::Source::User => {
if setting_roca.value {
SZ_ROCA_ALLOW_USER
} else {
SZ_ROCA_BLOCK_USER
}
}
settings::Source::Admin => {
if setting_roca.value {
SZ_ROCA_ALLOW_ADMIN
} else {
SZ_ROCA_BLOCK_ADMIN
}
}
_ => SZ_ROCA_DEFAULT,
};
warn!(
"YubiKey serial number {} is affected by vulnerability CVE-2017-15361 \
(ROCA) and should be replaced. On-chip key generation {} See \
YSA-2017-01 <https://www.yubico.com/support/security-advisories/ysa-2017-01/> \
for additional information on device replacement and mitigation assistance",
yubikey.serial, psz_msg
);
if !setting_roca.value {
return Err(Error::NotSupported);
}
}
match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 | YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => (),
_ => {
error!("invalid algorithm specified");
return Err(Error::GenericError);
}
}
let txn = yubikey.begin_transaction()?;
templ[3] = slot;
let mut offset = 5;
in_data[..offset].copy_from_slice(&[
0xac,
3, // length sans this 2-byte header
YKPIV_ALGO_TAG,
1,
algorithm,
]);
if in_data[4] == 0 {
error!("unexpected algorithm");
return Err(Error::AlgorithmError);
}
if pin_policy != YKPIV_PINPOLICY_DEFAULT {
in_data[1] += 3;
in_data[offset..(offset + 3)].copy_from_slice(&[YKPIV_PINPOLICY_TAG, 1, pin_policy]);
offset += 3;
}
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
in_data[1] += 3;
in_data[offset..(offset + 3)].copy_from_slice(&[YKPIV_TOUCHPOLICY_TAG, 1, touch_policy]);
}
let response = txn.transfer_data(&templ, &in_data[..offset], 1024)?;
if !response.is_success() {
let err_msg = "failed to generate new key";
match response.status_words() {
StatusWords::IncorrectSlotError => {
error!("{} (incorrect slot)", err_msg);
return Err(Error::KeyError);
}
StatusWords::IncorrectParamError => {
if pin_policy != YKPIV_PINPOLICY_DEFAULT {
error!("{} (pin policy not supported?)", err_msg);
} else if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
error!("{} (touch policy not supported?)", err_msg);
} else {
error!("{} (algorithm not supported?)", err_msg);
}
return Err(Error::AlgorithmError);
}
StatusWords::SecurityStatusError => {
error!("{} (not authenticated)", err_msg);
return Err(Error::AuthenticationError);
}
other => {
error!("{} (error {:x})", err_msg, other.code());
return Err(Error::GenericError);
}
}
}
let data = response.into_buffer();
match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
let mut offset = 5;
let mut len = 0;
if data[offset] != TAG_RSA_MODULUS {
error!("Failed to parse public key structure (modulus)");
return Err(Error::ParseError);
}
offset += 1;
offset += get_length(&data[offset..], &mut len);
let modulus = data[offset..(offset + len)].to_vec();
offset += len;
if data[offset] != TAG_RSA_EXP {
error!("failed to parse public key structure (public exponent)");
return Err(Error::ParseError);
}
offset += 1;
offset += get_length(&data[offset..], &mut len);
let exp = data[offset..(offset + len)].to_vec();
Ok(GeneratedKey::Rsa {
algorithm,
modulus,
exp,
})
}
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
let mut offset = 3;
let len = if algorithm == YKPIV_ALGO_ECCP256 {
CB_ECC_POINTP256
} else {
CB_ECC_POINTP384
};
if data[offset] != TAG_ECC_POINT {
error!("failed to parse public key structure");
return Err(Error::ParseError);
}
offset += 1;
// the curve point should always be determined by the curve
let len_byte = data[offset];
offset += 1;
if len_byte as usize != len {
error!("unexpected length");
return Err(Error::AlgorithmError);
}
let point = data[offset..(offset + len)].to_vec();
Ok(GeneratedKey::Ecc { algorithm, point })
}
_ => {
error!("wrong algorithm");
Err(Error::AlgorithmError)
}
}
}
+107 -24
View File
@@ -1,15 +1,30 @@
//! [YubiKey][1] PIV: [Personal Identity Verification][2] support for
//! [Yubico][3] devices using the Chip Card Interface Device ([CCID][4])
//! protocol.
//! [YubiKey] PIV: [Personal Identity Verification][PIV] support for
//! [Yubico] devices using the Personal Computer/Smart Card ([PC/SC])
//! interface as provided by the [`pcsc` crate].
//!
//! **PIV** is a [NIST][5] standard for both *signing* and *encryption*
//! **PIV** is a [NIST] 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
//! This library natively implements the protocol used to manage and
//! utilize PIV encryption and signing keys which can be generated, imported,
//! and stored on YubiKey devices.
//!
//! Supported algorithms:
//! See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
//! on which devices support PIV and the available functionality.
//!
//! ## Minimum Supported Rust Version
//!
//! Rust 1.39+
//!
//! ## Supported YubiKeys
//!
//! - [YubiKey NEO] series
//! - [YubiKey 4] series
//! - [YubiKey 5] series
//!
//! NOTE: Nano and USB-C variants of the above are also supported
//!
//! ## Supported Algorithms
//!
//! - **Authentication**: `3DES`
//! - **Encryption**: `RSA1024`, `RSA2048`, `ECCP256`, `ECCP384`
@@ -17,30 +32,64 @@
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
//! - ECDSA: `ECCP256`, `ECCP384`
//!
//! NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
//!
//! ## Status
//!
//! This library is a work-in-progress translation and is not yet usable.
//! Check back later for updates.
//! This is a work-in-progress effort, and while much of the library-level
//! code from upstream [yubico-piv-tool] has been translated into Rust
//! presenting a safe interface, much of it is still untested.
//!
//! Please see the [project's README.md for a complete status][status].
//!
//! ## History
//!
//! This library is a Rust translation of the [yubico-piv-tool][6] utility by
//! This library is a Rust translation of the [yubico-piv-tool] utility by
//! Yubico, which was originally written in C. It was mechanically translated
//! from C into Rust using [Corrode][7], and then subsequently heavily
//! from C into Rust using [Corrode], and then subsequently heavily
//! refactored into safer, more idiomatic Rust.
//!
//! For more information on `yubico-piv-tool` and background information on how
//! 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][8].
//! [Yubico PIV Tool Command Line Guide][piv-tool-guide].
//!
//! [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://github.com/Yubico/yubico-piv-tool/
//! [7]: https://github.com/jameysharp/corrode
//! [8]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
//! ## 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!
//!
//! ## 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
//!
//! **yubikey-piv.rs** is a fork of and originally a mechanical translation from
//! Yubico's [yubico-piv-tool], a C library/CLI program. The original library
//! was licensed under a [2-Clause BSD License][BSDL], which this library inherits
//! as a derived work.
//!
//! [YubiKey]: https://www.yubico.com/products/yubikey-hardware/
//! [PIV]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf
//! [Yubico]: https://www.yubico.com/
//! [PC/SC]: https://en.wikipedia.org/wiki/PC/SC
//! [`pcsc` crate]: https://github.com/bluetech/pcsc-rust
//! [NIST]: https://www.nist.gov/
//! [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
//! [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/
//! [status]: https://github.com/tarcieri/yubikey-piv.rs#status
//! [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
//! [Corrode]: https://github.com/jameysharp/corrode
//! [piv-tool-guide]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
//! [cc-web]: https://contributor-covenant.org/
//! [cc-md]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
//! [BSDL]: https://opensource.org/licenses/BSD-2-Clause
// Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/>
@@ -74,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.1"
html_root_url = "https://docs.rs/yubikey-piv/0.0.2"
)]
#![warn(
missing_docs,
@@ -86,10 +135,44 @@
)]
mod apdu;
#[cfg(feature = "untested")]
pub mod cccid;
#[cfg(feature = "untested")]
pub mod certificate;
#[cfg(feature = "untested")]
pub mod chuid;
#[cfg(feature = "untested")]
pub mod config;
pub mod consts;
#[cfg(feature = "untested")]
pub mod container;
pub mod error;
mod internal;
pub mod util;
#[cfg(feature = "untested")]
pub mod key;
#[cfg(feature = "untested")]
mod metadata;
#[cfg(feature = "untested")]
pub mod mgm;
#[cfg(feature = "untested")]
pub mod msroots;
mod response;
#[cfg(feature = "untested")]
mod serialization;
#[cfg(feature = "untested")]
pub mod settings;
mod transaction;
pub mod yubikey;
pub use self::yubikey::YubiKey;
#[cfg(feature = "untested")]
pub use self::{key::Key, mgm::MgmKey};
pub use yubikey::YubiKey;
/// Algorithm identifiers
// TODO(tarcieri): make this an enum
pub type AlgorithmId = u8;
/// Object identifiers
pub type ObjectId = u32;
/// Buffer type (self-zeroizing byte vector)
pub(crate) type Buffer = zeroize::Zeroizing<Vec<u8>>;
+271
View File
@@ -0,0 +1,271 @@
//! YubiKey Device Metadata
// 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::{consts::*, error::Error, serialization::*, transaction::Transaction, Buffer};
use std::{ptr, slice};
use zeroize::Zeroizing;
/// Get metadata item
pub(crate) fn get_item(data: &[u8], tag: u8) -> Result<&[u8], Error> {
let mut p_temp: *const u8 = data.as_ptr();
let mut cb_temp: usize = 0;
let mut tag_temp: u8;
unsafe {
while p_temp < data.as_ptr().add(data.len()) {
tag_temp = *p_temp;
p_temp = p_temp.add(1);
let p_slice = slice::from_raw_parts(
p_temp,
data.as_ptr() as usize + data.len() - p_temp as usize,
);
if !has_valid_length(
p_slice,
data.as_ptr().add(data.len()) as usize - p_temp as usize,
) {
return Err(Error::SizeError);
}
p_temp = p_temp.add(get_length(p_slice, &mut cb_temp));
if tag_temp == tag {
// found tag
break;
}
p_temp = p_temp.add(cb_temp);
}
if p_temp < data.as_ptr().add(data.len()) {
return Ok(slice::from_raw_parts(p_temp, cb_temp));
}
}
Err(Error::GenericError)
}
/// Set metadata item
pub(crate) fn set_item(
data: &mut [u8],
pcb_data: &mut usize,
cb_data_max: usize,
tag: u8,
p_item: &[u8],
) -> Result<(), Error> {
let mut p_temp: *mut u8 = data.as_mut_ptr();
let mut cb_temp: usize = 0;
let mut tag_temp: u8 = 0;
let mut cb_len: usize = 0;
let cb_item = p_item.len();
// Must be signed to have negative offsets
let cb_moved: isize;
let p_next: *mut u8;
while p_temp < data[*pcb_data..].as_mut_ptr() {
unsafe {
tag_temp = *p_temp;
p_temp = p_temp.add(1);
cb_len = get_length(
slice::from_raw_parts(
p_temp,
data.as_mut_ptr() as usize + data.len() - p_temp as usize,
),
&mut cb_temp,
);
p_temp = p_temp.add(cb_len);
if tag_temp == tag {
break;
}
p_temp = p_temp.add(cb_temp);
}
}
if tag_temp != tag {
if cb_item == 0 {
// We've been asked to delete an existing item that isn't in the blob
return Ok(());
}
unsafe {
p_temp = data.as_mut_ptr().add(*pcb_data);
cb_len = get_length_size(cb_item);
if (*pcb_data + cb_len + cb_item) > cb_data_max {
return Err(Error::GenericError);
}
*p_temp = tag;
p_temp = p_temp.add(1);
p_temp = p_temp.add(set_length(
slice::from_raw_parts_mut(
p_temp,
data.as_ptr() as usize + data.len() - p_temp as usize,
),
cb_item,
));
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
}
*pcb_data += 1 + cb_len + cb_item;
return Ok(());
}
if cb_temp == cb_item {
unsafe {
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
}
return Ok(());
}
p_next = unsafe { p_temp.add(cb_temp) };
cb_moved = (cb_item as isize - cb_temp as isize)
+ if cb_item != 0 {
get_length_size(cb_item) as isize
} else {
-1
}
- cb_len as isize;
if (*pcb_data + cb_moved as usize) > cb_data_max {
return Err(Error::GenericError);
}
unsafe {
ptr::copy(
p_next,
p_next.offset(cb_moved),
*pcb_data - p_next as usize - data.as_ptr() as usize,
);
}
*pcb_data += cb_moved as usize;
if cb_item != 0 {
unsafe {
p_temp = p_temp.offset(-(cb_len as isize));
p_temp = p_temp.add(set_length(
slice::from_raw_parts_mut(
p_temp,
data.as_ptr() as usize + data.len() - p_temp as usize,
),
cb_item,
));
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
}
}
Ok(())
}
/// Read metadata
pub(crate) fn read(txn: &Transaction<'_>, tag: u8) -> Result<Buffer, Error> {
let obj_id = match tag {
TAG_ADMIN => YKPIV_OBJ_ADMIN_DATA,
TAG_PROTECTED => YKPIV_OBJ_PRINTED,
_ => return Err(Error::InvalidObject),
};
let mut data = txn.fetch_object(obj_id)?;
if data.len() < CB_OBJ_TAG_MIN {
return Err(Error::GenericError);
}
if tag != data[0] {
return Err(Error::GenericError);
}
let mut pcb_data = 0;
let offset = 1 + get_length(&data[1..], &mut pcb_data);
if pcb_data > data.len() - offset {
return Err(Error::GenericError);
}
unsafe {
ptr::copy(data.as_ptr().add(offset), data.as_mut_ptr(), pcb_data);
}
data.truncate(pcb_data);
Ok(data)
}
/// Write metadata
pub(crate) fn write(
txn: &Transaction<'_>,
tag: u8,
data: &[u8],
max_size: usize,
) -> Result<(), Error> {
let mut buf = Zeroizing::new(vec![0u8; CB_OBJ_MAX]);
if data.len() > max_size - CB_OBJ_TAG_MAX {
return Err(Error::GenericError);
}
let obj_id = match tag {
TAG_ADMIN => YKPIV_OBJ_ADMIN_DATA,
TAG_PROTECTED => YKPIV_OBJ_PRINTED,
_ => return Err(Error::InvalidObject),
};
if data.is_empty() {
// Deleting metadata
return txn.save_object(obj_id, &[]);
}
buf[0] = tag;
let mut offset = set_length(&mut buf[1..], data.len());
buf[offset..(offset + data.len())].copy_from_slice(data);
offset += data.len();
txn.save_object(obj_id, &buf[..offset])
}
/// Get the size of a length tag for the given length
fn get_length_size(length: usize) -> usize {
if length < 0x80 {
1
} else if length < 0xff {
2
} else {
3
}
}
+363
View File
@@ -0,0 +1,363 @@
//! Management Key (MGM) for authenticating to the YubiKey management applet
// 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::{consts::*, error::Error, metadata, yubikey::YubiKey};
use des::{
block_cipher_trait::{generic_array::GenericArray, BlockCipher},
TdesEde3,
};
use getrandom::getrandom;
use hmac::Hmac;
use log::error;
use pbkdf2::pbkdf2;
use sha1::Sha1;
use std::convert::{TryFrom, TryInto};
use zeroize::{Zeroize, Zeroizing};
/// Default MGM key configured on all YubiKeys
const DEFAULT_MGM_KEY: [u8; DES_LEN_3DES] = [
1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8,
];
/// Management Key (MGM) key types (manual/derived/protected)
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum MgmType {
/// Manual
Manual = 0,
/// Derived
Derived = 1,
/// Protected
Protected = 2,
}
/// Management Key (MGM).
///
/// This key is used to authenticate to the management applet running on
/// a YubiKey in order to perform administrative functions.
///
/// The only supported algorithm for MGM keys is 3DES.
#[derive(Clone)]
pub struct MgmKey([u8; DES_LEN_3DES]);
impl MgmKey {
/// Generate a random MGM key
pub fn generate() -> Result<Self, Error> {
let mut key_bytes = [0u8; DES_LEN_3DES];
if getrandom(&mut key_bytes).is_err() {
return Err(Error::RandomnessError);
}
MgmKey::new(key_bytes)
}
/// Create an MGM key from byte slice.
///
/// Returns an error if the slice is the wrong size or the key is weak.
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, Error> {
bytes.as_ref().try_into()
}
/// Create an MGM key from the given byte array.
///
/// Returns an error if the key is weak.
pub fn new(key_bytes: [u8; DES_LEN_3DES]) -> Result<Self, Error> {
if is_weak_key(&key_bytes) {
error!(
"blacklisting key '{:?}' since it's weak (with odd parity)",
&key_bytes
);
return Err(Error::KeyError);
}
Ok(MgmKey(key_bytes))
}
/// Get derived management key (MGM)
pub fn get_derived(yubikey: &mut YubiKey, pin: &[u8]) -> Result<Self, Error> {
let txn = yubikey.begin_transaction()?;
// recover management key
let data = metadata::read(&txn, TAG_ADMIN)?;
let salt = metadata::get_item(&data, TAG_ADMIN_SALT)?;
if salt.len() != CB_ADMIN_SALT {
error!(
"derived MGM salt exists, but is incorrect size: {} (expected {})",
salt.len(),
CB_ADMIN_SALT
);
return Err(Error::GenericError);
}
let mut mgm = [0u8; DES_LEN_3DES];
pbkdf2::<Hmac<Sha1>>(pin, &salt, ITER_MGM_PBKDF2, &mut mgm);
MgmKey::from_bytes(mgm)
}
/// Get protected management key (MGM)
pub fn get_protected(yubikey: &mut YubiKey) -> Result<Self, Error> {
let txn = yubikey.begin_transaction()?;
let data = metadata::read(&txn, TAG_PROTECTED).map_err(|e| {
error!("could not read protected data (err: {:?})", e);
e
})?;
let item = metadata::get_item(&data, TAG_PROTECTED_MGM).map_err(|e| {
error!("could not read protected MGM from metadata (err: {:?})", e);
e
})?;
if item.len() != DES_LEN_3DES {
error!(
"protected data contains MGM, but is the wrong size: {} (expected {})",
item.len(),
DES_LEN_3DES
);
return Err(Error::AuthenticationError);
}
MgmKey::from_bytes(item)
}
/// Set the management key (MGM)
pub fn set(&self, yubikey: &mut YubiKey, touch: Option<u8>) -> Result<(), Error> {
let txn = yubikey.begin_transaction()?;
txn.set_mgm_key(&self, touch)
}
/// Set protected management key (MGM)
pub fn set_protected(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
let mut data = Zeroizing::new(vec![0u8; CB_BUF_MAX]);
let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
txn.set_mgm_key(self, None).map_err(|e| {
// log a warning, since the device mgm key is corrupt or we're in
// a state where we can't set the mgm key
error!("could not set new derived mgm key, err = {}", e);
e
})?;
// after this point, we've set the mgm key, so the function should
// succeed, regardless of being able to set the metadata
// set the new mgm key in protected data
let buffer = match metadata::read(&txn, TAG_PROTECTED) {
Ok(b) => b,
Err(_) => {
// set current metadata blob size to zero, we'll add to the blank blob
Zeroizing::new(vec![])
}
};
let mut cb_data = buffer.len();
data[..cb_data].copy_from_slice(&buffer);
if let Err(e) = metadata::set_item(
data.as_mut_slice(),
&mut cb_data,
CB_OBJ_MAX,
TAG_PROTECTED_MGM,
self.as_ref(),
) {
error!("could not set protected mgm item, err = {:?}", e);
} else {
metadata::write(&txn, TAG_PROTECTED, &data, max_size).map_err(|e| {
error!("could not write protected data, err = {:?}", e);
e
})?;
}
// set the protected mgm flag in admin data
cb_data = data.len();
let mut flags_1 = [0u8; 1];
if let Ok(buffer) = metadata::read(&txn, TAG_ADMIN) {
if let Ok(item) = metadata::get_item(&buffer, TAG_ADMIN_FLAGS_1) {
if item.len() == flags_1.len() {
flags_1.copy_from_slice(item);
} else {
error!(
"admin data flags are an incorrect size: {} (expected {})",
item.len(),
flags_1.len()
);
}
} else {
// flags are not set
error!("admin data exists, but flags are not present");
}
// remove any existing salt
if let Err(e) =
metadata::set_item(&mut data, &mut cb_data, CB_OBJ_MAX, TAG_ADMIN_SALT, &[])
{
error!("could not unset derived mgm salt (err = {})", e)
}
} else {
cb_data = 0;
}
flags_1[0] |= ADMIN_FLAGS_1_PROTECTED_MGM;
if let Err(e) = metadata::set_item(
data.as_mut_slice(),
&mut cb_data,
CB_OBJ_MAX,
TAG_ADMIN_FLAGS_1,
&flags_1,
) {
error!("could not set admin flags item, err = {}", e);
} else if let Err(e) = metadata::write(&txn, TAG_ADMIN, &data[..cb_data], max_size) {
error!("could not write admin data, err = {}", e);
}
Ok(())
}
/// Encrypt with 3DES key
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn encrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
let mut output = input.to_owned();
TdesEde3::new(GenericArray::from_slice(&self.0))
.encrypt_block(GenericArray::from_mut_slice(&mut output));
output
}
/// Decrypt with 3DES key
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn decrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
let mut output = input.to_owned();
TdesEde3::new(GenericArray::from_slice(&self.0))
.encrypt_block(GenericArray::from_mut_slice(&mut output));
output
}
}
impl AsRef<[u8; DES_LEN_3DES]> for MgmKey {
fn as_ref(&self) -> &[u8; DES_LEN_3DES] {
&self.0
}
}
impl Default for MgmKey {
fn default() -> Self {
MgmKey(DEFAULT_MGM_KEY)
}
}
impl Drop for MgmKey {
fn drop(&mut self) {
self.0.zeroize();
}
}
impl<'a> TryFrom<&'a [u8]> for MgmKey {
type Error = Error;
fn try_from(key_bytes: &'a [u8]) -> Result<Self, Error> {
Self::new(key_bytes.try_into().map_err(|_| Error::SizeError)?)
}
}
/// Weak and semi weak DES keys as taken from:
/// %A D.W. Davies
/// %A W.L. Price
/// %T Security for Computer Networks
/// %I John Wiley & Sons
/// %D 1984
const WEAK_DES_KEYS: &[[u8; DES_LEN_DES]] = &[
// weak keys
[0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01],
[0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE],
[0x1F, 0x1F, 0x1F, 0x1F, 0x0E, 0x0E, 0x0E, 0x0E],
[0xE0, 0xE0, 0xE0, 0xE0, 0xF1, 0xF1, 0xF1, 0xF1],
// semi-weak keys
[0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE],
[0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01, 0xFE, 0x01],
[0x1F, 0xE0, 0x1F, 0xE0, 0x0E, 0xF1, 0x0E, 0xF1],
[0xE0, 0x1F, 0xE0, 0x1F, 0xF1, 0x0E, 0xF1, 0x0E],
[0x01, 0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1],
[0xE0, 0x01, 0xE0, 0x01, 0xF1, 0x01, 0xF1, 0x01],
[0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E, 0xFE],
[0xFE, 0x1F, 0xFE, 0x1F, 0xFE, 0x0E, 0xFE, 0x0E],
[0x01, 0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E],
[0x1F, 0x01, 0x1F, 0x01, 0x0E, 0x01, 0x0E, 0x01],
[0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1, 0xFE],
[0xFE, 0xE0, 0xFE, 0xE0, 0xFE, 0xF1, 0xFE, 0xF1],
];
/// Is this 3DES key weak?
///
/// This check is performed automatically when the key is instantiated to
/// ensure no such keys are used.
fn is_weak_key(key: &[u8; DES_LEN_3DES]) -> bool {
// set odd parity of key
let mut tmp = Zeroizing::new([0u8; DES_LEN_3DES]);
for i in 0..DES_LEN_3DES {
// count number of set bits in byte, excluding the low-order bit - SWAR method
let mut c = key[i] & 0xFE;
c = (c & 0x55) + ((c >> 1) & 0x55);
c = (c & 0x33) + ((c >> 2) & 0x33);
c = (c & 0x0F) + ((c >> 4) & 0x0F);
// if count is even, set low key bit to 1, otherwise 0
tmp[i] = (key[i] & 0xFE) | (if c & 0x01 == 0x01 { 0x00 } else { 0x01 });
}
// check odd parity key against table by DES key block
let mut is_weak = false;
for weak_key in WEAK_DES_KEYS.iter() {
if weak_key == &tmp[0..DES_LEN_DES]
|| weak_key == &tmp[DES_LEN_DES..2 * DES_LEN_DES]
|| weak_key == &tmp[2 * DES_LEN_DES..3 * DES_LEN_DES]
{
is_weak = true;
break;
}
}
is_weak
}
+178
View File
@@ -0,0 +1,178 @@
//! `msroots`: PKCS#7 formatted certificate store for enterprise trusted roots.
//!
//! This `msroots` file contains a bag of certificates with empty content and
//! an empty signature, allowing an enterprise root certificate truststore to
//! be written to and read from a YubiKey.
//!
//! For more information, see:
//! <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
// 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::{consts::*, error::Error, serialization::*, yubikey::YubiKey};
use log::error;
use std::{ptr, slice};
/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots
pub struct MsRoots(Vec<u8>);
impl MsRoots {
/// Initialize a local certificate struct from the given bytebuffer
pub fn new(msroots: impl AsRef<[u8]>) -> Result<Self, Error> {
Ok(MsRoots(msroots.as_ref().into()))
}
/// Read `msroots` file from YubiKey
pub fn read(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
let mut len: usize = 0;
let mut ptr: *mut u8;
let mut tag: u8;
let mut offset: usize = 0;
let mut results = vec![];
let cb_data = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
// allocate first page
let mut p_data = vec![0u8; cb_data];
for object_id in YKPIV_OBJ_MSROOTS1..YKPIV_OBJ_MSROOTS5 {
let mut buf = txn.fetch_object(object_id)?;
let cb_buf = buf.len();
ptr = buf.as_mut_ptr();
if cb_buf < CB_OBJ_TAG_MIN {
return Ok(results);
}
unsafe {
tag = *ptr;
ptr = ptr.add(1);
}
if (TAG_MSROOTS_MID != tag || YKPIV_OBJ_MSROOTS5 == object_id)
&& (TAG_MSROOTS_END != tag)
{
// the current object doesn't contain a valid part of a msroots file
// treat condition as object isn't found
return Ok(results);
}
unsafe {
ptr = ptr.add(get_length(
slice::from_raw_parts(ptr, buf.as_ptr() as usize + buf.len() - ptr as usize),
&mut len,
));
}
// check that decoded length represents object contents
if len > cb_buf - (ptr as isize - buf.as_mut_ptr() as isize) as usize {
return Ok(results);
}
unsafe {
ptr::copy(ptr, p_data.as_mut_ptr().add(offset), len);
}
offset += len;
match MsRoots::new(&p_data[..offset]) {
Ok(msroots) => results.push(msroots),
Err(res) => error!("error parsing msroots: {:?}", res),
}
if tag == TAG_MSROOTS_END {
break;
}
}
Ok(results)
}
/// Write `msroots` file to YubiKey
pub fn write(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
let mut buf = [0u8; CB_OBJ_MAX];
let mut offset: usize;
let mut data_offset: usize = 0;
let mut data_chunk: usize;
let data = &self.0;
let data_len = data.len();
let n_objs: usize;
let cb_obj_max = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?;
if data_len == 0 {
return txn.save_object(YKPIV_OBJ_MSROOTS1, &[]);
}
// Calculate number of objects required to store blob
n_objs = (data_len / (cb_obj_max - CB_OBJ_TAG_MAX)) + 1;
if n_objs > 5 {
return Err(Error::SizeError);
}
for i in 0..n_objs {
offset = 0;
data_chunk = if cb_obj_max - CB_OBJ_TAG_MAX < data_len - data_offset {
cb_obj_max - CB_OBJ_TAG_MAX
} else {
data_len - data_offset
};
buf[offset] = if i == n_objs - 1 {
TAG_MSROOTS_END
} else {
TAG_MSROOTS_MID
};
offset += 1;
offset += set_length(&mut buf[offset..], data_chunk);
buf[offset..].copy_from_slice(&data[data_offset..(data_offset + data_chunk)]);
offset += data_chunk;
txn.save_object(YKPIV_OBJ_MSROOTS1 + i as u32, &buf[..offset])?;
data_offset += data_chunk;
}
Ok(())
}
}
impl AsRef<[u8]> for MsRoots {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
+186
View File
@@ -0,0 +1,186 @@
//! Responses to issued commands
// 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::Buffer;
/// Parsed response to a command
pub(crate) struct Response {
/// Status words
status_words: StatusWords,
/// Buffer
buffer: Buffer,
}
impl Response {
/// Parse a response from the given buffer
pub fn from_bytes(mut buffer: Buffer) -> Self {
if buffer.len() >= 2 {
let sw = StatusWords::from(
(buffer[buffer.len() - 2] as u32) << 8 | (buffer[buffer.len() - 1] as u32),
);
let len = buffer.len() - 2;
buffer.truncate(len);
Response {
status_words: sw,
buffer,
}
} else {
Response {
status_words: StatusWords::None,
buffer,
}
}
}
/// Create a new response from the given status words and buffer
#[cfg(feature = "untested")]
pub fn new(status_words: StatusWords, buffer: Buffer) -> Response {
Response {
status_words,
buffer,
}
}
/// Get the [`StatusWords`] for this response.
pub fn status_words(&self) -> StatusWords {
self.status_words
}
/// Get the raw [`StatusWords`] code for this response.
#[cfg(feature = "untested")]
pub fn code(&self) -> u32 {
self.status_words.code()
}
/// Do the status words for this response indicate success?
pub fn is_success(&self) -> bool {
self.status_words.is_success()
}
/// Borrow the response buffer
pub fn buffer(&self) -> &[u8] {
self.buffer.as_ref()
}
/// Consume this response, returning its buffer
#[cfg(feature = "untested")]
pub fn into_buffer(self) -> Buffer {
self.buffer
}
}
impl AsRef<[u8]> for Response {
fn as_ref(&self) -> &[u8] {
self.buffer()
}
}
/// Status Words (SW) are 2-byte values returned by a card command.
///
/// The first byte of a status word is referred to as SW1 and the second byte
/// of a status word is referred to as SW2.
///
/// See NIST special publication 800-73-4, section 5.6:
/// <https://csrc.nist.gov/publications/detail/sp/800-73/4/final>
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum StatusWords {
/// No status words present in response
None,
/// Successful execution
Success,
/// Security status not satisfied
SecurityStatusError,
/// Authentication method blocked
AuthBlockedError,
/// Incorrect parameter in command data field
IncorrectParamError,
//
// Custom Yubico Status Word extensions
//
/// Incorrect card slot error
IncorrectSlotError,
/// Not supported error
NotSupportedError,
/// Other/unrecognized status words
Other(u32),
}
impl StatusWords {
/// Get the numerical response code for these status words
pub fn code(self) -> u32 {
match self {
StatusWords::None => 0,
StatusWords::SecurityStatusError => 0x6982,
StatusWords::AuthBlockedError => 0x6983,
StatusWords::IncorrectParamError => 0x6a80,
StatusWords::IncorrectSlotError => 0x6b00,
StatusWords::NotSupportedError => 0x6d00,
StatusWords::Success => 0x9000,
StatusWords::Other(n) => n,
}
}
/// Do these status words indicate success?
pub fn is_success(self) -> bool {
self == StatusWords::Success
}
}
impl From<u32> for StatusWords {
fn from(sw: u32) -> Self {
match sw {
0x0000 => StatusWords::None,
0x6982 => StatusWords::SecurityStatusError,
0x6983 => StatusWords::AuthBlockedError,
0x6a80 => StatusWords::IncorrectParamError,
0x6b00 => StatusWords::IncorrectSlotError,
0x6d00 => StatusWords::NotSupportedError,
0x9000 => StatusWords::Success,
_ => StatusWords::Other(sw),
}
}
}
impl From<StatusWords> for u32 {
fn from(sw: StatusWords) -> u32 {
sw.code()
}
}
+99
View File
@@ -0,0 +1,99 @@
//! Serialization functions
// 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::{consts::*, ObjectId};
// TODO(tarcieri): refactor these into better serializers/message builders
/// Set length
pub(crate) fn set_length(buffer: &mut [u8], length: usize) -> usize {
if length < 0x80 {
buffer[0] = length as u8;
1
} else if length < 0x100 {
buffer[0] = 0x81;
buffer[1] = length as u8;
2
} else {
buffer[0] = 0x82;
buffer[1] = ((length >> 8) & 0xff) as u8;
buffer[2] = (length & 0xff) as u8;
3
}
}
/// Parse length tag, returning the size of the length tag itself as the
/// returned value, and setting the len parameter to the parsed length.
pub(crate) fn get_length(buffer: &[u8], len: &mut usize) -> usize {
if buffer[0] < 0x81 {
*len = buffer[0] as usize;
1
} else if (buffer[0] & 0x7f) == 1 {
*len = buffer[1] as usize;
2
} else if (buffer[0] & 0x7f) == 2 {
let tmp = buffer[1] as usize;
*len = (tmp << 8) + buffer[2] as usize;
3
} else {
0
}
}
/// Is length valid?
pub(crate) fn has_valid_length(buffer: &[u8], len: usize) -> bool {
(buffer[0] < 0x81 && len > 0)
|| ((buffer[0] & 0x7f) == 1 && len > 1)
|| ((buffer[0] & 0x7f == 2) && (len > 2))
}
/// Set an object ID header value in the given buffer, returning a mutable
/// slice immediately after the header.
///
/// Panics if the buffer is too small to contain the header.
pub(crate) fn set_object(object_id: ObjectId, mut buffer: &mut [u8]) -> &mut [u8] {
buffer[0] = 0x5c;
if object_id == YKPIV_OBJ_DISCOVERY {
buffer[1] = 1;
buffer[2] = YKPIV_OBJ_DISCOVERY as u8;
buffer = &mut buffer[3..];
} else if object_id > 0xffff && object_id <= 0x00ff_ffff {
buffer[1] = 3;
buffer[2] = ((object_id >> 16) & 0xff) as u8;
buffer[3] = ((object_id >> 8) & 0xff) as u8;
buffer[4] = (object_id & 0xff) as u8;
buffer = &mut buffer[5..];
}
buffer
}
+138
View File
@@ -0,0 +1,138 @@
//! Configuration setting values parsed from the environment and config file:
//! `/etc/yubico/yubikeypiv.conf`
// 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.
/// Default location of the YubiKey PIV configuration file
pub const DEFAULT_CONFIG_FILE: &str = "/etc/yubico/yubikeypiv.conf";
use std::{
env,
fs::File,
io::{BufRead, BufReader},
};
/// Source of how a setting was configured
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Source {
/// User-specified setting
User,
/// Admin-specified setting
Admin,
/// Default setting
Default,
}
/// Setting booleans
#[derive(Copy, Clone, Debug)]
pub struct BoolValue {
/// Boolean value
pub value: bool,
/// Source of the configuration setting (user/admin/default)
pub source: Source,
}
impl BoolValue {
/// Get a [`BoolValue`] value
pub fn get(key: &str, def: bool) -> Self {
let mut setting = get_setting_from_file(key);
if setting.source == Source::Default {
setting = get_setting_from_env(key);
}
if setting.source == Source::Default {
setting.value = def;
}
setting
}
}
/// Get a boolean config value
fn get_setting_from_file(key: &str) -> BoolValue {
let mut setting: BoolValue = BoolValue {
value: false,
source: Source::Default,
};
let file = match File::open(DEFAULT_CONFIG_FILE) {
Ok(f) => f,
Err(_) => return setting,
};
for line in BufReader::new(file).lines() {
let line = match line {
Ok(line) => line,
_ => continue,
};
if line.starts_with('#') || line.starts_with('\r') || line.starts_with('\n') {
continue;
}
let (name, value) = {
let mut parts = line.splitn(1, '=');
let name = parts.next();
let value = parts.next();
match (name, value, parts.next()) {
(Some(name), Some(value), None) => (name.trim(), value.trim()),
_ => continue,
}
};
if name == key {
setting.source = Source::Admin;
setting.value = value == "1" || value == "true";
break;
}
}
setting
}
/// Get a setting boolean from an environment variable
fn get_setting_from_env(key: &str) -> BoolValue {
let mut setting: BoolValue = BoolValue {
value: false,
source: Source::Default,
};
if let Ok(value) = env::var(format!("YUBIKEY_PIV_{}", key)) {
setting.source = Source::User;
setting.value = value == "1" || value == "true";
}
setting
}
+561
View File
@@ -0,0 +1,561 @@
//! YubiKey PC/SC transactions
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*, Buffer};
#[cfg(feature = "untested")]
use crate::{
mgm::MgmKey,
response::{Response, StatusWords},
serialization::*,
ObjectId,
};
use log::{error, trace};
use std::convert::TryInto;
#[cfg(feature = "untested")]
use std::ptr;
use zeroize::Zeroizing;
/// Exclusive transaction with the YubiKey's PC/SC card.
pub(crate) struct Transaction<'tx> {
inner: pcsc::Transaction<'tx>,
}
impl<'tx> Transaction<'tx> {
/// Create a new transaction with the given card
pub fn new(card: &'tx mut pcsc::Card) -> Result<Self, Error> {
Ok(Transaction {
inner: card.transaction()?,
})
}
/// Get an attribute of the card or card reader.
pub fn get_attribute<'buf>(
&self,
attribute: pcsc::Attribute,
buffer: &'buf mut [u8],
) -> Result<&'buf [u8], Error> {
Ok(self.inner.get_attribute(attribute, buffer)?)
}
/// Transmit a single serialized APDU to the card this transaction is open
/// with and receive a response.
///
/// This is a wrapper for the raw `SCardTransmit` function and operates on
/// single APDU messages at a time. For larger messages that need to be
/// split into multiple APDUs, use the [`Transaction::transfer_data`]
/// method instead.
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Buffer, Error> {
trace!(">>> {:?}", send_buffer);
let mut recv_buffer = Zeroizing::new(vec![0u8; recv_len]);
let len = self
.inner
.transmit(send_buffer, recv_buffer.as_mut())?
.len();
recv_buffer.truncate(len);
trace!("<<< {:?}", recv_buffer.as_slice());
Ok(recv_buffer)
}
/// Select application.
pub fn select_application(&self) -> Result<(), Error> {
let response = APDU::new(YKPIV_INS_SELECT_APPLICATION)
.p1(0x04)
.data(&AID)
.transmit(self, 0xFF)
.map_err(|e| {
error!("failed communicating with card: '{}'", e);
e
})?;
if !response.is_success() {
error!(
"failed selecting application: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
Ok(())
}
/// Get the version of the PIV application installed on the YubiKey
pub fn get_version(&self) -> Result<Version, Error> {
// get version from device
let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?;
if !response.is_success() {
return Err(Error::GenericError);
}
if response.buffer().len() < 3 {
return Err(Error::SizeError);
}
Ok(Version {
major: response.buffer()[0],
minor: response.buffer()[1],
patch: response.buffer()[2],
})
}
/// Get YubiKey device serial number
pub fn get_serial(&self, version: Version) -> Result<Serial, Error> {
let yk_applet = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
let response = if version.major < 5 {
// get serial from neo/yk4 devices using the otp applet
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
.p1(0x04)
.data(&yk_applet)
.transmit(self, 0xFF)?
.status_words();
if !sw.is_success() {
error!("failed selecting yk application: {:04x}", sw.code());
return Err(Error::GenericError);
}
let resp = APDU::new(0x01).p1(0x10).transmit(self, 0xFF)?;
if !resp.is_success() {
error!(
"failed retrieving serial number: {:04x}",
resp.status_words().code()
);
return Err(Error::GenericError);
}
// reselect the PIV applet
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
.p1(0x04)
.data(&AID)
.transmit(self, 0xFF)?
.status_words();
if !sw.is_success() {
error!("failed selecting application: {:04x}", sw.code());
return Err(Error::GenericError);
}
resp
} else {
// get serial from yk5 and later devices using the f8 command
let resp = APDU::new(YKPIV_INS_GET_SERIAL).transmit(self, 0xFF)?;
if !resp.is_success() {
error!(
"failed retrieving serial number: {:04x}",
resp.status_words().code()
);
return Err(Error::GenericError);
}
resp
};
response.buffer()[..4]
.try_into()
.map(|serial| Serial::from(u32::from_be_bytes(serial)))
.map_err(|_| Error::SizeError)
}
/// Verify device PIN.
#[cfg(feature = "untested")]
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> {
// TODO(tarcieri): allow unpadded (with `0xFF`) PIN shorter than CB_PIN_MAX?
if pin.len() != CB_PIN_MAX {
return Err(Error::SizeError);
}
let response = APDU::new(YKPIV_INS_VERIFY)
.params(0x00, 0x80)
.data(pin)
.transmit(self, 261)?;
match response.status_words() {
StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }),
_ => Err(Error::GenericError),
}
}
/// Change the PIN
#[cfg(feature = "untested")]
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
let mut templ = [0, YKPIV_INS_CHANGE_REFERENCE, 0, 0x80];
let mut indata = Zeroizing::new([0u8; 16]);
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
return Err(Error::SizeError);
}
if action == CHREF_ACT_UNBLOCK_PIN {
templ[1] = YKPIV_INS_RESET_RETRY;
} else if action == CHREF_ACT_CHANGE_PUK {
templ[3] = 0x81;
}
unsafe {
ptr::copy(current_pin.as_ptr(), indata.as_mut_ptr(), current_pin.len());
if current_pin.len() < CB_PIN_MAX {
ptr::write_bytes(
indata.as_mut_ptr().add(current_pin.len()),
0xff,
CB_PIN_MAX - current_pin.len(),
);
}
ptr::copy(
new_pin.as_ptr(),
indata.as_mut_ptr().offset(8),
new_pin.len(),
);
if new_pin.len() < CB_PIN_MAX {
ptr::write_bytes(
indata.as_mut_ptr().offset(8).add(new_pin.len()),
0xff,
CB_PIN_MAX - new_pin.len(),
);
}
}
let status_words = self
.transfer_data(&templ, indata.as_ref(), 0xFF)?
.status_words();
match status_words {
StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::PinLocked),
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }),
_ => {
error!(
"failed changing pin, token response code: {:x}.",
status_words.code()
);
Err(Error::GenericError)
}
}
}
/// Set the management key (MGM).
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, touch: Option<u8>) -> Result<(), Error> {
let p2 = match touch.unwrap_or_default() {
0 => 0xff,
1 => 0xfe,
_ => {
return Err(Error::GenericError);
}
};
let mut data = [0u8; DES_LEN_3DES + 3];
data[0] = YKPIV_ALGO_3DES;
data[1] = YKPIV_KEY_CARDMGM;
data[2] = DES_LEN_3DES as u8;
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
let status_words = APDU::new(YKPIV_INS_SET_MGMKEY)
.params(0xff, p2)
.data(&data)
.transmit(self, 261)?
.status_words();
if !status_words.is_success() {
return Err(Error::GenericError);
}
Ok(())
}
/// Perform a YubiKey operation which requires authentication.
///
/// This is the common backend for all public key encryption and signing
/// operations.
// TODO(tarcieri): refactor this to be less gross/coupled.
#[cfg(feature = "untested")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn authenticated_command(
&self,
sign_in: &[u8],
out: &mut [u8],
out_len: &mut usize,
algorithm: u8,
key: u8,
decipher: bool,
) -> Result<(), Error> {
let in_len = sign_in.len();
let mut indata = [0u8; 1024];
let templ = [0, YKPIV_INS_AUTHENTICATE, algorithm, key];
let mut len: usize = 0;
match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
let key_len = if algorithm == YKPIV_ALGO_RSA1024 {
128
} else {
256
};
if in_len != key_len {
return Err(Error::SizeError);
}
}
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
let key_len = if algorithm == YKPIV_ALGO_ECCP256 {
32
} else {
48
};
if (!decipher && (in_len > key_len)) || (decipher && (in_len != (key_len * 2) + 1))
{
return Err(Error::SizeError);
}
}
_ => return Err(Error::AlgorithmError),
}
let bytes = if in_len < 0x80 {
1
} else if in_len < 0xff {
2
} else {
3
};
indata[0] = 0x7c;
let mut offset = 1 + set_length(&mut indata[1..], in_len + bytes + 3);
indata[offset] = 0x82;
indata[offset + 1] = 0x00;
indata[offset + 2] =
if (algorithm == YKPIV_ALGO_ECCP256 || algorithm == YKPIV_ALGO_ECCP384) && decipher {
0x85
} else {
0x81
};
offset += 3;
offset += set_length(&mut indata[offset..], in_len);
indata[offset..(offset + in_len)].copy_from_slice(sign_in);
offset += in_len;
let response = self
.transfer_data(&templ, &indata[..offset], 1024)
.map_err(|e| {
error!("sign command failed to communicate: {}", e);
e
})?;
if !response.is_success() {
error!("failed sign command with code {:x}", response.code());
if response.status_words() == StatusWords::SecurityStatusError {
return Err(Error::AuthenticationError);
} else {
return Err(Error::GenericError);
}
}
let data = response.buffer();
// skip the first 7c tag
if data[0] != 0x7c {
error!("failed parsing signature reply (0x7c byte)");
return Err(Error::ParseError);
}
let mut offset = 1 + get_length(&data[1..], &mut len);
// skip the 82 tag
if data[offset] != 0x82 {
error!("failed parsing signature reply (0x82 byte)");
return Err(Error::ParseError);
}
offset += 1;
offset += get_length(&data[offset..], &mut len);
if len > *out_len {
error!("wrong size on output buffer");
return Err(Error::SizeError);
}
*out_len = len;
out[..len].copy_from_slice(&data[offset..(offset + len)]);
Ok(())
}
/// Send/receive large amounts of data to/from the YubiKey, splitting long
/// messages into smaller APDU-sized messages (using the provided APDU
/// template to construct them), and then sending those via
/// [`Transaction::transmit`].
#[cfg(feature = "untested")]
pub fn transfer_data(
&self,
templ: &[u8],
in_data: &[u8],
max_out: usize,
) -> Result<Response, Error> {
let mut in_offset = 0;
let mut out_data = Zeroizing::new(vec![]);
let mut sw = 0;
loop {
let mut this_size = 0xff;
let cla = if in_offset + 0xff < in_data.len() {
0x10
} else {
this_size = in_data.len() - in_offset;
templ[0]
};
trace!("going to send {} bytes in this go", this_size);
let response = APDU::new(templ[1])
.cla(cla)
.params(templ[2], templ[3])
.data(&in_data[in_offset..(in_offset + this_size)])
.transmit(self, 261)?;
if !response.is_success() && (response.status_words().code() >> 8 != 0x61) {
// TODO(tarcieri): is this really OK?
return Ok(Response::new(sw.into(), out_data));
}
sw = response.status_words().code();
if out_data.len() - response.buffer().len() - 2 > max_out {
error!(
"output buffer too small: wanted to write {}, max was {}",
out_data.len() - response.buffer().len() - 2,
max_out
);
return Err(Error::SizeError);
}
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]);
in_offset += this_size;
if in_offset >= in_data.len() {
break;
}
}
while sw >> 8 == 0x61 {
trace!(
"The card indicates there is {} bytes more data for us",
sw & 0xff
);
let response = APDU::new(YKPIV_INS_GET_RESPONSE_APDU).transmit(self, 261)?;
sw = response.status_words().code();
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
return Ok(Response::new(sw.into(), Zeroizing::new(vec![])));
}
if out_data.len() + response.buffer().len() - 2 > max_out {
error!(
"output buffer too small: wanted to write {}, max was {}",
out_data.len() + response.buffer().len() - 2,
max_out
);
return Err(Error::SizeError);
}
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]);
}
Ok(Response::new(sw.into(), out_data))
}
/// Fetch an object
#[cfg(feature = "untested")]
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
let mut indata = [0u8; 5];
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff];
let mut inlen = indata.len();
let indata_remaining = set_object(object_id, &mut indata);
inlen -= indata_remaining.len();
let response = self.transfer_data(&templ, &indata[..inlen], CB_BUF_MAX)?;
if !response.is_success() {
return Err(Error::GenericError);
}
let data = response.into_buffer();
let mut outlen = 0;
if data.len() < 2 || !has_valid_length(&data[1..], data.len() - 1) {
return Err(Error::SizeError);
}
let offs = get_length(&data[1..], &mut outlen);
if offs == 0 {
return Err(Error::SizeError);
}
if outlen + offs + 1 != data.len() {
error!(
"invalid length indicated in object: total len is {} but indicated length is {}",
data.len(),
outlen
);
return Err(Error::SizeError);
}
Ok(Zeroizing::new(
data[(1 + offs)..(1 + offs + outlen)].to_vec(),
))
}
/// Save an object
#[cfg(feature = "untested")]
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff];
// TODO(tarcieri): replace with vector
let mut data = [0u8; CB_BUF_MAX];
if indata.len() > CB_OBJ_MAX {
return Err(Error::SizeError);
}
let mut len = data.len();
let mut data_remaining = set_object(object_id, &mut data);
data_remaining[0] = 0x53;
data_remaining = &mut data_remaining[1..];
let offset = set_length(data_remaining, indata.len());
data_remaining = &mut data_remaining[offset..];
data_remaining[..indata.len()].copy_from_slice(indata);
data_remaining = &mut data_remaining[indata.len()..];
len -= data_remaining.len();
let status_words = self
.transfer_data(&templ, &data[..len], 255)?
.status_words();
match status_words {
StatusWords::Success => Ok(()),
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
_ => Err(Error::GenericError),
}
}
}
-2354
View File
File diff suppressed because it is too large Load Diff
+571 -2237
View File
File diff suppressed because it is too large Load Diff
-46
View File
@@ -1,46 +0,0 @@
# 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.
AM_CFLAGS = $(WARN_CFLAGS) @CHECK_CFLAGS@ $(OPENSSL_CFLAGS)
AM_CPPFLAGS = -I$(top_srcdir)/lib -I$(top_builddir)/lib $(OPENSSL_CFLAGS) $(PCSC_CFLAGS)
AM_LDFLAGS = @CHECK_LIBS@
if COMPILER_CLANG
AM_LDFLAGS += -no-fast-install
else
AM_LDFLAGS += -no-install
endif
LDADD = ../libykpiv.la $(OPENSSL_LIBS)
api_LDADD = ../../tool/libpiv_util.la
api_SOURCES = api.c
check_PROGRAMS = basic parse_key api
TESTS = $(check_PROGRAMS)
LOG_COMPILER = $(VALGRIND)
-993
View File
@@ -1,993 +0,0 @@
/*
* 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.
*
*/
#include "ykpiv.h"
#include "internal.h"
#include "../../tool/openssl-compat.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <check.h>
#ifdef __MINGW32__
#define dprintf(fd, ...) fprintf(stdout, __VA_ARGS__)
#endif
int destruction_confirmed(void);
// only defined in libcheck 0.11+ (linux distros still shipping 0.10)
#ifndef ck_assert_ptr_nonnull
#define ck_assert_ptr_nonnull(a) ck_assert((a) != NULL)
#endif
#ifndef ck_assert_mem_eq
#define ck_assert_mem_eq(a,b,n) ck_assert(memcmp((a), (b), (n)) == 0)
#endif
// only defined in libcheck 0.10+ (RHEL7 is still shipping 0.9)
#ifndef ck_assert_ptr_eq
#define ck_assert_ptr_eq(a,b) ck_assert((void *)(a) == (void *)(b))
#endif
ykpiv_state *g_state;
const uint8_t g_cert[] = {
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
};
void setup(void) {
ykpiv_rc res;
// Require user confirmation to continue, since this test suite will clear
// any data stored on connected keys.
if (!destruction_confirmed())
exit(77); // exit code 77 == skipped tests
res = ykpiv_init(&g_state, true);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(g_state);
res = ykpiv_connect(g_state, NULL);
ck_assert_int_eq(res, YKPIV_OK);
}
void teardown(void) {
ykpiv_rc res;
// This is the expected case, if the allocator test ran, since it de-inits.
if (NULL == g_state)
return;
res = ykpiv_disconnect(g_state);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_done(g_state);
ck_assert_int_eq(res, YKPIV_OK);
}
#ifdef HW_TESTS
START_TEST(test_devicemodel) {
ykpiv_rc res;
ykpiv_devmodel model;
char version[256];
char reader_buf[2048];
size_t num_readers = sizeof(reader_buf);
res = ykpiv_get_version(g_state, version, sizeof(version));
ck_assert_int_eq(res, YKPIV_OK);
fprintf(stderr, "Version: %s\n", version);
model = ykpiv_util_devicemodel(g_state);
fprintf(stdout, "Model: %u\n", model);
ck_assert(model == DEVTYPE_YK4 || model == DEVTYPE_NEOr3);
res = ykpiv_list_readers(g_state, reader_buf, &num_readers);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_gt(num_readers, 0);
if (model == DEVTYPE_YK4) {
ck_assert_ptr_nonnull(strstr(reader_buf, "Yubikey 4"));
ck_assert(version[0] == '4'); // Verify app version 4.x
ck_assert(version[1] == '.');
}
else {
ck_assert_ptr_nonnull(strstr(reader_buf, "Yubikey NEO"));
ck_assert(version[0] == '1'); // Verify app version 1.x
ck_assert(version[1] == '.');
}
}
END_TEST
START_TEST(test_get_set_cardid) {
ykpiv_rc res;
ykpiv_cardid set_id;
ykpiv_cardid get_id;
memset(&set_id.data, 'i', sizeof(set_id.data));
memset(&get_id.data, 0, sizeof(get_id.data));
res = ykpiv_util_set_cardid(g_state, &set_id);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_get_cardid(g_state, &get_id);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_mem_eq(&set_id.data, &get_id.data, sizeof(set_id.data));
}
END_TEST
START_TEST(test_list_readers) {
ykpiv_rc res;
char reader_buf[2048];
size_t num_readers = sizeof(reader_buf);
char *reader_ptr;
res = ykpiv_list_readers(g_state, reader_buf, &num_readers);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_gt(num_readers, 0);
for(reader_ptr = reader_buf; *reader_ptr != '\0'; reader_ptr += strlen(reader_ptr) + 1) {
fprintf(stdout, "Found device: %s\n", reader_ptr);
}
}
END_TEST
START_TEST(test_read_write_list_delete_cert) {
ykpiv_rc res;
uint8_t *read_cert = NULL;
size_t read_cert_len = 0;
{
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(read_cert);
ck_assert_int_eq(read_cert_len, sizeof(g_cert));
ck_assert_mem_eq(g_cert, read_cert, sizeof(g_cert));
res = ykpiv_util_free(g_state, read_cert);
ck_assert_int_eq(res, YKPIV_OK);
}
{
ykpiv_key *keys = NULL;
size_t data_len;
uint8_t key_count;
res = ykpiv_util_list_keys(g_state, &key_count, &keys, &data_len);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(keys);
ck_assert_int_gt(key_count, 0);
res = ykpiv_util_free(g_state, keys);
ck_assert_int_eq(res, YKPIV_OK);
}
{
res = ykpiv_util_delete_cert(g_state, YKPIV_KEY_AUTHENTICATION);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
ck_assert_int_eq(res, YKPIV_GENERIC_ERROR);
res = ykpiv_util_free(g_state, read_cert);
ck_assert_int_eq(res, YKPIV_OK);
}
}
END_TEST
#include <openssl/des.h>
#include <openssl/pem.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
// RSA2048 private key, generated with: `openssl genrsa 2048 -out private.pem`
static const char *private_key_pem =
"-----BEGIN RSA PRIVATE KEY-----\n"
"MIIEpAIBAAKCAQEAwVUwmVbc+ffOy2+RivxBpgleTVN6bUa0q7jNYB+AseFQYaYq\n"
"EGfa+VGdxSGo+8DV1KT9+fNEd5243gXn/tcjtMItKeB+oAQc64s9lIFlYuR8bpq1\n"
"ibr33iW2elnnv9mpecqohdCVwM2McWveoPyb7MwlwVuhqexOzJO29bqJcazLbtkf\n"
"ZETK0oBx53/ylA4Y6nE9Pa46jW2qhj+KShf1iBg+gAyt3eI+wI2Wmub1WxLLH8D2\n"
"w+kow8QhQOa8dHCkRRw771JxVO5+d+Y/Y+x9B1HgF4q0q9xUlhWLK2TR4ChBFzXe\n"
"47sAHsSqi/pl5JbwYrHPOE/VEBLukmjL8NFCSQIDAQABAoIBADmEyOK2DyRnb6Ti\n"
"2qBJEJb/boj+7wuX36S/ZIrWlIlXiXyj3RvoaiOG/rNpokbURknvlIhKsfIMgLW9\n"
"eBo/k6Xxp1IwMjwVPS1uzbFjFfDoHYUijiQd9iSnf7TDDsnrThqoCp9VQViNTt1n\n"
"xGKNBS7cRddTFbPiVEdVIzfUeZPR2oRrc4maBCRCrQgg8WNknawmc8zhkf2NiPj3\n"
"tWLQHMy1/MgW2W1LM9sgzllEtS5CZUnyGy2HbbhS2tbZ6j9kPzOp0pPxxTTzJmmV\n"
"fi1vkJcVW4+MdXjWmhALcPA4dO7Y2Ljiu6VxIxQORRO1DyiCjAs1AVMQxgPAAY41\n"
"YR4Q2EkCgYEA4zE0oytg97aVaBY9CKi7/PqR+NI/uEvfoQCnT+ddaJgp/qsspuXo\n"
"tJt94p13ANd8O7suqQTVNvbZq1rX10xQjJZ9nvlqQa6iHkN6Epq31XBK3Z+acjIV\n"
"A2rAgKBByjz9/CpKHqnOsrTWU1Y7x416IG4BZt42hHdrxRH98/wiDH8CgYEA2djj\n"
"AjwgK+MwDnshwT1NNgCSP/2ZHatBAykZ5BCs9BJ6MNYqqXVGYoqs5Z5kSkow+Db3\n"
"pipkEieo5w2Rd5zkolTThaVCvRkSe5wRiBpZhaeY+b0UFwavGCb6zU/MmJIMDPiI\n"
"2iRGeCXgQDvIS/icIqzbTtp6dZaoMgG7LdSR7TcCgYBtxGhaLas8A8tL7vKuLFgn\n"
"cij0vyBqOr5hW596y54l2t7vXGTGfm5gVIAN7WaB0ZsEgPuaTet2Eu44DDwcmZKR\n"
"WmR3Wqor8eQCGzfvpTEMvqRtT5+fbPMaI4m+m68ttyo/m28UQZbMYPLscM2RLJnE\n"
"8WFcAiD0/33iST8ZksggoQKBgQDE/7Yhsj+hkHxHzB+1QPtOp2uaBHnvc4uCESwB\n"
"qvbMbN0kxrejsJLqz98UcozdBYSNIiAHmvQN2uGJuCJhGXdEORNjGxRkLoUhVPwh\n"
"qTplfC8BQHQncnrqi21oNw6ctg3BuQsAwaccRZwqWiWCVhrT3J8iCr6NEaWeOySK\n"
"iF1CNwKBgQCRpkkZArlccwS0kMvkK+tQ1rG2xWm7c05G34gP/g6dHFRy0gPNMyvi\n"
"SkiLTJmQIEZSAEiq0FFgcVwM6o556ftvQZuwDp5rHUbwqnHCpMJKpD9aJpStvfPi\n"
"4p9JbYdaGqnq4eoNKemmGnbUof0dR9Zr0lGmcMTwwzBib+4E1d7soA==\n"
"-----END RSA PRIVATE KEY-----\n";
// Certificate signed with key above:
// `openssl req -x509 -key private.pem -out cert.pem -subj "/CN=bar/OU=test/O=example.com/" -new`
static const char *certificate_pem =
"-----BEGIN CERTIFICATE-----\n"
"MIIC5zCCAc+gAwIBAgIJAOq8A/cmpxF5MA0GCSqGSIb3DQEBCwUAMDMxDDAKBgNV\n"
"BAMMA2JhcjENMAsGA1UECwwEdGVzdDEUMBIGA1UECgwLZXhhbXBsZS5jb20wHhcN\n"
"MTcwODAzMTE1MDI2WhcNMTgwODAzMTE1MDI2WjAzMQwwCgYDVQQDDANiYXIxDTAL\n"
"BgNVBAsMBHRlc3QxFDASBgNVBAoMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B\n"
"AQEFAAOCAQ8AMIIBCgKCAQEAwVUwmVbc+ffOy2+RivxBpgleTVN6bUa0q7jNYB+A\n"
"seFQYaYqEGfa+VGdxSGo+8DV1KT9+fNEd5243gXn/tcjtMItKeB+oAQc64s9lIFl\n"
"YuR8bpq1ibr33iW2elnnv9mpecqohdCVwM2McWveoPyb7MwlwVuhqexOzJO29bqJ\n"
"cazLbtkfZETK0oBx53/ylA4Y6nE9Pa46jW2qhj+KShf1iBg+gAyt3eI+wI2Wmub1\n"
"WxLLH8D2w+kow8QhQOa8dHCkRRw771JxVO5+d+Y/Y+x9B1HgF4q0q9xUlhWLK2TR\n"
"4ChBFzXe47sAHsSqi/pl5JbwYrHPOE/VEBLukmjL8NFCSQIDAQABMA0GCSqGSIb3\n"
"DQEBCwUAA4IBAQCamrwdEhNmY2GCQWq6U90Q3XQT6w0HHW/JmtuGeF+BTpVr12gN\n"
"/UvEXTo9geWbGcCTjaMMURTa7mUjVUIttIWEVHZMKqBuvsUM1RcuOEX/vitaJJ8K\n"
"Sw4upjCNa3ZxUXmSA1FBixZgDzFqjEeSiaJjMU0yX5W2p1T4iNYtF3YqzMF5AWSI\n"
"qCO7gP5ezPyg5kDnrO3V7DBgnDiqawq7Pyn9DynKNULX/hc1yls/R+ebb2u8Z+h5\n"
"W4YXbzGZb8qdT27qIZaHD638tL6liLkI6UE4KCXH8X8e3fqdbmqvwrq403nOGmsP\n"
"cbJb2PEXibNEQG234riKxm7x7vNDLL79Jwtc\n"
"-----END CERTIFICATE-----\n";
static bool set_component(unsigned char *in_ptr, const BIGNUM *bn, int element_len) {
int real_len = BN_num_bytes(bn);
if(real_len > element_len) {
return false;
}
memset(in_ptr, 0, (size_t)(element_len - real_len));
in_ptr += element_len - real_len;
BN_bn2bin(bn, in_ptr);
return true;
}
static bool prepare_rsa_signature(const unsigned char *in, unsigned int in_len, unsigned char *out, unsigned int *out_len, int nid) {
X509_SIG *digestInfo;
X509_ALGOR *algor;
ASN1_OCTET_STRING *digest;
unsigned char data[1024];
memcpy(data, in, in_len);
digestInfo = X509_SIG_new();
X509_SIG_getm(digestInfo, &algor, &digest);
algor->algorithm = OBJ_nid2obj(nid);
X509_ALGOR_set0(algor, OBJ_nid2obj(nid), V_ASN1_NULL, NULL);
ASN1_STRING_set(digest, data, in_len);
*out_len = (unsigned int)i2d_X509_SIG(digestInfo, &out);
X509_SIG_free(digestInfo);
return true;
}
static void import_key(unsigned char slot, unsigned char pin_policy) {
ykpiv_rc res;
{
unsigned char pp = pin_policy;
unsigned char tp = YKPIV_TOUCHPOLICY_DEFAULT;
EVP_PKEY *private_key = NULL;
BIO *bio = NULL;
RSA *rsa_private_key = NULL;
unsigned char e[4];
unsigned char p[128];
unsigned char q[128];
unsigned char dmp1[128];
unsigned char dmq1[128];
unsigned char iqmp[128];
int element_len = 128;
const BIGNUM *bn_e, *bn_p, *bn_q, *bn_dmp1, *bn_dmq1, *bn_iqmp;
bio = BIO_new_mem_buf(private_key_pem, strlen(private_key_pem));
private_key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
ck_assert_ptr_nonnull(private_key);
BIO_free(bio);
rsa_private_key = EVP_PKEY_get1_RSA(private_key);
ck_assert_ptr_nonnull(rsa_private_key);
RSA_get0_key(rsa_private_key, NULL, &bn_e, NULL);
RSA_get0_factors(rsa_private_key, &bn_p, &bn_q);
RSA_get0_crt_params(rsa_private_key, &bn_dmp1, &bn_dmq1, &bn_iqmp);
ck_assert(set_component(e, bn_e, 3));
ck_assert(set_component(p, bn_p, element_len));
ck_assert(set_component(q, bn_q, element_len));
ck_assert(set_component(dmp1, bn_dmp1, element_len));
ck_assert(set_component(dmq1, bn_dmq1, element_len));
ck_assert(set_component(iqmp, bn_iqmp, element_len));
// Try wrong algorithm, fail.
res = ykpiv_import_private_key(g_state,
slot,
YKPIV_ALGO_RSA1024,
p, element_len,
q, element_len,
dmp1, element_len,
dmq1, element_len,
iqmp, element_len,
NULL, 0,
pp, tp);
ck_assert_int_eq(res, YKPIV_ALGORITHM_ERROR);
// Try right algorithm
res = ykpiv_import_private_key(g_state,
slot,
YKPIV_ALGO_RSA2048,
p, element_len,
q, element_len,
dmp1, element_len,
dmq1, element_len,
iqmp, element_len,
NULL, 0,
pp, tp);
ck_assert_int_eq(res, YKPIV_OK);
RSA_free(rsa_private_key);
EVP_PKEY_free(private_key);
}
// Use imported key to decrypt a thing. See that it works.
{
BIO *bio = NULL;
X509 *cert = NULL;
EVP_PKEY *pub_key = NULL;
unsigned char secret[32];
unsigned char secret2[32];
unsigned char data[256];
int len;
size_t len2 = sizeof(data);
RSA *rsa = NULL;
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
ck_assert_ptr_nonnull(cert);
BIO_free(bio);
pub_key = X509_get_pubkey(cert);
ck_assert_ptr_nonnull(pub_key);
rsa = EVP_PKEY_get1_RSA(pub_key);
ck_assert_ptr_nonnull(rsa);
EVP_PKEY_free(pub_key);
ck_assert_int_gt(RAND_bytes(secret, sizeof(secret)), 0);
len = RSA_public_encrypt(sizeof(secret), secret, data, rsa, RSA_PKCS1_PADDING);
ck_assert_int_ge(len, 0);
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_decipher_data(g_state, data, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, slot);
ck_assert_int_eq(res, YKPIV_OK);
len = RSA_padding_check_PKCS1_type_2(secret2, sizeof(secret2), data + 1, len2 - 1, RSA_size(rsa));
ck_assert_int_eq(len, sizeof(secret));
ck_assert_int_eq(memcmp(secret, secret2, sizeof(secret)), 0);
RSA_free(rsa);
X509_free(cert);
}
}
START_TEST(test_import_key) {
ykpiv_rc res;
import_key(0x9a, YKPIV_PINPOLICY_DEFAULT);
// Verify certificate
{
BIO *bio = NULL;
X509 *cert = NULL;
RSA *rsa = NULL;
EVP_PKEY *pub_key = NULL;
const EVP_MD *md = EVP_sha256();
EVP_MD_CTX *mdctx;
unsigned char signature[1024];
unsigned char encoded[1024];
unsigned char data[1024];
unsigned char signinput[1024];
unsigned char rand[128];
size_t sig_len = sizeof(signature);
size_t padlen = 256;
unsigned int enc_len;
unsigned int data_len;
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
ck_assert_ptr_nonnull(cert);
BIO_free(bio);
pub_key = X509_get_pubkey(cert);
ck_assert_ptr_nonnull(pub_key);
rsa = EVP_PKEY_get1_RSA(pub_key);
ck_assert_ptr_nonnull(rsa);
EVP_PKEY_free(pub_key);
ck_assert_int_gt(RAND_bytes(rand, 128), 0);
mdctx = EVP_MD_CTX_create();
EVP_DigestInit_ex(mdctx, md, NULL);
EVP_DigestUpdate(mdctx, rand, 128);
EVP_DigestFinal_ex(mdctx, data, &data_len);
prepare_rsa_signature(data, data_len, encoded, &enc_len, EVP_MD_type(md));
ck_assert_int_ne(RSA_padding_add_PKCS1_type_1(signinput, padlen, encoded, enc_len), 0);
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9a);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(RSA_verify(EVP_MD_type(md), data, data_len, signature, sig_len, rsa), 1);
RSA_free(rsa);
X509_free(cert);
EVP_MD_CTX_destroy(mdctx);
}
// Verify that imported key can not be attested
{
unsigned char attest[2048];
size_t attest_len = sizeof(attest);
ykpiv_devmodel model;
model = ykpiv_util_devicemodel(g_state);
res = ykpiv_attest(g_state, 0x9a, attest, &attest_len);
if (model == DEVTYPE_YK4) {
ck_assert_int_eq(res, YKPIV_GENERIC_ERROR);
}
else {
ck_assert_int_eq(res, YKPIV_NOT_SUPPORTED);
}
}
}
END_TEST
START_TEST(test_pin_policy_always) {
ykpiv_rc res;
{
ykpiv_devmodel model;
model = ykpiv_util_devicemodel(g_state);
// Only works with YK4. NEO should skip.
if (model != DEVTYPE_YK4) {
fprintf(stderr, "WARNING: Not supported with Yubikey NEO. Test skipped.\n");
return;
}
}
import_key(0x9e, YKPIV_PINPOLICY_ALWAYS);
// Verify certificate
{
BIO *bio = NULL;
X509 *cert = NULL;
RSA *rsa = NULL;
EVP_PKEY *pub_key = NULL;
const EVP_MD *md = EVP_sha256();
EVP_MD_CTX *mdctx;
unsigned char signature[1024];
unsigned char encoded[1024];
unsigned char data[1024];
unsigned char signinput[1024];
unsigned char rand[128];
size_t sig_len = sizeof(signature);
size_t padlen = 256;
unsigned int enc_len;
unsigned int data_len;
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
ck_assert_ptr_nonnull(cert);
BIO_free(bio);
pub_key = X509_get_pubkey(cert);
ck_assert_ptr_nonnull(pub_key);
rsa = EVP_PKEY_get1_RSA(pub_key);
ck_assert_ptr_nonnull(rsa);
EVP_PKEY_free(pub_key);
ck_assert_int_gt(RAND_bytes(rand, 128), 0);
mdctx = EVP_MD_CTX_create();
EVP_DigestInit_ex(mdctx, md, NULL);
EVP_DigestUpdate(mdctx, rand, 128);
EVP_DigestFinal_ex(mdctx, data, &data_len);
prepare_rsa_signature(data, data_len, encoded, &enc_len, EVP_MD_type(md));
ck_assert_int_ne(RSA_padding_add_PKCS1_type_1(signinput, padlen, encoded, enc_len), 0);
// Sign without verify: fail
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
// Sign with verify: pass
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
ck_assert_int_eq(res, YKPIV_OK);
// Sign again without verify: fail
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
// Sign again with verify: pass
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(RSA_verify(EVP_MD_type(md), data, data_len, signature, sig_len, rsa), 1);
RSA_free(rsa);
X509_free(cert);
EVP_MD_CTX_destroy(mdctx);
}
}
END_TEST
START_TEST(test_generate_key) {
ykpiv_rc res;
uint8_t *mod, *exp;
size_t mod_len, exp_len;
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_generate_key(g_state,
YKPIV_KEY_AUTHENTICATION,
YKPIV_ALGO_RSA2048,
YKPIV_PINPOLICY_DEFAULT,
YKPIV_TOUCHPOLICY_DEFAULT,
&mod,
&mod_len,
&exp,
&exp_len,
NULL,
NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_free(g_state, mod);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_free(g_state, exp);
ck_assert_int_eq(res, YKPIV_OK);
// Verify that imported key can be attested
{
ykpiv_devmodel model;
unsigned char attest[2048];
size_t attest_len = sizeof(attest);
model = ykpiv_util_devicemodel(g_state);
res = ykpiv_attest(g_state, YKPIV_KEY_AUTHENTICATION, attest, &attest_len);
// Only works with YK4. NEO should error.
if (model == DEVTYPE_YK4) {
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_gt(attest_len, 0);
}
else {
ck_assert_int_eq(res, YKPIV_NOT_SUPPORTED);
}
}
}
END_TEST
START_TEST(test_authenticate) {
ykpiv_rc res;
const char *default_mgm_key = "010203040506070801020304050607080102030405060708";
const char *mgm_key = "112233445566778811223344556677881122334455667788";
const char *weak_mgm_key = "FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE";
unsigned char key[24];
size_t key_len = sizeof(key);
// Try new key, fail.
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
// Try default key, succeed
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Verify same key works twice
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Change to new key
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_set_mgmkey(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Try new key, succeed.
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Change back to default key
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_set_mgmkey(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Try default key, succeed
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
// Try to set a weak key, fail
res = ykpiv_hex_decode(weak_mgm_key, strlen(weak_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_set_mgmkey(g_state, key);
ck_assert_int_eq(res, YKPIV_KEY_ERROR);
// Try default key, succeed
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_authenticate(g_state, key);
ck_assert_int_eq(res, YKPIV_OK);
}
END_TEST
START_TEST(test_change_pin) {
ykpiv_rc res;
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_change_pin(g_state, "123456", 6, "ABCDEF", 6, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
res = ykpiv_verify(g_state, "ABCDEF", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_change_pin(g_state, "ABCDEF", 6, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_verify(g_state, "ABCDEF", NULL);
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
}
END_TEST
START_TEST(test_change_puk) {
ykpiv_rc res;
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_change_puk(g_state, "12345678", 8, "ABCDEFGH", 8, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
res = ykpiv_unblock_pin(g_state, "ABCDEFGH", 8, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_change_puk(g_state, "ABCDEFGH", 8, "12345678", 8, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_unblock_pin(g_state, "ABCDEFGH", 8, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
ck_assert_int_eq(res, YKPIV_OK);
}
END_TEST
static int block_and_reset() {
ykpiv_rc res;
int tries = 100;
int tries_until_blocked;
tries_until_blocked = 0;
while (tries) {
res = ykpiv_verify(g_state, "AAAAAA", &tries);
if (res == YKPIV_PIN_LOCKED)
break;
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
tries_until_blocked++;
}
// Verify no PIN retries remaining
tries = 100;
res = ykpiv_get_pin_retries(g_state, &tries);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(tries, 0);
tries = 100;
while (tries) {
res = ykpiv_change_puk(g_state, "AAAAAAAA", 8, "AAAAAAAA", 8, &tries);
if (res == YKPIV_PIN_LOCKED)
break;
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
}
res = ykpiv_util_reset(g_state);
ck_assert_int_eq(res, YKPIV_OK);
return tries_until_blocked;
}
START_TEST(test_reset) {
ykpiv_rc res;
int tries = 100;
int tries_until_blocked;
// Block and reset, with default PIN retries
tries_until_blocked = block_and_reset();
ck_assert_int_eq(tries_until_blocked, 3);
// Authenticate and increase PIN retries
test_authenticate(0);
res = ykpiv_verify(g_state, "123456", NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_set_pin_retries(g_state, 8, 3);
ck_assert_int_eq(res, YKPIV_OK);
// Block and reset again, verifying increased PIN retries
tries_until_blocked = block_and_reset();
ck_assert_int_eq(tries_until_blocked, 8);
// Note: defaults back to 3 retries after reset
// Verify default (3) PIN retries remaining
tries = 0;
res = ykpiv_get_pin_retries(g_state, &tries);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(tries, 3);
// Verify still (3) PIN retries remaining
tries = 0;
res = ykpiv_get_pin_retries(g_state, &tries);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(tries, 3);
// Try wrong PIN
res = ykpiv_verify(g_state, "AAAAAA", &tries);
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
// Verify 2 PIN retries remaining
tries = 0;
res = ykpiv_get_pin_retries(g_state, &tries);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(tries, 2);
// Verify correct PIN
tries = 100;
res = ykpiv_verify(g_state, "123456", &tries);
ck_assert_int_eq(res, YKPIV_OK);
// Verify back to 3 PIN retries remaining
tries = 0;
res = ykpiv_get_pin_retries(g_state, &tries);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_int_eq(tries, 3);
}
END_TEST
struct t_alloc_data{
uint32_t count;
} g_alloc_data;
static void* _test_alloc(void *data, size_t cb) {
ck_assert_ptr_eq(data, &g_alloc_data);
((struct t_alloc_data*)data)->count++;
return calloc(cb, 1);
}
static void * _test_realloc(void *data, void *p, size_t cb) {
ck_assert_ptr_eq(data, &g_alloc_data);
return realloc(p, cb);
}
static void _test_free(void *data, void *p) {
fflush(stderr);
ck_assert_ptr_eq(data, &g_alloc_data);
((struct t_alloc_data*)data)->count--;
free(p);
}
ykpiv_allocator test_allocator_cbs = {
.pfn_alloc = _test_alloc,
.pfn_realloc = _test_realloc,
.pfn_free = _test_free,
.alloc_data = &g_alloc_data
};
uint8_t *alloc_auth_cert() {
ykpiv_rc res;
uint8_t *read_cert = NULL;
size_t read_cert_len = 0;
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(read_cert);
ck_assert_int_eq(read_cert_len, sizeof(g_cert));
ck_assert_mem_eq(g_cert, read_cert, sizeof(g_cert));
return read_cert;
}
START_TEST(test_allocator) {
ykpiv_rc res;
const ykpiv_allocator allocator;
uint8_t *cert1, *cert2;
res = ykpiv_done(g_state);
ck_assert_int_eq(res, YKPIV_OK);
g_state = NULL;
res = ykpiv_init_with_allocator(&g_state, false, &test_allocator_cbs);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(g_state);
// Verify we can communicate with device and make some allocations
res = ykpiv_connect(g_state, NULL);
ck_assert_int_eq(res, YKPIV_OK);
test_authenticate(0);
cert1 = alloc_auth_cert();
cert2 = alloc_auth_cert();
// Verify allocations went through custom allocator, and still live
ck_assert_int_gt(g_alloc_data.count, 1);
// Free and shutdown everything
ykpiv_util_free(g_state, cert2);
ykpiv_util_free(g_state, cert1);
res = ykpiv_disconnect(g_state);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_done(g_state);
ck_assert_int_eq(res, YKPIV_OK);
// Verify equal number of frees as allocations
ck_assert_int_eq(g_alloc_data.count, 0);
// Clear g_state so teardown() is skipped
g_state = NULL;
}
END_TEST
START_TEST(test_pin_cache) {
ykpiv_rc res;
ykpiv_state *local_state;
unsigned char data[256];
unsigned char data_in[256] = {0};
int len = sizeof(data);
size_t len2 = sizeof(data);
import_key(0x9a, YKPIV_PINPOLICY_DEFAULT);
// Disconnect and reconnect to device to guarantee it is not authed
res = ykpiv_disconnect(g_state);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_done(g_state);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_init(&g_state, true);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(g_state);
res = ykpiv_connect(g_state, NULL);
ck_assert_int_eq(res, YKPIV_OK);
// Verify decryption does not work without auth
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
// Verify decryption does work when authed
res = ykpiv_verify_select(g_state, "123456", 6, NULL, true);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
ck_assert_int_eq(res, YKPIV_OK);
// Verify PIN policy allows continuing to decrypt without re-verifying
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
ck_assert_int_eq(res, YKPIV_OK);
// Create a new ykpiv state, connect, and close it.
// This forces a card reset from another context, so the original global
// context will require a reconnect for its next transaction.
res = ykpiv_init(&local_state, true);
ck_assert_int_eq(res, YKPIV_OK);
ck_assert_ptr_nonnull(local_state);
res = ykpiv_connect(local_state, NULL);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_disconnect(local_state);
ck_assert_int_eq(res, YKPIV_OK);
res = ykpiv_done(local_state);
ck_assert_int_eq(res, YKPIV_OK);
// Verify we are still authenticated on the global context. This will
// require an automatic reconnect and re-verify with the cached PIN.
//
// Note that you can verify that this fails by rebuilding with
// DISABLE_PIN_CACHE set to 1.
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
ck_assert_int_eq(res, YKPIV_OK);
}
END_TEST
#endif
int destruction_confirmed(void) {
char *confirmed = getenv("YKPIV_ENV_HWTESTS_CONFIRMED");
if (confirmed && confirmed[0] == '1')
return 1;
// Use dprintf() to write directly to stdout, since automake eats the standard stdout/stderr pointers.
dprintf(0, "\n***\n*** Hardware tests skipped. Run \"make hwcheck\".\n***\n\n");
return 0;
}
Suite *test_suite(void) {
Suite *s;
TCase *tc;
s = suite_create("libykpiv api");
tc = tcase_create("api");
#ifdef HW_TESTS
tcase_add_unchecked_fixture(tc, setup, teardown);
// Must be first: Reset device. Tests run serially, and depend on a clean slate.
tcase_add_test(tc, test_reset);
// Authenticate after reset.
tcase_add_test(tc, test_authenticate);
// Test API functionality
tcase_add_test(tc, test_change_pin);
tcase_add_test(tc, test_change_puk);
tcase_add_test(tc, test_devicemodel);
tcase_add_test(tc, test_get_set_cardid);
tcase_add_test(tc, test_list_readers);
tcase_add_test(tc, test_read_write_list_delete_cert);
tcase_add_test(tc, test_import_key);
tcase_add_test(tc, test_pin_policy_always);
tcase_add_test(tc, test_generate_key);
tcase_add_test(tc, test_pin_cache);
// Must be last: tear down and re-test with custom memory allocator
tcase_add_test(tc, test_allocator);
#endif
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = test_suite();
sr = srunner_create(s);
srunner_set_fork_status(sr, CK_NOFORK);
srunner_run_all(sr, CK_VERBOSE);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
-98
View File
@@ -1,98 +0,0 @@
/*
* 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.
*
*/
#include "ykpiv.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <check.h>
START_TEST(test_version_string) {
if (strcmp(YKPIV_VERSION_STRING, ykpiv_check_version(NULL)) != 0) {
ck_abort_msg("version mismatch %s != %s\n", YKPIV_VERSION_STRING,
ykpiv_check_version(NULL));
}
if (ykpiv_check_version(YKPIV_VERSION_STRING) == NULL) {
ck_abort_msg("version NULL?\n");
}
if (ykpiv_check_version("99.99.99") != NULL) {
ck_abort_msg("version not NULL?\n");
}
fprintf(stderr, "ykpiv version: header %s library %s\n",
YKPIV_VERSION_STRING, ykpiv_check_version (NULL));
}
END_TEST
START_TEST(test_strerror) {
const char *s;
if (ykpiv_strerror(YKPIV_OK) == NULL) {
ck_abort_msg("ykpiv_strerror NULL\n");
}
s = ykpiv_strerror_name(YKPIV_OK);
if (s == NULL || strcmp(s, "YKPIV_OK") != 0) {
ck_abort_msg("ykpiv_strerror_name %s\n", s);
}
}
END_TEST
Suite *basic_suite(void) {
Suite *s;
TCase *tc;
s = suite_create("libykpiv basic");
tc = tcase_create("basic");
tcase_add_test(tc, test_version_string);
tcase_add_test(tc, test_strerror);
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = basic_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
+20
View File
@@ -0,0 +1,20 @@
//! Integration tests
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
use std::env;
use yubikey_piv::YubiKey;
#[test]
#[ignore]
fn connect() {
// Only show logs if `RUST_LOG` is set
if env::var("RUST_LOG").is_ok() {
env_logger::builder().format_timestamp(None).init();
}
let mut yubikey = YubiKey::open(None).unwrap();
dbg!(&yubikey.version());
dbg!(&yubikey.serial());
}
-101
View File
@@ -1,101 +0,0 @@
/*
* 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.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <check.h>
#include "ykpiv.h"
struct key {
const char text[49];
const unsigned char formatted[24];
int valid;
} keys[] = {
{"010203040506070801020304050607080102030405060708",
{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
1},
{"a1a2a3a4a5a6a7a8a1a2a3a4a5a6a7a8a1a2a3a4a5a6a7a8",
{0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8},
1},
{"A1A2A3A4A5A6A7A8A1A2A3A4A5A6A7A8A1A2A3A4A5A6A7A8",
{0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8},
1},
{"This is not something considered valid hex......",
{},
0},
};
static int parse_key(const char *text, const unsigned char *expected, int valid) {
unsigned char key[24];
size_t len = sizeof(key);
ykpiv_rc res = ykpiv_hex_decode(text, strlen(text), key, &len);
if (valid) {
ck_assert(res == YKPIV_OK);
ck_assert(memcmp(expected, key, 24) == 0);
} else {
ck_assert(res != YKPIV_OK);
}
return EXIT_SUCCESS;
}
START_TEST(test_parse_key) {
int res = parse_key(keys[_i].text, keys[_i].formatted, keys[_i].valid);
ck_assert(res == EXIT_SUCCESS);
}
END_TEST
Suite *parsekey_suite(void) {
Suite *s;
TCase *tc;
s = suite_create("libykpiv parsekey");
tc = tcase_create("parsekey");
tcase_add_loop_test(tc, test_parse_key, 0, sizeof(keys) / sizeof(struct key));
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int number_failed;
Suite *s;
SRunner *sr;
s = parsekey_suite();
sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}