Compare commits

..

52 Commits

Author SHA1 Message Date
Tony Arcieri 3a41fdc3bc yubikey-cli v0.0.1 2019-12-02 11:54:41 -08:00
Tony Arcieri c377f226e2 Merge pull request #53 from iqlusioninc/yubikey-piv/v0.0.3
yubikey-piv v0.0.3
2019-12-02 11:40:43 -08:00
Tony Arcieri da897b99bb yubikey-piv v0.0.3 2019-12-02 11:17:10 -08:00
Tony Arcieri 9fa2d1c051 Merge pull request #52 from iqlusioninc/cli
cli: Initial `yubikey-cli` utility with `list` command
2019-12-02 11:00:09 -08:00
Tony Arcieri 07f70bccb5 cli: Initial yubikey-cli utility with list command
Adds a `yubikey-cli` crate to the workspace, with a `yubikey` binary,
which presently provides a `list` command for listing detected readers.

Dependencies:

- `env_logger`: logging
- `gumdrop`: argument parsing
- `termcolor`: colored terminal output

As this repo now contains a binary, it also checks in `Cargo.lock`.
2019-12-02 10:42:17 -08:00
Tony Arcieri 8e1469cff6 Merge pull request #51 from iqlusioninc/readers
readers: Initial `Readers` enumerator for detecting YubiKeys
2019-12-02 10:20:04 -08:00
Tony Arcieri 9ce2ffe938 readers: Use Reader to connect to YubiKey
Removes the legacy API inherited from `yubico-piv-tool` and uses
the `reader` module exclusively for selecting and opening the PC/SC
reader.
2019-12-02 10:11:58 -08:00
Tony Arcieri 589ca3de12 readers: Initial Readers enumerator for detecting YubiKeys
Adds a `yubikey_piv::Readers` type which opens a PC/SC context and can
enumerate detected PC/SC readers with a slightly more ergonomic API than
what's provided in the upstream crate.

Does not support actually instantiating a `YubiKey` from a `Reader<'_>`
yet, but ideally all connections to YubiKeys should go through this API.
2019-12-02 09:32:42 -08:00
Tony Arcieri ae071e706c Merge pull request #45 from str4d/certificate-parsing
Certificate parsing
2019-12-01 11:17:49 -08:00
Jack Grigg cd704c28d7 Extract OID strings as constants 2019-12-01 18:42:12 +00:00
Jack Grigg 3a283aca40 Use ecdsa crate for EC point representations 2019-12-01 18:23:57 +00:00
Jack Grigg e72ee5c60e Parse EC public keys within certificates 2019-12-01 16:54:22 +00:00
Jack Grigg 9ee1494c6f Parse RSA public keys within certificates 2019-12-01 16:09:59 +00:00
Jack Grigg d3e565ef55 Derive PartialEq for SlotId 2019-12-01 15:35:00 +00:00
Tony Arcieri 2bdeca0069 Merge pull request #44 from str4d/more-enums
Convert SlotId and AlgorithmId into enums
2019-11-30 14:28:36 -08:00
Jack Grigg bc95d8b7b9 Delete unnecessary commented-out code
We will handle the CardManagement slot separately.
2019-11-30 22:18:31 +00:00
Jack Grigg 11c93d6421 Inline SlotId constants 2019-11-30 22:01:22 +00:00
Jack Grigg afca0fec0a Convert AlgorithmId into an enum
3DES also has an algorithm ID, but it is completely disjoint from the
key algorithms, and can be handled separately later.
2019-11-30 20:47:37 +00:00
Tony Arcieri c8837d485f Merge pull request #43 from iqlusioninc/pin-secrecy
Use `secrecy` crate for storing `CachedPin`
2019-11-30 12:39:33 -08:00
Jack Grigg 12b5bd1e3c Convert SlotId into an enum 2019-11-30 20:15:16 +00:00
Jack Grigg c3698dcffb Key::list: Skip Certificate::new for empty buffers
This matches the C code behaviour.
2019-11-30 20:15:12 +00:00
Tony Arcieri 6a16c59567 Use secrecy crate for storing CachedPin
The `SecretVec` type automatically handles zeroing and may prevent
accidental exposure of the cached PIN via `Debug`.
2019-11-30 12:11:53 -08:00
Tony Arcieri 8e38cf6c4e Merge pull request #42 from carl-wallace/develop
Change CHUID struct to hold complete CHUID value. Add getters for sub components. Add additional consts to support this. Modified CCCID struct to be public (as prelude to similar treatment).
2019-11-30 11:44:51 -08:00
Tony Arcieri ac665f9ec9 Merge pull request #40 from str4d/pin-fixups
PIN fixups
2019-11-30 11:43:35 -08:00
Carl Wallace 77302af21e address formatting per cargo fmt 2019-11-30 14:22:33 -05:00
Carl Wallace 78288b4200 address formatting and documentation issues flagged by clippy 2019-11-30 14:13:34 -05:00
Jack Grigg a61a6fd94b Define more YubiKey-recognized status words
Recognized values sourced from https://github.com/Yubico/yubikey-manager
NotFoundError and NoSpaceError are specified in SP 800-73-4 Table 6.
2019-11-30 15:39:11 +00:00
Jack Grigg cfef291ad9 Use u16 for raw StatusWords 2019-11-30 15:39:10 +00:00
Jack Grigg 4b5cd8dd45 Make PIN verification failure a StatusWord case
Retry count is now u8, as  it cannot exceed 16 (being returned in the
lower half of SW2).
2019-11-30 15:39:09 +00:00
Jack Grigg 9fe363661e verify_pin: Don't set APDU data for empty PIN 2019-11-30 15:16:15 +00:00
Tony Arcieri 4af95edc74 Merge pull request #39 from iqlusioninc/safety-dance-and-other-readme-badge-fixups
Add #![forbid(unsafe_code)]; fix up README.md badges and links
2019-11-29 10:18:25 -08:00
Tony Arcieri 7f3d821df2 Add #![forbid(unsafe_code)]; fix up README.md badges and links
- Forbids unsafe code
- Adds a "Safety Dance" badge
- Fixes the GitHub Actions status badge
- Fixes up links that changed with the move to `iqlusioninc` org
2019-11-29 10:06:52 -08:00
Tony Arcieri 2f963a15d0 Merge pull request #37 from str4d/safety-rails
Safety rails
2019-11-29 09:56:54 -08:00
Carl Wallace 4210571da3 Change CHUID struct to hold complete CHUID value. Add getters for subcomponents. Add additional consts to support this. Modified CCCID struct to be public (as prelude to similar treatment). 2019-11-29 09:31:24 -05:00
Jack Grigg 1db929c10f Mark excluded nested match branches as unreachable 2019-11-29 00:09:08 +00:00
Jack Grigg 8240575bb4 Rewrite YubiKey::import_private_key without unsafe 2019-11-28 23:44:16 +00:00
Jack Grigg 1935216cf3 Rewrite MsRoots::read without unsafe 2019-11-28 23:43:02 +00:00
Jack Grigg 7c08674fac Use slice::copy_within in metadata::read 2019-11-28 23:43:01 +00:00
Jack Grigg 8b86a0f578 Rewrite metadata::get_item without unsafe 2019-11-28 23:42:55 +00:00
Jack Grigg bd5669d9ef Rewrite metadata::set_item without unsafe
Also re-introduces some comments that were lost during corrosion.
2019-11-28 23:06:09 +00:00
Jack Grigg afb6a9479e Use slice::copy_within in read_certificate 2019-11-28 23:03:11 +00:00
Jack Grigg 48d0a2ab04 Use slice::copy_from_slice in Transaction::change_pin 2019-11-28 23:02:33 +00:00
Tony Arcieri 82b4bbb35d Merge pull request #36 from carl-wallace/develop
Make anonymous CHUID struct public, remove spurious subtraction of two bytes in transfer_data
2019-11-27 12:37:06 -08:00
Carl Wallace 13b350f822 change length comparison to is_empty check per clippy 2019-11-27 15:26:13 -05:00
Carl Wallace 0f1ef2f519 Make anonymous field of CHUID struct public. Remove spurious -2 instances inside Transaction::transfer_data (the Response object is already eating the status words) 2019-11-27 15:09:53 -05:00
Tony Arcieri d799e9c35b Merge pull request #34 from tarcieri/have-encrypt-sign-return-buffer
Have `sign_data` and `decrypt_data` return a `Buffer`
2019-11-26 11:21:38 -08:00
Tony Arcieri 5bf27f5422 Have sign_data and decrypt_data return a Buffer 2019-11-26 11:06:11 -08:00
Tony Arcieri ecea0081b5 Merge pull request #33 from tarcieri/ins-enum
`Ins` (APDU instruction codes) enum
2019-11-26 10:05:35 -08:00
Tony Arcieri debde6e765 Ins (APDU instruction codes) enum
Converts a bag of constant values (`YKPIV_INS_*`) into an enum
representing APDU instruction codes (a.k.a. `ins`).

Among other things, this makes the `Debug` output for `APDU` more human
meaningful, since it can print a text label for the instruction rather
than a code number, which is helpful in trace debugging.
2019-11-26 09:52:19 -08:00
Tony Arcieri 3fa5555943 Merge pull request #32 from tarcieri/factor-responses-into-apdu-module
Factor `Response` into `apdu` module; improved debugging
2019-11-26 09:26:55 -08:00
Tony Arcieri d3af2f2d80 Factor Response into apdu module; improved debugging
This commit merges the `apdu` and `response` modules: the responses are
APDU responses, and so the two are related.

This also moves the `trace` logging into the APDU type, which allows it
to display `Debug` output for APDUs and responses, which makes it easier
to understand what's going on (and will be even better once instructions
are converted into an enum so you can actually see what's happening).
2019-11-26 09:15:48 -08:00
Tony Arcieri 5fab09e54d Merge pull request #31 from tarcieri/v0.0.2
v0.0.2
2019-11-25 16:59:02 -08:00
31 changed files with 2918 additions and 972 deletions
+6 -6
View File
@@ -3,7 +3,7 @@
on: on:
pull_request: {} pull_request: {}
push: push:
branches: master branches: develop
name: Rust name: Rust
@@ -57,7 +57,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: test command: test
args: --release args: --all --release
- name: Run cargo build --all-features - name: Run cargo build --all-features
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -65,7 +65,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: build command: build
args: --all-features args: --all --all-features
test: test:
name: Test Suite name: Test Suite
@@ -94,7 +94,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: test command: test
args: --release args: --all --release
- name: Run cargo build --all-features - name: Run cargo build --all-features
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -102,7 +102,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
with: with:
command: build command: build
args: --all-features args: --all --all-features
fmt: fmt:
name: Rustfmt name: Rustfmt
@@ -149,7 +149,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: -- -D warnings args: --all --all-features -- -D warnings
# TODO: use actions-rs/audit-check # TODO: use actions-rs/audit-check
security_audit: security_audit:
-1
View File
@@ -1,3 +1,2 @@
/target /target
**/*.rs.bk **/*.rs.bk
Cargo.lock
+40 -11
View File
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.3] (2019-12-02)
### Added
- Initial `Readers` enumerator for detecting YubiKeys ([#51])
- Certificate parsing ([#45])
### Changed
- Use `Reader` to connect to `YubiKey` ([#51])
- Convert `SlotId` and `AlgorithmId` into enums ([#44])
- Use `secrecy` crate for storing `CachedPin` ([#43])
- Change `CHUID` struct to hold complete CHUID value ([#42])
- Eliminate all usages of `unsafe` ([#37], [#39])
- Make anonymous CHUID struct public ([#36])
- Have `sign_data` and `decrypt_data` return a `Buffer` ([#34])
- `Ins` (APDU instruction codes) enum ([#33])
- Factor `Response` into `apdu` module; improved debugging ([#32])
[0.0.3]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/53
[#51]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/51
[#45]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/45
[#44]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/44
[#43]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/43
[#42]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/42
[#39]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/39
[#37]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/37
[#36]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/36
[#34]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/34
[#33]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/33
[#32]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/32
## [0.0.2] (2019-11-25) ## [0.0.2] (2019-11-25)
### Added ### Added
- `untested` Cargo feature to mark untested functionality ([#30]) - `untested` Cargo feature to mark untested functionality ([#30])
@@ -19,17 +48,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `log` crate for logging ([#7]) - Use `log` crate for logging ([#7])
- Replace `ErrorKind::Ok` with `Result` ([#6]) - Replace `ErrorKind::Ok` with `Result` ([#6])
[0.0.2]: https://github.com/tarcieri/yubikey-piv.rs/pull/31 [0.0.2]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/31
[#30]: https://github.com/tarcieri/yubikey-piv.rs/pull/30 [#30]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/30
[#19]: https://github.com/tarcieri/yubikey-piv.rs/pull/19 [#19]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/19
[#17]: https://github.com/tarcieri/yubikey-piv.rs/pull/17 [#17]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/17
[#15]: https://github.com/tarcieri/yubikey-piv.rs/pull/15 [#15]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/15
[#13]: https://github.com/tarcieri/yubikey-piv.rs/pull/13 [#13]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/13
[#10]: https://github.com/tarcieri/yubikey-piv.rs/pull/10 [#10]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/10
[#9]: https://github.com/tarcieri/yubikey-piv.rs/pull/9 [#9]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/9
[#8]: https://github.com/tarcieri/yubikey-piv.rs/pull/8 [#8]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/8
[#7]: https://github.com/tarcieri/yubikey-piv.rs/pull/7 [#7]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/7
[#6]: https://github.com/tarcieri/yubikey-piv.rs/pull/6 [#6]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/6
## 0.0.1 (2019-11-18) ## 0.0.1 (2019-11-18)
- It typechecks, ship it! - It typechecks, ship it!
+7 -9
View File
@@ -6,8 +6,8 @@ In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience, size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and education, socio-economic status, nationality, personal appearance, race,
orientation. religion, or sexual identity and orientation.
## Our Standards ## Our Standards
@@ -55,8 +55,8 @@ further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [bascule@gmail.com]. All reported by contacting the project team at [oss@iqlusion.io](mailto:oss@iqlusion.io).
complaints will be reviewed and investigated and will result in a response that All complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident. obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately. Further details of specific enforcement policies may be posted separately.
@@ -65,12 +65,10 @@ Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other faith may face temporary or permanent repercussions as determined by other
members of the project's leadership. members of the project's leadership.
[bascule@gmail.com]: mailto:bascule@gmail.com
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version] available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
Generated
+1000
View File
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "yubikey-piv" name = "yubikey-piv"
version = "0.0.2" # Also update html_root_url in lib.rs when bumping this version = "0.0.3" # Also update html_root_url in lib.rs when bumping this
description = """ description = """
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV) Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
application providing general-purpose public-key signing and encryption application providing general-purpose public-key signing and encryption
@@ -10,23 +10,32 @@ algorithms (e.g, PKCS#1v1.5, ECDSA)
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"] authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
edition = "2018" edition = "2018"
license = "BSD-2-Clause" license = "BSD-2-Clause"
repository = "https://github.com/tarcieri/yubikey-piv.rs" repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
readme = "README.md" readme = "README.md"
categories = ["api-bindings", "cryptography", "hardware-support"] categories = ["api-bindings", "cryptography", "hardware-support"]
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"] keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
[workspace]
members = [".", "cli"]
[badges] [badges]
maintenance = { status = "experimental" } maintenance = { status = "experimental" }
[dependencies] [dependencies]
der-parser = "3"
des = "0.3" des = "0.3"
ecdsa = "0.1"
getrandom = "0.1" getrandom = "0.1"
hmac = "0.7" hmac = "0.7"
log = "0.4" log = "0.4"
nom = "5"
pbkdf2 = "0.3" pbkdf2 = "0.3"
pcsc = "2" pcsc = "2"
rsa = "0.1.4"
secrecy = "0.5"
sha-1 = "0.8" sha-1 = "0.8"
subtle = "2" subtle = "2"
x509-parser = "0.6"
zeroize = "1" zeroize = "1"
[dev-dependencies] [dev-dependencies]
+24 -18
View File
@@ -7,6 +7,7 @@
![Apache2/MIT licensed][license-image] ![Apache2/MIT licensed][license-image]
![Rust Version][rustc-image] ![Rust Version][rustc-image]
![Maintenance Status: Experimental][maintenance-image] ![Maintenance Status: Experimental][maintenance-image]
[![Safety Dance][safety-image]][safety-link]
[![Build Status][build-image]][build-link] [![Build Status][build-image]][build-link]
[![Gitter Chat][gitter-image]][gitter-link] [![Gitter Chat][gitter-image]][gitter-link]
@@ -115,12 +116,15 @@ Application Protocol Data Unit (APDU) messages, use the `trace` log level:
running 1 test running 1 test
[INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID' [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 [INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: SelectApplication, p1: 4, p2: 0, data: [160, 0, 0, 3, 8] }
[TRACE yubikey_piv::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8] [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::apdu] <<< Response { status_words: Success, data: [97, 17, 79, 6, 0, 0, 16, 0, 1, 0, 121, 7, 79, 5, 160, 0, 0, 3, 8] }
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetVersion, p1: 0, p2: 0, data: [] }
[TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0] [TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0]
[TRACE yubikey_piv::transaction] <<< [5, 1, 2, 144, 0] [TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [5, 1, 2] }
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetSerial, p1: 0, p2: 0, data: [] }
[TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0] [TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0]
[TRACE yubikey_piv::transaction] <<< [0, 115, 0, 178, 144, 0] [TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [0, 115, 0, 178] }
test connect ... ok test connect ... ok
``` ```
@@ -190,10 +194,12 @@ or conditions.
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg [license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg [rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg [maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[build-image]: https://github.com/tarcieri/yubikey-piv.rs/workflows/Rust/badge.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[build-link]: https://github.com/tarcieri/yubikey-piv.rs/actions [safety-link]: https://github.com/rust-secure-code/safety-dance/
[gitter-image]: https://badges.gitter.im/yubihsm-piv-rs.svg [build-image]: https://github.com/iqlusioninc/yubikey-piv.rs/workflows/Rust/badge.svg?branch=develop&event=push
[gitter-link]: https://gitter.im/yubikey-piv-rs/community [build-link]: https://github.com/iqlusioninc/yubikey-piv.rs/actions
[gitter-image]: https://badges.gitter.im/badge.svg
[gitter-link]: https://gitter.im/iqlusioninc/community
[//]: # (general links) [//]: # (general links)
@@ -206,18 +212,18 @@ or conditions.
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/ [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[Corrode]: https://github.com/jameysharp/corrode [Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/ [cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md [cc-md]: https://github.com/iqlusioninc/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
[BSDL]: https://opensource.org/licenses/BSD-2-Clause [BSDL]: https://opensource.org/licenses/BSD-2-Clause
[//]: # (github issues) [//]: # (github issues)
[#18]: https://github.com/tarcieri/yubikey-piv.rs/issues/18 [#18]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/18
[#20]: https://github.com/tarcieri/yubikey-piv.rs/issues/20 [#20]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/20
[#21]: https://github.com/tarcieri/yubikey-piv.rs/issues/21 [#21]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/21
[#22]: https://github.com/tarcieri/yubikey-piv.rs/issues/22 [#22]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/22
[#23]: https://github.com/tarcieri/yubikey-piv.rs/issues/23 [#23]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/23
[#24]: https://github.com/tarcieri/yubikey-piv.rs/issues/24 [#24]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/24
[#25]: https://github.com/tarcieri/yubikey-piv.rs/issues/25 [#25]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/25
[#26]: https://github.com/tarcieri/yubikey-piv.rs/issues/26 [#26]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/26
[#27]: https://github.com/tarcieri/yubikey-piv.rs/issues/27 [#27]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/27
[#28]: https://github.com/tarcieri/yubikey-piv.rs/issues/28 [#28]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/28
+8
View File
@@ -0,0 +1,8 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.0.1 (2019-12-02)
- Initial release
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "yubikey-cli"
version = "0.0.1"
description = """
Command-line interface for performing encryption and signing using RSA and/or
ECC keys stored on YubiKey devices.
"""
authors = ["Tony Arcieri <bascule@gmail.com>"]
edition = "2018"
license = "BSD-2-Clause"
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
readme = "README.md"
categories = ["command-line-utilities", "cryptography", "hardware-support"]
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
[dependencies]
gumdrop = "0.7"
env_logger = "0.7"
lazy_static = "1"
termcolor = "1"
yubikey-piv = { version = "0.0.3", path = ".." }
+112
View File
@@ -0,0 +1,112 @@
<img src="https://raw.githubusercontent.com/tendermint/yubihsm-rs/develop/img/logo.png" width="150" height="110">
# yubikey-cli.rs
[![crate][crate-image]][crate-link]
[![Docs][docs-image]][docs-link]
![Apache2/MIT licensed][license-image]
![Rust Version][rustc-image]
![Maintenance Status: Experimental][maintenance-image]
[![Safety Dance][safety-image]][safety-link]
[![Build Status][build-image]][build-link]
[![Gitter Chat][gitter-image]][gitter-link]
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][PIV] CLI
utility with general-purpose public-key encryption and signing support.
[Documentation][docs-link]
## Minimum Supported Rust Version
- Rust **1.39+**
## Supported YubiKeys
- [YubiKey NEO] series (may be dropped in the future, see [#18])
- [YubiKey 4] series
- [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported
## Security Warning
No security audits of this crate have ever been performed. Presently it is in
an experimental stage and may still contain high-severity issues.
USE AT YOUR OWN RISK!
## Status
WIP. Check back later.
## Code of Conduct
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
## License
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you shall be licensed under the
[2-Clause BSD License][BSDL] as shown above, without any additional terms
or conditions.
[//]: # (badges)
[crate-image]: https://img.shields.io/crates/v/yubikey-cli.svg
[crate-link]: https://crates.io/crates/yubikey-cli
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
[docs-link]: https://docs.rs/yubikey-cli/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/
[build-image]: https://github.com/iqlusioninc/yubikey-cli.rs/workflows/Rust/badge.svg?branch=develop&event=push
[build-link]: https://github.com/iqlusioninc/yubikey-cli.rs/actions
[gitter-image]: https://badges.gitter.im/badge.svg
[gitter-link]: https://gitter.im/iqlusioninc/community
[//]: # (general links)
[PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/iqlusioninc/yubikey-cli.rs/blob/develop/CODE_OF_CONDUCT.md
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
+16
View File
@@ -0,0 +1,16 @@
//! `yubikey` command-line utility
#![forbid(unsafe_code)]
#![warn(
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
use gumdrop::Options;
use yubikey_cli::commands::YubikeyCli;
fn main() {
YubikeyCli::parse_args_default_or_exit().run();
}
+98
View File
@@ -0,0 +1,98 @@
//! Commands of the CLI application
pub mod list;
use self::list::ListCmd;
use crate::status;
use gumdrop::Options;
use std::env;
use std::process::exit;
use termcolor::ColorChoice;
/// The `yubikey` CLI utility
#[derive(Debug, Options)]
pub struct YubikeyCli {
/// Obtain help about the current command
#[options(short = "h", help = "print help message")]
pub help: bool,
/// Subcommand to execute.
#[options(command)]
pub command: Option<Commands>,
}
impl YubikeyCli {
/// Run the underlying command type or print usage info and exit
pub fn run(&self) {
// TODO(tarcieri): make this more configurable
status::set_color_choice(ColorChoice::Auto);
// Only show logs if `RUST_LOG` is set
if env::var("RUST_LOG").is_ok() {
env_logger::builder().format_timestamp(None).init();
}
match &self.command {
Some(cmd) => cmd.run(),
None => println!("{}", Commands::usage()),
}
}
}
/// Subcommands of this application
#[derive(Debug, Options)]
pub enum Commands {
/// `help` subcommand
#[options(help = "show help for a command")]
Help(HelpOpts),
/// `version` subcommand
#[options(help = "display version information")]
Version(VersionOpts),
/// `list` subcommand
#[options(help = "list detected readers")]
List(ListCmd),
}
impl Commands {
/// Run the given command
pub fn run(&self) {
match self {
Commands::Help(help) => help.run(),
Commands::Version(version) => version.run(),
Commands::List(list) => list.run(),
}
}
}
/// Help options
#[derive(Debug, Options)]
pub struct HelpOpts {
#[options(free, help = "subcommand to get help for")]
free: Vec<String>,
}
impl HelpOpts {
fn run(&self) {
if let Some(command) = self.free.first() {
if let Some(usage) = Commands::command_usage(command) {
println!("{}", usage);
exit(1);
}
}
println!("{}", Commands::usage());
}
}
/// Version options
#[derive(Debug, Options)]
pub struct VersionOpts {}
impl VersionOpts {
/// Display version information
pub fn run(&self) {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
}
+40
View File
@@ -0,0 +1,40 @@
//! List detected readers
use gumdrop::Options;
use std::process::exit;
use yubikey_piv::readers::Readers;
/// The `list` subcommand
#[derive(Debug, Options)]
pub struct ListCmd {}
impl ListCmd {
/// Run the `list` subcommand
pub fn run(&self) {
let mut readers = Readers::open().unwrap_or_else(|e| {
status_err!("couldn't open PC/SC context: {}", e);
exit(1);
});
let readers_iter = readers.iter().unwrap_or_else(|e| {
status_err!("couldn't enumerate PC/SC readers: {}", e);
exit(1);
});
if readers_iter.len() == 0 {
status_err!("no YubiKeys detected!");
exit(1);
}
for (i, reader) in readers_iter.enumerate() {
let name = reader.name();
let mut yubikey = match reader.open() {
Ok(yk) => yk,
Err(_) => continue,
};
let serial = yubikey.serial();
println!("{}: {} (serial: {})", i + 1, name, serial);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
//! `yubikey` command-line utility
#![forbid(unsafe_code)]
#![warn(
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
#[macro_use]
pub mod status;
pub mod commands;
+165
View File
@@ -0,0 +1,165 @@
//! Status messages
use lazy_static::lazy_static;
use std::io::{self, Write};
use std::sync::Mutex;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
/// Print a success status message (in green if colors are enabled)
#[macro_export]
macro_rules! status_ok {
($status:expr, $msg:expr) => {
$crate::status::Status::new()
.justified()
.bold()
.color(termcolor::Color::Green)
.status($status)
.print_stdout($msg);
};
($status:expr, $fmt:expr, $($arg:tt)+) => {
$crate::status_ok!($status, format!($fmt, $($arg)+));
};
}
/// Print a warning status message (in yellow if colors are enabled)
#[macro_export]
macro_rules! status_warn {
($msg:expr) => {
$crate::status::Status::new()
.bold()
.color(termcolor::Color::Yellow)
.status("warning:")
.print_stdout($msg);
};
($fmt:expr, $($arg:tt)+) => {
$crate::status_warn!(format!($fmt, $($arg)+));
};
}
/// Print an error message (in red if colors are enabled)
#[macro_export]
macro_rules! status_err {
($msg:expr) => {
$crate::status::Status::new()
.bold()
.color(termcolor::Color::Red)
.status("error:")
.print_stderr($msg);
};
($fmt:expr, $($arg:tt)+) => {
$crate::status_err!(format!($fmt, $($arg)+));
};
}
lazy_static! {
/// Color configuration
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
/// Standard output
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice());
/// Standard error
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice());
}
/// Obtain the color configuration.
///
/// Panics if no configuration has been provided.
fn get_color_choice() -> ColorChoice {
let choice = COLOR_CHOICE.lock().unwrap();
*choice
.as_ref()
.expect("terminal stream accessed before initialized!")
}
/// Set the color configuration.
///
/// Panics if the terminal has already been configured.
pub(super) fn set_color_choice(color_choice: ColorChoice) {
let mut choice = COLOR_CHOICE.lock().unwrap();
assert!(choice.is_none(), "terminal colors already configured!");
*choice = Some(color_choice);
}
/// Status message builder
#[derive(Clone, Debug, Default)]
pub struct Status {
/// Should the status be justified?
justified: bool,
/// Should colors be bold?
bold: bool,
/// Color in which status should be displayed
color: Option<Color>,
/// Prefix of the status message (e.g. `Success`)
status: Option<String>,
}
impl Status {
/// Create a new status message with default settings
pub fn new() -> Self {
Self::default()
}
/// Justify status on display
pub fn justified(mut self) -> Self {
self.justified = true;
self
}
/// Make colors bold
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
/// Set the colors used to display this message
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
/// Set a status message to display
pub fn status<S>(mut self, msg: S) -> Self
where
S: ToString,
{
self.status = Some(msg.to_string());
self
}
/// Print the given message to stdout
pub fn print_stdout(self, msg: impl AsRef<str>) {
self.print(&*STDOUT, msg)
.expect("error printing to stdout!")
}
/// Print the given message to stderr
pub fn print_stderr(self, msg: impl AsRef<str>) {
self.print(&*STDERR, msg)
.expect("error printing to stderr!")
}
/// Print the given message
fn print(self, stream: &StandardStream, msg: impl AsRef<str>) -> Result<(), io::Error> {
let mut s = stream.lock();
s.reset()?;
s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?;
if let Some(status) = self.status {
if self.justified {
write!(s, "{:>12}", status)?;
} else {
write!(s, "{}", status)?;
}
}
s.reset()?;
writeln!(s, " {}", msg.as_ref())?;
s.flush()?;
Ok(())
}
}
+335 -29
View File
@@ -30,8 +30,8 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{error::Error, response::Response, transaction::Transaction, Buffer}; use crate::{error::Error, transaction::Transaction, Buffer};
use std::fmt::{self, Debug}; use log::trace;
use zeroize::{Zeroize, Zeroizing}; use zeroize::{Zeroize, Zeroizing};
/// Maximum amount of command data that can be included in an APDU /// Maximum amount of command data that can be included in an APDU
@@ -40,13 +40,13 @@ const APDU_DATA_MAX: usize = 0xFF;
/// Application Protocol Data Unit (APDU). /// Application Protocol Data Unit (APDU).
/// ///
/// These messages are packets used to communicate with the YubiKey. /// These messages are packets used to communicate with the YubiKey.
#[derive(Clone)] #[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct APDU { pub(crate) struct APDU {
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary) /// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
cla: u8, cla: u8,
/// Instruction code: indicates the specific command (e.g. "write data") /// Instruction code: indicates the specific command (e.g. "write data")
ins: u8, ins: Ins,
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data) /// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
p1: u8, p1: u8,
@@ -60,10 +60,10 @@ pub(crate) struct APDU {
impl APDU { impl APDU {
/// Create a new APDU with the given instruction code /// Create a new APDU with the given instruction code
pub fn new(ins: u8) -> Self { pub fn new(ins: impl Into<Ins>) -> Self {
Self { Self {
cla: 0, cla: 0,
ins, ins: ins.into(),
p1: 0, p1: 0,
p2: 0, p2: 0,
data: vec![], data: vec![],
@@ -112,15 +112,17 @@ impl APDU {
/// Transmit this APDU using the given card transaction /// Transmit this APDU using the given card transaction
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> { pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> {
let response_bytes = txn.transmit(&self.to_bytes(), recv_len)?; trace!(">>> {:?}", self);
Ok(Response::from_bytes(response_bytes)) let response = Response::from(txn.transmit(&self.to_bytes(), recv_len)?);
trace!("<<< {:?}", &response);
Ok(response)
} }
/// Consume this APDU and return a self-zeroizing buffer /// Serialize this APDU as a self-zeroizing byte buffer
pub fn to_bytes(&self) -> Buffer { pub fn to_bytes(&self) -> Buffer {
let mut bytes = Vec::with_capacity(5 + self.data.len()); let mut bytes = Vec::with_capacity(5 + self.data.len());
bytes.push(self.cla); bytes.push(self.cla);
bytes.push(self.ins); bytes.push(self.ins.code());
bytes.push(self.p1); bytes.push(self.p1);
bytes.push(self.p2); bytes.push(self.p2);
bytes.push(self.data.len() as u8); bytes.push(self.data.len() as u8);
@@ -129,21 +131,6 @@ impl APDU {
} }
} }
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 { impl Drop for APDU {
fn drop(&mut self) { fn drop(&mut self) {
self.zeroize(); self.zeroize();
@@ -152,10 +139,329 @@ impl Drop for APDU {
impl Zeroize for APDU { impl Zeroize for APDU {
fn zeroize(&mut self) { fn zeroize(&mut self) {
self.cla.zeroize(); // Only `data` may contain secrets
self.ins.zeroize();
self.p1.zeroize();
self.p2.zeroize();
self.data.zeroize(); self.data.zeroize();
} }
} }
/// APDU instruction codes
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Ins {
/// Verify
Verify,
/// Change reference
ChangeReference,
/// Reset retry
ResetRetry,
/// Generate asymmetric
GenerateAsymmetric,
/// Authenticate
Authenticate,
/// Get data
GetData,
/// Put data
PutData,
/// Select application
SelectApplication,
/// Get response APDU
GetResponseApdu,
// Yubico vendor specific instructions
// <https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html>
/// Set MGM key
SetMgmKey,
/// Import key
ImportKey,
/// Get version
GetVersion,
/// Reset device
Reset,
/// Set PIN retries
SetPinRetries,
/// Generate attestation certificate for asymmetric key
Attest,
/// Get device serial
GetSerial,
/// Other/unrecognized instruction codes
Other(u8),
}
impl Ins {
/// Get the code that corresponds to this instruction
pub fn code(self) -> u8 {
match self {
Ins::Verify => 0x20,
Ins::ChangeReference => 0x24,
Ins::ResetRetry => 0x2c,
Ins::GenerateAsymmetric => 0x47,
Ins::Authenticate => 0x87,
Ins::GetData => 0xcb,
Ins::PutData => 0xdb,
Ins::SelectApplication => 0xa4,
Ins::GetResponseApdu => 0xc0,
Ins::SetMgmKey => 0xff,
Ins::ImportKey => 0xfe,
Ins::GetVersion => 0xfd,
Ins::Reset => 0xfb,
Ins::SetPinRetries => 0xfa,
Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8,
Ins::Other(code) => code,
}
}
}
impl From<u8> for Ins {
fn from(code: u8) -> Self {
match code {
0x20 => Ins::Verify,
0x24 => Ins::ChangeReference,
0x2c => Ins::ResetRetry,
0x47 => Ins::GenerateAsymmetric,
0x87 => Ins::Authenticate,
0xcb => Ins::GetData,
0xdb => Ins::PutData,
0xa4 => Ins::SelectApplication,
0xc0 => Ins::GetResponseApdu,
0xff => Ins::SetMgmKey,
0xfe => Ins::ImportKey,
0xfd => Ins::GetVersion,
0xfb => Ins::Reset,
0xfa => Ins::SetPinRetries,
0xf9 => Ins::Attest,
0xf8 => Ins::GetSerial,
code => Ins::Other(code),
}
}
}
impl From<Ins> for u8 {
fn from(ins: Ins) -> u8 {
ins.code()
}
}
/// APDU responses
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Response {
/// Status words
status_words: StatusWords,
/// Buffer
data: Vec<u8>,
}
impl Response {
/// Create a new response from the given status words and buffer
#[cfg(feature = "untested")]
pub fn new(status_words: StatusWords, data: Vec<u8>) -> Response {
Response { status_words, data }
}
/// 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) -> u16 {
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 data
pub fn data(&self) -> &[u8] {
self.data.as_ref()
}
}
impl AsRef<[u8]> for Response {
fn as_ref(&self) -> &[u8] {
self.data()
}
}
impl Drop for Response {
fn drop(&mut self) {
self.zeroize();
}
}
impl From<Vec<u8>> for Response {
fn from(mut bytes: Vec<u8>) -> Self {
if bytes.len() < 2 {
return Response {
status_words: StatusWords::None,
data: bytes,
};
}
let sw = StatusWords::from(
(bytes[bytes.len() - 2] as u16) << 8 | (bytes[bytes.len() - 1] as u16),
);
let len = bytes.len() - 2;
bytes.truncate(len);
Response {
status_words: sw,
data: bytes,
}
}
}
impl Zeroize for Response {
fn zeroize(&mut self) {
// Only `data` may contain secrets
self.data.zeroize();
}
}
/// 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,
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L53
NoInputDataError,
/// PIN verification failure
VerifyFailError {
/// Remaining verification attempts
tries: u8,
},
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L55
WrongLengthError,
/// Security status not satisfied
SecurityStatusError,
/// Authentication method blocked
AuthBlockedError,
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L58
DataInvalidError,
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L59
ConditionsNotSatisfiedError,
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L60
CommandNotAllowedError,
/// Incorrect parameter in command data field
IncorrectParamError,
/// Data object or application not found
NotFoundError,
/// Not enough memory
NoSpaceError,
//
// Custom Yubico Status Word extensions
//
/// Incorrect card slot error
IncorrectSlotError,
/// Not supported error
NotSupportedError,
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L65
CommandAbortedError,
/// Other/unrecognized status words
Other(u16),
}
impl StatusWords {
/// Get the numerical response code for these status words
pub fn code(self) -> u16 {
match self {
StatusWords::None => 0,
StatusWords::NoInputDataError => 0x6285,
StatusWords::VerifyFailError { tries } => 0x63c0 & tries as u16,
StatusWords::WrongLengthError => 0x6700,
StatusWords::SecurityStatusError => 0x6982,
StatusWords::AuthBlockedError => 0x6983,
StatusWords::DataInvalidError => 0x6984,
StatusWords::ConditionsNotSatisfiedError => 0x6985,
StatusWords::CommandNotAllowedError => 0x6986,
StatusWords::IncorrectParamError => 0x6a80,
StatusWords::NotFoundError => 0x6a82,
StatusWords::NoSpaceError => 0x6a84,
StatusWords::IncorrectSlotError => 0x6b00,
StatusWords::NotSupportedError => 0x6d00,
StatusWords::CommandAbortedError => 0x6f00,
StatusWords::Success => 0x9000,
StatusWords::Other(n) => n,
}
}
/// Do these status words indicate success?
pub fn is_success(self) -> bool {
self == StatusWords::Success
}
}
impl From<u16> for StatusWords {
fn from(sw: u16) -> Self {
match sw {
0x0000 => StatusWords::None,
0x6285 => StatusWords::NoInputDataError,
sw if sw & 0xfff0 == 0x63c0 => StatusWords::VerifyFailError {
tries: (sw & 0x000f) as u8,
},
0x6700 => StatusWords::WrongLengthError,
0x6982 => StatusWords::SecurityStatusError,
0x6983 => StatusWords::AuthBlockedError,
0x6984 => StatusWords::DataInvalidError,
0x6985 => StatusWords::ConditionsNotSatisfiedError,
0x6986 => StatusWords::CommandNotAllowedError,
0x6a80 => StatusWords::IncorrectParamError,
0x6a82 => StatusWords::NotFoundError,
0x6a84 => StatusWords::NoSpaceError,
0x6b00 => StatusWords::IncorrectSlotError,
0x6d00 => StatusWords::NotSupportedError,
0x6f00 => StatusWords::CommandAbortedError,
0x9000 => StatusWords::Success,
_ => StatusWords::Other(sw),
}
}
}
impl From<StatusWords> for u16 {
fn from(sw: StatusWords) -> u16 {
sw.code()
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ const CCC_TMPL: &[u8] = &[
/// Cardholder Capability Container (CCC) Identifier /// Cardholder Capability Container (CCC) Identifier
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct CCCID([u8; YKPIV_CCCID_SIZE]); pub struct CCCID(pub [u8; YKPIV_CCCID_SIZE]);
impl CCCID { impl CCCID {
/// Generate a random CCCID /// Generate a random CCCID
+230 -14
View File
@@ -33,19 +33,141 @@
use crate::{ use crate::{
consts::*, consts::*,
error::Error, error::Error,
key::{self, SlotId}, key::{AlgorithmId, SlotId},
serialization::*, serialization::*,
transaction::Transaction, transaction::Transaction,
yubikey::YubiKey, yubikey::YubiKey,
Buffer, Buffer,
}; };
use ecdsa::{
curve::{CompressedCurvePoint, NistP256, NistP384, UncompressedCurvePoint},
generic_array::GenericArray,
};
use log::error; use log::error;
use std::ptr; use rsa::{PublicKey, RSAPublicKey};
use std::fmt;
use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo};
use zeroize::Zeroizing; use zeroize::Zeroizing;
// TODO: Make these der_parser::oid::Oid constants when it has const fn support.
const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
const OID_NIST_P256: &str = "1.2.840.10045.3.1.7";
const OID_NIST_P384: &str = "1.3.132.0.34";
/// An encoded point on the Nist P-256 curve.
#[derive(Clone, Eq, PartialEq)]
pub enum EcP256Point {
/// Compressed encoding of a point on the curve.
Compressed(CompressedCurvePoint<NistP256>),
/// Uncompressed encoding of a point on the curve.
Uncompressed(UncompressedCurvePoint<NistP256>),
}
/// An encoded point on the Nist P-384 curve.
#[derive(Clone, Eq, PartialEq)]
pub enum EcP384Point {
/// Compressed encoding of a point on the curve.
Compressed(CompressedCurvePoint<NistP384>),
/// Uncompressed encoding of a point on the curve.
Uncompressed(UncompressedCurvePoint<NistP384>),
}
/// Information about a public key within a [`Certificate`].
#[derive(Clone, Eq, PartialEq)]
pub enum PublicKeyInfo {
/// RSA keys
Rsa {
/// RSA algorithm
algorithm: AlgorithmId,
/// Public key
pubkey: RSAPublicKey,
},
/// EC P-256 keys
EcP256(EcP256Point),
/// EC P-384 keys
EcP384(EcP384Point),
}
impl fmt::Debug for PublicKeyInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKeyInfo({:?})", self.algorithm())
}
}
impl PublicKeyInfo {
fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result<Self, Error> {
match subject_pki.algorithm.algorithm.to_string().as_str() {
OID_RSA_ENCRYPTION => {
let pubkey = read_pki::rsa_pubkey(subject_pki.subject_public_key.data)?;
Ok(PublicKeyInfo::Rsa {
algorithm: match pubkey.n().bits() {
1024 => AlgorithmId::Rsa1024,
2048 => AlgorithmId::Rsa2048,
_ => return Err(Error::AlgorithmError),
},
pubkey,
})
}
OID_EC_PUBLIC_KEY => {
let key_bytes = &subject_pki.subject_public_key.data;
match read_pki::ec_parameters(&subject_pki.algorithm.parameters)? {
AlgorithmId::EccP256 => match key_bytes.len() {
33 => CompressedCurvePoint::<NistP256>::from_bytes(
GenericArray::clone_from_slice(key_bytes),
)
.map(EcP256Point::Compressed),
65 => UncompressedCurvePoint::<NistP256>::from_bytes(
GenericArray::clone_from_slice(key_bytes),
)
.map(EcP256Point::Uncompressed),
_ => None,
}
.map(PublicKeyInfo::EcP256)
.ok_or(Error::InvalidObject),
AlgorithmId::EccP384 => match key_bytes.len() {
49 => CompressedCurvePoint::<NistP384>::from_bytes(
GenericArray::clone_from_slice(key_bytes),
)
.map(EcP384Point::Compressed),
97 => UncompressedCurvePoint::<NistP384>::from_bytes(
GenericArray::clone_from_slice(key_bytes),
)
.map(EcP384Point::Uncompressed),
_ => None,
}
.map(PublicKeyInfo::EcP384)
.ok_or(Error::InvalidObject),
_ => Err(Error::AlgorithmError),
}
}
_ => Err(Error::InvalidObject),
}
}
/// Returns the algorithm that this public key can be used with.
pub fn algorithm(&self) -> AlgorithmId {
match self {
PublicKeyInfo::Rsa { algorithm, .. } => *algorithm,
PublicKeyInfo::EcP256(_) => AlgorithmId::EccP256,
PublicKeyInfo::EcP384(_) => AlgorithmId::EccP384,
}
}
}
/// Certificates /// Certificates
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Certificate(Buffer); pub struct Certificate {
subject: String,
subject_pki: PublicKeyInfo,
data: Buffer,
}
impl Certificate { impl Certificate {
/// Read a certificate from the given slot in the YubiKey /// Read a certificate from the given slot in the YubiKey
@@ -57,14 +179,14 @@ impl Certificate {
return Err(Error::InvalidObject); return Err(Error::InvalidObject);
} }
Ok(Certificate(buf)) Certificate::new(buf)
} }
/// Write this certificate into the YubiKey in the given slot /// Write this certificate into the YubiKey in the given slot
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> { pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> {
let max_size = yubikey.obj_size_max(); let max_size = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, Some(&self.0), certinfo, max_size) write_certificate(&txn, slot, Some(&self.data), certinfo, max_size)
} }
/// Delete a certificate located at the given slot of the given YubiKey /// Delete a certificate located at the given slot of the given YubiKey
@@ -83,25 +205,47 @@ impl Certificate {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
Ok(Certificate(cert)) let parsed_cert = match parse_x509_der(&cert) {
Ok((_, cert)) => cert,
_ => return Err(Error::InvalidObject),
};
let subject = format!("{}", parsed_cert.tbs_certificate.subject);
let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?;
Ok(Certificate {
subject,
subject_pki,
data: cert,
})
}
/// Returns the SubjectName field of the certificate.
pub fn subject(&self) -> &str {
&self.subject
}
/// Returns the SubjectPublicKeyInfo field of the certificate.
pub fn subject_pki(&self) -> &PublicKeyInfo {
&self.subject_pki
} }
/// Extract the inner buffer /// Extract the inner buffer
pub fn into_buffer(self) -> Buffer { pub fn into_buffer(self) -> Buffer {
self.0 self.data
} }
} }
impl AsRef<[u8]> for Certificate { impl AsRef<[u8]> for Certificate {
fn as_ref(&self) -> &[u8] { fn as_ref(&self) -> &[u8] {
self.0.as_ref() self.data.as_ref()
} }
} }
/// Read certificate /// Read certificate
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> { pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> {
let mut len: usize = 0; let mut len: usize = 0;
let object_id = key::slot_object(slot)?; let object_id = slot.object_id();
let mut buf = match txn.fetch_object(object_id) { let mut buf = match txn.fetch_object(object_id) {
Ok(b) => b, Ok(b) => b,
@@ -124,10 +268,7 @@ pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Bu
return Ok(Zeroizing::new(vec![])); return Ok(Zeroizing::new(vec![]));
} }
unsafe { buf.copy_within(offset..offset + len, 0);
ptr::copy(buf.as_ptr().add(offset), buf.as_mut_ptr(), len);
}
buf.truncate(len); buf.truncate(len);
} }
@@ -145,7 +286,7 @@ pub(crate) fn write_certificate(
let mut buf = [0u8; CB_OBJ_MAX]; let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = 0; let mut offset = 0;
let object_id = key::slot_object(slot)?; let object_id = slot.object_id();
if data.is_none() { if data.is_none() {
return txn.save_object(object_id, &[]); return txn.save_object(object_id, &[]);
@@ -184,3 +325,78 @@ pub(crate) fn write_certificate(
txn.save_object(object_id, &buf[..offset]) txn.save_object(object_id, &buf[..offset])
} }
mod read_pki {
use der_parser::{
ber::BerObjectContent,
der::{parse_der_integer, DerObject},
error::BerError,
*,
};
use nom::{combinator, IResult};
use rsa::{BigUint, RSAPublicKey};
use super::{OID_NIST_P256, OID_NIST_P384};
use crate::{error::Error, key::AlgorithmId};
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
/// ```text
/// RSAPublicKey ::= SEQUENCE {
/// modulus INTEGER, -- n
/// publicExponent INTEGER -- e
/// }
/// ```
pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result<RSAPublicKey, Error> {
fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], DerObject<'_>, BerError> {
parse_der_sequence_defined!(i, parse_der_integer >> parse_der_integer)
}
fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> {
combinator::map(parse_rsa_pubkey, |object| {
let seq = object.as_sequence().expect("is DER sequence");
assert_eq!(seq.len(), 2);
let n = match seq[0].content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
};
let e = match seq[1].content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
};
(n, e)
})(i)
}
let (n, e) = match rsa_pubkey_parts(encoded) {
Ok((_, res)) => res,
_ => return Err(Error::InvalidObject),
};
RSAPublicKey::new(n, e).map_err(|_| Error::InvalidObject)
}
/// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
/// ```text
/// ECParameters ::= CHOICE {
/// namedCurve OBJECT IDENTIFIER
/// -- implicitCurve NULL
/// -- specifiedCurve SpecifiedECDomain
/// }
/// ```
pub(super) fn ec_parameters(parameters: &DerObject<'_>) -> Result<AlgorithmId, Error> {
let curve_oid = match parameters.as_context_specific() {
Ok((_, Some(named_curve))) => {
named_curve.as_oid_val().map_err(|_| Error::InvalidObject)
}
_ => Err(Error::InvalidObject),
}?;
match curve_oid.to_string().as_str() {
OID_NIST_P256 => Ok(AlgorithmId::EccP256),
OID_NIST_P384 => Ok(AlgorithmId::EccP384),
_ => Err(Error::AlgorithmError),
}
}
}
+37 -9
View File
@@ -55,20 +55,47 @@ const CHUID_TMPL: &[u8] = &[
0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00, 0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
]; ];
/// Cardholder Unique Identifier (CHUID) /// Cardholder Unique Identifier (CHUID) Card UUID/GUID value
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct CHUID([u8; YKPIV_CARDID_SIZE]); pub struct ChuidUuid(pub [u8; YKPIV_CARDID_SIZE]);
/// Cardholder Unique Identifier (CHUID)
#[derive(Copy, Clone)]
pub struct CHUID(pub [u8; YKPIV_CHUID_SIZE]);
impl CHUID { impl CHUID {
/// Return FASC-N component of CHUID
pub fn fascn(&self) -> Result<[u8; YKPIV_FASCN_SIZE], Error> {
let mut fascn = [0u8; YKPIV_FASCN_SIZE];
fascn.copy_from_slice(&self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + YKPIV_FASCN_SIZE)]);
Ok(fascn)
}
/// Return Card UUID/GUID component of CHUID
pub fn uuid(&self) -> Result<[u8; YKPIV_CARDID_SIZE], Error> {
let mut uuid = [0u8; YKPIV_CARDID_SIZE];
uuid.copy_from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + YKPIV_CARDID_SIZE)]);
Ok(uuid)
}
/// Return expiration date component of CHUID
pub fn expiration(&self) -> Result<[u8; YKPIV_EXPIRATION_SIZE], Error> {
let mut expiration = [0u8; YKPIV_EXPIRATION_SIZE];
expiration.copy_from_slice(
&self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + YKPIV_EXPIRATION_SIZE)],
);
Ok(expiration)
}
/// Generate a random Cardholder Unique Identifier (CHUID) /// Generate a random Cardholder Unique Identifier (CHUID)
pub fn generate() -> Result<Self, Error> { pub fn generate() -> Result<ChuidUuid, Error> {
let mut id = [0u8; YKPIV_CARDID_SIZE]; let mut id = [0u8; YKPIV_CARDID_SIZE];
getrandom(&mut id).map_err(|_| Error::RandomnessError)?; getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
Ok(CHUID(id)) Ok(ChuidUuid(id))
} }
/// Get Cardholder Unique Identifier (CHUID) /// Get Cardholder Unique Identifier (CHUID)
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> { pub fn get(yubikey: &mut YubiKey) -> Result<CHUID, Error> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(YKPIV_OBJ_CHUID)?; let response = txn.fetch_object(YKPIV_OBJ_CHUID)?;
@@ -76,15 +103,16 @@ impl CHUID {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
let mut cardid = [0u8; YKPIV_CARDID_SIZE]; let mut chuid = [0u8; YKPIV_CHUID_SIZE];
cardid.copy_from_slice(&response[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + YKPIV_CARDID_SIZE)]); chuid.copy_from_slice(&response[0..YKPIV_CHUID_SIZE]);
Ok(CHUID(cardid)) let retval = CHUID { 0: chuid };
Ok(retval)
} }
/// Set Cardholder Unique Identifier (CHUID) /// Set Cardholder Unique Identifier (CHUID)
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> { pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
let mut buf = CHUID_TMPL.to_vec(); let mut buf = CHUID_TMPL.to_vec();
buf[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + self.0.len())].copy_from_slice(&self.0); buf[0..self.0.len()].copy_from_slice(&self.0);
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
txn.save_object(YKPIV_OBJ_CHUID, &buf) txn.save_object(YKPIV_OBJ_CHUID, &buf)
+5 -75
View File
@@ -63,7 +63,9 @@ pub const CB_PIN_MAX: usize = 8;
pub const CCC_ID_OFFS: usize = 9; pub const CCC_ID_OFFS: usize = 9;
pub const CHUID_FASCN_OFFS: usize = 2;
pub const CHUID_GUID_OFFS: usize = 29; pub const CHUID_GUID_OFFS: usize = 29;
pub const CHUID_EXPIRATION_OFFS: usize = 47;
pub const CHREF_ACT_CHANGE_PIN: i32 = 0; pub const CHREF_ACT_CHANGE_PIN: i32 = 0;
pub const CHREF_ACT_UNBLOCK_PIN: i32 = 1; pub const CHREF_ACT_UNBLOCK_PIN: i32 = 1;
@@ -122,106 +124,34 @@ pub const TAG_ECC_POINT: u8 = 0x86;
pub const YKPIV_ALGO_TAG: u8 = 0x80; pub const YKPIV_ALGO_TAG: u8 = 0x80;
pub const YKPIV_ALGO_3DES: u8 = 0x03; pub const YKPIV_ALGO_3DES: u8 = 0x03;
pub const YKPIV_ALGO_RSA1024: u8 = 0x06;
pub const YKPIV_ALGO_RSA2048: u8 = 0x07;
pub const YKPIV_ALGO_ECCP256: u8 = 0x11;
pub const YKPIV_ALGO_ECCP384: u8 = 0x14;
pub const YKPIV_ATR_NEO_R3: &[u8] = b";\xFC\x13\0\0\x811\xFE\x15YubikeyNEOr3\xE1\0"; pub const YKPIV_ATR_NEO_R3: &[u8] = b";\xFC\x13\0\0\x811\xFE\x15YubikeyNEOr3\xE1\0";
pub const YKPIV_CHUID_SIZE: usize = 59;
pub const YKPIV_CARDID_SIZE: usize = 16; pub const YKPIV_CARDID_SIZE: usize = 16;
pub const YKPIV_FASCN_SIZE: usize = 25;
pub const YKPIV_EXPIRATION_SIZE: usize = 8;
pub const YKPIV_CCCID_SIZE: usize = 14; pub const YKPIV_CCCID_SIZE: usize = 14;
pub const YKPIV_CERTINFO_UNCOMPRESSED: u8 = 0; pub const YKPIV_CERTINFO_UNCOMPRESSED: u8 = 0;
pub const YKPIV_CERTINFO_GZIP: u8 = 1; pub const YKPIV_CERTINFO_GZIP: u8 = 1;
pub const YKPIV_INS_VERIFY: u8 = 0x20;
pub const YKPIV_INS_CHANGE_REFERENCE: u8 = 0x24;
pub const YKPIV_INS_RESET_RETRY: u8 = 0x2c;
pub const YKPIV_INS_GENERATE_ASYMMETRIC: u8 = 0x47;
pub const YKPIV_INS_AUTHENTICATE: u8 = 0x87;
pub const YKPIV_INS_GET_DATA: u8 = 0xcb;
pub const YKPIV_INS_PUT_DATA: u8 = 0xdb;
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;
pub const YKPIV_INS_RESET: u8 = 0xfb;
pub const YKPIV_INS_SET_PIN_RETRIES: u8 = 0xfa;
pub const YKPIV_INS_ATTEST: u8 = 0xf9;
pub const YKPIV_INS_GET_SERIAL: u8 = 0xf8;
pub const YKPIV_KEY_AUTHENTICATION: u8 = 0x9a;
pub const YKPIV_KEY_CARDMGM: u8 = 0x9b; pub const YKPIV_KEY_CARDMGM: u8 = 0x9b;
pub const YKPIV_KEY_SIGNATURE: u8 = 0x9c;
pub const YKPIV_KEY_KEYMGM: u8 = 0x9d;
pub const YKPIV_KEY_CARDAUTH: u8 = 0x9e;
pub const YKPIV_KEY_RETIRED1: u8 = 0x82;
pub const YKPIV_KEY_RETIRED2: u8 = 0x83;
pub const YKPIV_KEY_RETIRED3: u8 = 0x84;
pub const YKPIV_KEY_RETIRED4: u8 = 0x85;
pub const YKPIV_KEY_RETIRED5: u8 = 0x86;
pub const YKPIV_KEY_RETIRED6: u8 = 0x87;
pub const YKPIV_KEY_RETIRED7: u8 = 0x88;
pub const YKPIV_KEY_RETIRED8: u8 = 0x89;
pub const YKPIV_KEY_RETIRED9: u8 = 0x8a;
pub const YKPIV_KEY_RETIRED10: u8 = 0x8b;
pub const YKPIV_KEY_RETIRED11: u8 = 0x8c;
pub const YKPIV_KEY_RETIRED12: u8 = 0x8d;
pub const YKPIV_KEY_RETIRED13: u8 = 0x8e;
pub const YKPIV_KEY_RETIRED14: u8 = 0x8f;
pub const YKPIV_KEY_RETIRED15: u8 = 0x90;
pub const YKPIV_KEY_RETIRED16: u8 = 0x91;
pub const YKPIV_KEY_RETIRED17: u8 = 0x92;
pub const YKPIV_KEY_RETIRED18: u8 = 0x93;
pub const YKPIV_KEY_RETIRED19: u8 = 0x94;
pub const YKPIV_KEY_RETIRED20: u8 = 0x95;
pub const YKPIV_KEY_ATTESTATION: u8 = 0xf9;
pub const YKPIV_OBJ_CAPABILITY: u32 = 0x005f_c107; pub const YKPIV_OBJ_CAPABILITY: u32 = 0x005f_c107;
pub const YKPIV_OBJ_CHUID: u32 = 0x005f_c102; pub const YKPIV_OBJ_CHUID: u32 = 0x005f_c102;
pub const YKPIV_OBJ_AUTHENTICATION: u32 = 0x005f_c105; // cert for 9a key
pub const YKPIV_OBJ_FINGERPRINTS: u32 = 0x005f_c103; pub const YKPIV_OBJ_FINGERPRINTS: u32 = 0x005f_c103;
pub const YKPIV_OBJ_SECURITY: u32 = 0x005f_c106; pub const YKPIV_OBJ_SECURITY: u32 = 0x005f_c106;
pub const YKPIV_OBJ_FACIAL: u32 = 0x005f_c108; pub const YKPIV_OBJ_FACIAL: u32 = 0x005f_c108;
pub const YKPIV_OBJ_PRINTED: u32 = 0x005f_c109; pub const YKPIV_OBJ_PRINTED: u32 = 0x005f_c109;
pub const YKPIV_OBJ_SIGNATURE: u32 = 0x005f_c10a; // cert for 9c key
pub const YKPIV_OBJ_KEY_MANAGEMENT: u32 = 0x005f_c10b; // cert for 9d key
pub const YKPIV_OBJ_CARD_AUTH: u32 = 0x005f_c101; // cert for 9e key
pub const YKPIV_OBJ_DISCOVERY: u32 = 0x7e; pub const YKPIV_OBJ_DISCOVERY: u32 = 0x7e;
pub const YKPIV_OBJ_KEY_HISTORY: u32 = 0x005f_c10c; pub const YKPIV_OBJ_KEY_HISTORY: u32 = 0x005f_c10c;
pub const YKPIV_OBJ_IRIS: u32 = 0x005f_c121; pub const YKPIV_OBJ_IRIS: u32 = 0x005f_c121;
pub const YKPIV_OBJ_RETIRED1: u32 = 0x005f_c10d;
pub const YKPIV_OBJ_RETIRED2: u32 = 0x005f_c10e;
pub const YKPIV_OBJ_RETIRED3: u32 = 0x005f_c10f;
pub const YKPIV_OBJ_RETIRED4: u32 = 0x005f_c110;
pub const YKPIV_OBJ_RETIRED5: u32 = 0x005f_c111;
pub const YKPIV_OBJ_RETIRED6: u32 = 0x005f_c112;
pub const YKPIV_OBJ_RETIRED7: u32 = 0x005f_c113;
pub const YKPIV_OBJ_RETIRED8: u32 = 0x005f_c114;
pub const YKPIV_OBJ_RETIRED9: u32 = 0x005f_c115;
pub const YKPIV_OBJ_RETIRED10: u32 = 0x005f_c116;
pub const YKPIV_OBJ_RETIRED11: u32 = 0x005f_c117;
pub const YKPIV_OBJ_RETIRED12: u32 = 0x005f_c118;
pub const YKPIV_OBJ_RETIRED13: u32 = 0x005f_c119;
pub const YKPIV_OBJ_RETIRED14: u32 = 0x005f_c11a;
pub const YKPIV_OBJ_RETIRED15: u32 = 0x005f_c11b;
pub const YKPIV_OBJ_RETIRED16: u32 = 0x005f_c11c;
pub const YKPIV_OBJ_RETIRED17: u32 = 0x005f_c11d;
pub const YKPIV_OBJ_RETIRED18: u32 = 0x005f_c11e;
pub const YKPIV_OBJ_RETIRED19: u32 = 0x005f_c11f;
pub const YKPIV_OBJ_RETIRED20: u32 = 0x005f_c120;
// Internal object IDs // Internal object IDs
pub const YKPIV_OBJ_ADMIN_DATA: u32 = 0x005f_ff00; pub const YKPIV_OBJ_ADMIN_DATA: u32 = 0x005f_ff00;
pub const YKPIV_OBJ_ATTESTATION: u32 = 0x005f_ff01;
pub const YKPIV_OBJ_MSCMAP: u32 = 0x005f_ff10; pub const YKPIV_OBJ_MSCMAP: u32 = 0x005f_ff10;
pub const YKPIV_OBJ_MSROOTS1: u32 = 0x005f_ff11; pub const YKPIV_OBJ_MSROOTS1: u32 = 0x005f_ff11;
pub const YKPIV_OBJ_MSROOTS2: u32 = 0x005f_ff12; pub const YKPIV_OBJ_MSROOTS2: u32 = 0x005f_ff12;
+3 -3
View File
@@ -158,7 +158,7 @@ impl Container {
Ok(Container { Ok(Container {
name, name,
slot: bytes[name_bytes_len], slot: bytes[name_bytes_len].try_into()?,
key_spec: bytes[name_bytes_len + 1], key_spec: bytes[name_bytes_len + 1],
key_size_bits: u16::from_le_bytes( key_size_bits: u16::from_le_bytes(
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)] bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
@@ -185,7 +185,7 @@ impl Container {
bytes.extend_from_slice(&self.name[i].to_le_bytes()); bytes.extend_from_slice(&self.name[i].to_le_bytes());
} }
bytes.push(self.slot); bytes.push(self.slot.into());
bytes.push(self.key_spec); bytes.push(self.key_spec);
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes()); bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
bytes.push(self.flags); bytes.push(self.flags);
@@ -204,7 +204,7 @@ impl Debug for Container {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"PivContainer {{ name: {:?}, slot: {}, key_spec: {}, key_size_bits: {}, \ "PivContainer {{ name: {:?}, slot: {:?}, key_spec: {}, key_size_bits: {}, \
flags: {}, pin_id: {}, associated_echd_container: {}, cert_fingerprint: {:?} }}", flags: {}, pin_id: {}, associated_echd_container: {}, cert_fingerprint: {:?} }}",
&self.name[..], &self.name[..],
self.slot, self.slot,
+1 -1
View File
@@ -68,7 +68,7 @@ pub enum Error {
/// Wrong PIN /// Wrong PIN
WrongPin { WrongPin {
/// Number of tries remaining /// Number of tries remaining
tries: u32, tries: u8,
}, },
/// Invalid object /// Invalid object
+269 -63
View File
@@ -38,68 +38,279 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{ use crate::{
apdu::{Ins, StatusWords},
certificate::{self, Certificate}, certificate::{self, Certificate},
consts::*, consts::*,
error::Error, error::Error,
response::StatusWords,
serialization::*, serialization::*,
settings, settings,
yubikey::YubiKey, yubikey::YubiKey,
AlgorithmId, ObjectId, Buffer, ObjectId,
}; };
use log::{debug, error, warn}; use log::{debug, error, warn};
use std::convert::TryFrom;
/// Slot identifiers. /// Slot identifiers.
/// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html> /// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html>
// TODO(tarcieri): replace these with enums #[derive(Clone, Copy, Debug, PartialEq)]
pub type SlotId = u8; pub enum SlotId {
/// This certificate and its associated private key is used to authenticate the card
/// and the cardholder. This slot is used for things like system login. The end user
/// PIN is required to perform any private key operations. Once the PIN has been
/// provided successfully, multiple private key operations may be performed without
/// additional cardholder consent.
Authentication,
/// Get the [`ObjectId`] that corresponds to a given [`SlotId`] /// This certificate and its associated private key is used for digital signatures for
// TODO(tarcieri): factor this into a slot ID enum /// the purpose of document signing, or signing files and executables. The end user
pub(crate) fn slot_object(slot: SlotId) -> Result<ObjectId, Error> { /// PIN is required to perform any private key operations. The PIN must be submitted
let id = match slot { /// every time immediately before a sign operation, to ensure cardholder participation
YKPIV_KEY_AUTHENTICATION => YKPIV_OBJ_AUTHENTICATION, /// for every digital signature generated.
YKPIV_KEY_SIGNATURE => YKPIV_OBJ_SIGNATURE, Signature,
YKPIV_KEY_KEYMGM => YKPIV_OBJ_KEY_MANAGEMENT,
YKPIV_KEY_CARDAUTH => YKPIV_OBJ_CARD_AUTH, /// This certificate and its associated private key is used for encryption for the
YKPIV_KEY_ATTESTATION => YKPIV_OBJ_ATTESTATION, /// purpose of confidentiality. This slot is used for things like encrypting e-mails
slot if slot >= YKPIV_KEY_RETIRED1 && (slot <= YKPIV_KEY_RETIRED20) => { /// or files. The end user PIN is required to perform any private key operations. Once
YKPIV_OBJ_RETIRED1 + (slot - YKPIV_KEY_RETIRED1) as u32 /// the PIN has been provided successfully, multiple private key operations may be
/// performed without additional cardholder consent.
KeyManagement,
/// This certificate and its associated private key is used to support additional
/// physical access applications, such as providing physical access to buildings via
/// PIV-enabled door locks. The end user PIN is NOT required to perform private key
/// operations for this slot.
CardAuthentication,
/// These slots are only available on the YubiKey 4 & 5. They are meant for previously
/// used Key Management keys to be able to decrypt earlier encrypted documents or
/// emails. In the YubiKey 4 & 5 all 20 of them are fully available for use.
Retired(RetiredSlotId),
/// This slot is only available on YubiKey version 4.3 and newer. It is only used for
/// attestation of other keys generated on device with instruction `f9`. This slot is
/// not cleared on reset, but can be overwritten.
Attestation,
} }
_ => return Err(Error::InvalidObject),
};
Ok(id) impl TryFrom<u8> for SlotId {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x9a => Ok(SlotId::Authentication),
0x9c => Ok(SlotId::Signature),
0x9d => Ok(SlotId::KeyManagement),
0x9e => Ok(SlotId::CardAuthentication),
0xf9 => Ok(SlotId::Attestation),
_ => RetiredSlotId::try_from(value).map(SlotId::Retired),
}
}
}
impl From<SlotId> for u8 {
fn from(slot: SlotId) -> u8 {
match slot {
SlotId::Authentication => 0x9a,
SlotId::Signature => 0x9c,
SlotId::KeyManagement => 0x9d,
SlotId::CardAuthentication => 0x9e,
SlotId::Retired(retired) => retired.into(),
SlotId::Attestation => 0xf9,
}
}
}
impl SlotId {
/// Returns the [`ObjectId`] that corresponds to a given [`SlotId`].
pub(crate) fn object_id(self) -> ObjectId {
match self {
SlotId::Authentication => 0x005f_c105,
SlotId::Signature => 0x005f_c10a,
SlotId::KeyManagement => 0x005f_c10b,
SlotId::CardAuthentication => 0x005f_c101,
SlotId::Retired(retired) => retired.object_id(),
SlotId::Attestation => 0x005f_ff01,
}
}
}
/// Retired slot IDs.
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RetiredSlotId {
R1,
R2,
R3,
R4,
R5,
R6,
R7,
R8,
R9,
R10,
R11,
R12,
R13,
R14,
R15,
R16,
R17,
R18,
R19,
R20,
}
impl TryFrom<u8> for RetiredSlotId {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x82 => Ok(RetiredSlotId::R1),
0x83 => Ok(RetiredSlotId::R2),
0x84 => Ok(RetiredSlotId::R3),
0x85 => Ok(RetiredSlotId::R4),
0x86 => Ok(RetiredSlotId::R5),
0x87 => Ok(RetiredSlotId::R6),
0x88 => Ok(RetiredSlotId::R7),
0x89 => Ok(RetiredSlotId::R8),
0x8a => Ok(RetiredSlotId::R9),
0x8b => Ok(RetiredSlotId::R10),
0x8c => Ok(RetiredSlotId::R11),
0x8d => Ok(RetiredSlotId::R12),
0x8e => Ok(RetiredSlotId::R13),
0x8f => Ok(RetiredSlotId::R14),
0x90 => Ok(RetiredSlotId::R15),
0x91 => Ok(RetiredSlotId::R16),
0x92 => Ok(RetiredSlotId::R17),
0x93 => Ok(RetiredSlotId::R18),
0x94 => Ok(RetiredSlotId::R19),
0x95 => Ok(RetiredSlotId::R20),
_ => Err(Error::InvalidObject),
}
}
}
impl From<RetiredSlotId> for u8 {
fn from(slot: RetiredSlotId) -> u8 {
match slot {
RetiredSlotId::R1 => 0x82,
RetiredSlotId::R2 => 0x83,
RetiredSlotId::R3 => 0x84,
RetiredSlotId::R4 => 0x85,
RetiredSlotId::R5 => 0x86,
RetiredSlotId::R6 => 0x87,
RetiredSlotId::R7 => 0x88,
RetiredSlotId::R8 => 0x89,
RetiredSlotId::R9 => 0x8a,
RetiredSlotId::R10 => 0x8b,
RetiredSlotId::R11 => 0x8c,
RetiredSlotId::R12 => 0x8d,
RetiredSlotId::R13 => 0x8e,
RetiredSlotId::R14 => 0x8f,
RetiredSlotId::R15 => 0x90,
RetiredSlotId::R16 => 0x91,
RetiredSlotId::R17 => 0x92,
RetiredSlotId::R18 => 0x93,
RetiredSlotId::R19 => 0x94,
RetiredSlotId::R20 => 0x95,
}
}
}
impl RetiredSlotId {
/// Returns the [`ObjectId`] that corresponds to a given [`RetiredSlotId`].
pub(crate) fn object_id(self) -> ObjectId {
match self {
RetiredSlotId::R1 => 0x005f_c10d,
RetiredSlotId::R2 => 0x005f_c10e,
RetiredSlotId::R3 => 0x005f_c10f,
RetiredSlotId::R4 => 0x005f_c110,
RetiredSlotId::R5 => 0x005f_c111,
RetiredSlotId::R6 => 0x005f_c112,
RetiredSlotId::R7 => 0x005f_c113,
RetiredSlotId::R8 => 0x005f_c114,
RetiredSlotId::R9 => 0x005f_c115,
RetiredSlotId::R10 => 0x005f_c116,
RetiredSlotId::R11 => 0x005f_c117,
RetiredSlotId::R12 => 0x005f_c118,
RetiredSlotId::R13 => 0x005f_c119,
RetiredSlotId::R14 => 0x005f_c11a,
RetiredSlotId::R15 => 0x005f_c11b,
RetiredSlotId::R16 => 0x005f_c11c,
RetiredSlotId::R17 => 0x005f_c11d,
RetiredSlotId::R18 => 0x005f_c11e,
RetiredSlotId::R19 => 0x005f_c11f,
RetiredSlotId::R20 => 0x005f_c120,
}
}
} }
/// Personal Identity Verification (PIV) key slots /// Personal Identity Verification (PIV) key slots
pub const SLOTS: [u8; 24] = [ pub const SLOTS: [SlotId; 24] = [
YKPIV_KEY_AUTHENTICATION, SlotId::Authentication,
YKPIV_KEY_SIGNATURE, SlotId::Signature,
YKPIV_KEY_KEYMGM, SlotId::KeyManagement,
YKPIV_KEY_RETIRED1, SlotId::Retired(RetiredSlotId::R1),
YKPIV_KEY_RETIRED2, SlotId::Retired(RetiredSlotId::R2),
YKPIV_KEY_RETIRED3, SlotId::Retired(RetiredSlotId::R3),
YKPIV_KEY_RETIRED4, SlotId::Retired(RetiredSlotId::R4),
YKPIV_KEY_RETIRED5, SlotId::Retired(RetiredSlotId::R5),
YKPIV_KEY_RETIRED6, SlotId::Retired(RetiredSlotId::R6),
YKPIV_KEY_RETIRED7, SlotId::Retired(RetiredSlotId::R7),
YKPIV_KEY_RETIRED8, SlotId::Retired(RetiredSlotId::R8),
YKPIV_KEY_RETIRED9, SlotId::Retired(RetiredSlotId::R9),
YKPIV_KEY_RETIRED10, SlotId::Retired(RetiredSlotId::R10),
YKPIV_KEY_RETIRED11, SlotId::Retired(RetiredSlotId::R11),
YKPIV_KEY_RETIRED12, SlotId::Retired(RetiredSlotId::R12),
YKPIV_KEY_RETIRED13, SlotId::Retired(RetiredSlotId::R13),
YKPIV_KEY_RETIRED14, SlotId::Retired(RetiredSlotId::R14),
YKPIV_KEY_RETIRED15, SlotId::Retired(RetiredSlotId::R15),
YKPIV_KEY_RETIRED16, SlotId::Retired(RetiredSlotId::R16),
YKPIV_KEY_RETIRED17, SlotId::Retired(RetiredSlotId::R17),
YKPIV_KEY_RETIRED18, SlotId::Retired(RetiredSlotId::R18),
YKPIV_KEY_RETIRED19, SlotId::Retired(RetiredSlotId::R19),
YKPIV_KEY_RETIRED20, SlotId::Retired(RetiredSlotId::R20),
YKPIV_KEY_CARDAUTH, SlotId::CardAuthentication,
]; ];
/// Algorithm identifiers
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlgorithmId {
/// 1024-bit RSA.
Rsa1024,
/// 2048-bit RSA.
Rsa2048,
/// ECDSA with the NIST P256 curve.
EccP256,
/// ECDSA with the NIST P384 curve.
EccP384,
}
impl TryFrom<u8> for AlgorithmId {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x06 => Ok(AlgorithmId::Rsa1024),
0x07 => Ok(AlgorithmId::Rsa2048),
0x11 => Ok(AlgorithmId::EccP256),
0x14 => Ok(AlgorithmId::EccP384),
_ => Err(Error::AlgorithmError),
}
}
}
impl From<AlgorithmId> for u8 {
fn from(id: AlgorithmId) -> u8 {
match id {
AlgorithmId::Rsa1024 => 0x06,
AlgorithmId::Rsa2048 => 0x07,
AlgorithmId::EccP256 => 0x11,
AlgorithmId::EccP384 => 0x14,
}
}
}
/// PIV cryptographic keys stored in a YubiKey /// PIV cryptographic keys stored in a YubiKey
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Key { pub struct Key {
@@ -120,14 +331,16 @@ impl Key {
let buf = match certificate::read_certificate(&txn, slot) { let buf = match certificate::read_certificate(&txn, slot) {
Ok(b) => b, Ok(b) => b,
Err(e) => { Err(e) => {
debug!("error reading certificate in slot {}: {}", slot, e); debug!("error reading certificate in slot {:?}: {}", slot, e);
continue; continue;
} }
}; };
if !buf.is_empty() {
let cert = Certificate::new(buf)?; let cert = Certificate::new(buf)?;
keys.push(Key { slot, cert }); keys.push(Key { slot, cert });
} }
}
Ok(keys) Ok(keys)
} }
@@ -199,13 +412,15 @@ pub fn generate(
touch_policy: u8, touch_policy: u8,
) -> Result<GeneratedKey, Error> { ) -> Result<GeneratedKey, Error> {
let mut in_data = [0u8; 11]; let mut in_data = [0u8; 11];
let mut templ = [0, YKPIV_INS_GENERATE_ASYMMETRIC, 0, 0]; let mut templ = [0, Ins::GenerateAsymmetric.code(), 0, 0];
let setting_roca: settings::BoolValue; let setting_roca: settings::BoolValue;
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
if yubikey.device_model() == DEVTYPE_YK4 if yubikey.device_model() == DEVTYPE_YK4
&& (algorithm == YKPIV_ALGO_RSA1024 || algorithm == YKPIV_ALGO_RSA2048)
&& yubikey.version.major == 4 && yubikey.version.major == 4
&& (yubikey.version.minor < 3 || yubikey.version.minor == 3 && (yubikey.version.patch < 5)) && (yubikey.version.minor < 3
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
{ {
setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true); setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true);
@@ -239,18 +454,13 @@ pub fn generate(
return Err(Error::NotSupported); 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()?; let txn = yubikey.begin_transaction()?;
templ[3] = slot; templ[3] = slot.into();
let mut offset = 5; let mut offset = 5;
in_data[..offset].copy_from_slice(&[ in_data[..offset].copy_from_slice(&[
@@ -258,7 +468,7 @@ pub fn generate(
3, // length sans this 2-byte header 3, // length sans this 2-byte header
YKPIV_ALGO_TAG, YKPIV_ALGO_TAG,
1, 1,
algorithm, algorithm.into(),
]); ]);
if in_data[4] == 0 { if in_data[4] == 0 {
@@ -309,10 +519,10 @@ pub fn generate(
} }
} }
let data = response.into_buffer(); let data = Buffer::new(response.data().into());
match algorithm { match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => { AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
let mut offset = 5; let mut offset = 5;
let mut len = 0; let mut len = 0;
@@ -340,10 +550,10 @@ pub fn generate(
exp, exp,
}) })
} }
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => { AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let mut offset = 3; let mut offset = 3;
let len = if algorithm == YKPIV_ALGO_ECCP256 { let len = if let AlgorithmId::EccP256 = algorithm {
CB_ECC_POINTP256 CB_ECC_POINTP256
} else { } else {
CB_ECC_POINTP384 CB_ECC_POINTP384
@@ -367,9 +577,5 @@ pub fn generate(
let point = data[offset..(offset + len)].to_vec(); let point = data[offset..(offset + len)].to_vec();
Ok(GeneratedKey::Ecc { algorithm, point }) Ok(GeneratedKey::Ecc { algorithm, point })
} }
_ => {
error!("wrong algorithm");
Err(Error::AlgorithmError)
}
} }
} }
+5 -7
View File
@@ -123,8 +123,9 @@
#![doc( #![doc(
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png", html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
html_root_url = "https://docs.rs/yubikey-piv/0.0.2" html_root_url = "https://docs.rs/yubikey-piv/0.0.3"
)] )]
#![forbid(unsafe_code)]
#![warn( #![warn(
missing_docs, missing_docs,
rust_2018_idioms, rust_2018_idioms,
@@ -155,7 +156,7 @@ mod metadata;
pub mod mgm; pub mod mgm;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub mod msroots; pub mod msroots;
mod response; pub mod readers;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
mod serialization; mod serialization;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
@@ -163,13 +164,10 @@ pub mod settings;
mod transaction; mod transaction;
pub mod yubikey; pub mod yubikey;
pub use self::{readers::Readers, yubikey::YubiKey};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub use self::{key::Key, mgm::MgmKey}; pub use self::{key::Key, mgm::MgmKey};
pub use yubikey::YubiKey;
/// Algorithm identifiers
// TODO(tarcieri): make this an enum
pub type AlgorithmId = u8;
/// Object identifiers /// Object identifiers
pub type ObjectId = u32; pub type ObjectId = u32;
+48 -85
View File
@@ -31,49 +31,37 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{consts::*, error::Error, serialization::*, transaction::Transaction, Buffer}; use crate::{consts::*, error::Error, serialization::*, transaction::Transaction, Buffer};
use std::{ptr, slice};
use zeroize::Zeroizing; use zeroize::Zeroizing;
/// Get metadata item /// Get metadata item
pub(crate) fn get_item(data: &[u8], tag: u8) -> Result<&[u8], Error> { 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 cb_temp: usize = 0;
let mut tag_temp: u8; let mut offset = 0;
unsafe { while offset < data.len() {
while p_temp < data.as_ptr().add(data.len()) { let tag_temp = data[offset];
tag_temp = *p_temp; offset += 1;
p_temp = p_temp.add(1);
let p_slice = slice::from_raw_parts( if !has_valid_length(&data[offset..], data.len() - 1) {
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); return Err(Error::SizeError);
} }
p_temp = p_temp.add(get_length(p_slice, &mut cb_temp)); offset += get_length(&data[offset..], &mut cb_temp);
if tag_temp == tag { if tag_temp == tag {
// found tag // found tag
break; break;
} }
p_temp = p_temp.add(cb_temp); offset += cb_temp;
}
if p_temp < data.as_ptr().add(data.len()) {
return Ok(slice::from_raw_parts(p_temp, cb_temp));
}
} }
if offset < data.len() {
Ok(&data[offset..offset + cb_temp])
} else {
Err(Error::GenericError) Err(Error::GenericError)
} }
}
/// Set metadata item /// Set metadata item
pub(crate) fn set_item( pub(crate) fn set_item(
@@ -83,35 +71,25 @@ pub(crate) fn set_item(
tag: u8, tag: u8,
p_item: &[u8], p_item: &[u8],
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut p_temp: *mut u8 = data.as_mut_ptr();
let mut cb_temp: usize = 0; let mut cb_temp: usize = 0;
let mut tag_temp: u8 = 0; let mut tag_temp: u8 = 0;
let mut cb_len: usize = 0; let mut cb_len: usize = 0;
let cb_item = p_item.len(); 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() { let mut offset = 0;
unsafe {
tag_temp = *p_temp;
p_temp = p_temp.add(1);
cb_len = get_length( while offset < *pcb_data {
slice::from_raw_parts( tag_temp = data[offset];
p_temp, offset += 1;
data.as_mut_ptr() as usize + data.len() - p_temp as usize,
), cb_len = get_length(&data[offset..], &mut cb_temp);
&mut cb_temp, offset += cb_len;
);
p_temp = p_temp.add(cb_len);
if tag_temp == tag { if tag_temp == tag {
break; break;
} }
p_temp = p_temp.add(cb_temp); offset += cb_temp;
}
} }
if tag_temp != tag { if tag_temp != tag {
@@ -120,75 +98,63 @@ pub(crate) fn set_item(
return Ok(()); return Ok(());
} }
unsafe { // We did not find an existing tag, append
p_temp = data.as_mut_ptr().add(*pcb_data); offset = *pcb_data;
cb_len = get_length_size(cb_item); cb_len = get_length_size(cb_item);
// If length would cause buffer overflow, return error
if (*pcb_data + cb_len + cb_item) > cb_data_max { if (*pcb_data + cb_len + cb_item) > cb_data_max {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
*p_temp = tag; data[offset] = tag;
p_temp = p_temp.add(1); offset += 1;
p_temp = p_temp.add(set_length( offset += set_length(&mut data[offset..], cb_item);
slice::from_raw_parts_mut( data[offset..offset + cb_item].copy_from_slice(p_item);
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; *pcb_data += 1 + cb_len + cb_item;
return Ok(()); return Ok(());
} }
if cb_temp == cb_item { // Found tag
unsafe {
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
}
// Check length, if it matches, overwrite
if cb_temp == cb_item {
data[offset..offset + cb_item].copy_from_slice(p_item);
return Ok(()); return Ok(());
} }
p_next = unsafe { p_temp.add(cb_temp) }; // Length doesn't match, expand/shrink to fit
cb_moved = (cb_item as isize - cb_temp as isize) let next_offset = offset + cb_temp;
// Must be signed to have negative offsets
let cb_moved: isize = (cb_item as isize - cb_temp as isize)
+ if cb_item != 0 { + if cb_item != 0 {
get_length_size(cb_item) as isize get_length_size(cb_item) as isize
} else { } else {
// For tag, if deleting
-1 -1
} }
// Accounts for different length encoding
- cb_len as isize; - cb_len as isize;
if (*pcb_data + cb_moved as usize) > cb_data_max { // If length would cause buffer overflow, return error
if (*pcb_data as isize + cb_moved) as usize > cb_data_max {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
unsafe { // Move remaining data
ptr::copy( data.copy_within(
p_next, next_offset..*pcb_data,
p_next.offset(cb_moved), (next_offset as isize + cb_moved) as usize,
*pcb_data - p_next as usize - data.as_ptr() as usize,
); );
} *pcb_data = (*pcb_data as isize + cb_moved) as usize;
*pcb_data += cb_moved as usize;
// Re-encode item and insert
if cb_item != 0 { if cb_item != 0 {
unsafe { offset -= cb_len;
p_temp = p_temp.offset(-(cb_len as isize)); offset += set_length(&mut data[offset..], cb_item);
p_temp = p_temp.add(set_length( data[offset..offset + cb_item].copy_from_slice(p_item);
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(()) Ok(())
@@ -219,10 +185,7 @@ pub(crate) fn read(txn: &Transaction<'_>, tag: u8) -> Result<Buffer, Error> {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
unsafe { data.copy_within(offset..offset + pcb_data, 0);
ptr::copy(data.as_ptr().add(offset), data.as_mut_ptr(), pcb_data);
}
data.truncate(pcb_data); data.truncate(pcb_data);
Ok(data) Ok(data)
} }
+15 -37
View File
@@ -39,7 +39,6 @@
use crate::{consts::*, error::Error, serialization::*, yubikey::YubiKey}; use crate::{consts::*, error::Error, serialization::*, yubikey::YubiKey};
use log::error; use log::error;
use std::{ptr, slice};
/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots /// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots
pub struct MsRoots(Vec<u8>); pub struct MsRoots(Vec<u8>);
@@ -51,33 +50,22 @@ impl MsRoots {
} }
/// Read `msroots` file from YubiKey /// Read `msroots` file from YubiKey
pub fn read(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> { pub fn read(yubikey: &mut YubiKey) -> Result<Option<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 cb_data = yubikey.obj_size_max();
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
// allocate first page // allocate first page
let mut p_data = vec![0u8; cb_data]; let mut data = Vec::with_capacity(cb_data);
for object_id in YKPIV_OBJ_MSROOTS1..YKPIV_OBJ_MSROOTS5 { for object_id in YKPIV_OBJ_MSROOTS1..YKPIV_OBJ_MSROOTS5 {
let mut buf = txn.fetch_object(object_id)?; let buf = txn.fetch_object(object_id)?;
let cb_buf = buf.len(); let cb_buf = buf.len();
ptr = buf.as_mut_ptr();
if cb_buf < CB_OBJ_TAG_MIN { if cb_buf < CB_OBJ_TAG_MIN {
return Ok(results); return Ok(None);
} }
unsafe { let tag = buf[0];
tag = *ptr;
ptr = ptr.add(1);
}
if (TAG_MSROOTS_MID != tag || YKPIV_OBJ_MSROOTS5 == object_id) if (TAG_MSROOTS_MID != tag || YKPIV_OBJ_MSROOTS5 == object_id)
&& (TAG_MSROOTS_END != tag) && (TAG_MSROOTS_END != tag)
@@ -85,38 +73,28 @@ impl MsRoots {
// the current object doesn't contain a valid part of a msroots file // the current object doesn't contain a valid part of a msroots file
// treat condition as object isn't found // treat condition as object isn't found
return Ok(results); return Ok(None);
} }
unsafe { let mut len: usize = 0;
ptr = ptr.add(get_length( let offset = 1 + get_length(&buf[1..], &mut len);
slice::from_raw_parts(ptr, buf.as_ptr() as usize + buf.len() - ptr as usize),
&mut len,
));
}
// check that decoded length represents object contents // check that decoded length represents object contents
if len > cb_buf - (ptr as isize - buf.as_mut_ptr() as isize) as usize { if len > cb_buf - offset {
return Ok(results); return Ok(None);
} }
unsafe { data.extend_from_slice(&buf[offset..offset + len]);
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 { if tag == TAG_MSROOTS_END {
break; break;
} }
} }
Ok(results) MsRoots::new(&data).map(Some).map_err(|e| {
error!("error parsing msroots: {:?}", e);
e
})
} }
/// Write `msroots` file to YubiKey /// Write `msroots` file to YubiKey
+89
View File
@@ -0,0 +1,89 @@
//! Support for enumerating available readers
use crate::{error::Error, yubikey::YubiKey};
use std::{
borrow::Cow,
convert::TryInto,
ffi::CStr,
sync::{Arc, Mutex},
};
/// Iterator over connected readers
pub type Iter<'ctx> = std::vec::IntoIter<Reader<'ctx>>;
/// Enumeration support for available readers
pub struct Readers {
/// PC/SC context
ctx: Arc<Mutex<pcsc::Context>>,
/// Buffer for storing reader names
reader_names: Vec<u8>,
}
impl Readers {
/// Open a PC/SC context, which can be used to enumerate available PC/SC
/// readers (which can be used to connect to YubiKeys).
pub fn open() -> Result<Self, Error> {
let ctx = pcsc::Context::establish(pcsc::Scope::System)?;
let reader_names = vec![0u8; ctx.list_readers_len()?];
Ok(Self {
ctx: Arc::new(Mutex::new(ctx)),
reader_names,
})
}
/// Iterate over the available readers
pub fn iter(&mut self) -> Result<Iter<'_>, Error> {
let Self { ctx, reader_names } = self;
let reader_cstrs: Vec<_> = {
let c = ctx.lock().unwrap();
// ensure PC/SC context is valid
c.is_valid()?;
c.list_readers(reader_names)?.collect()
};
let readers: Vec<_> = reader_cstrs
.iter()
.map(|name| Reader::new(name, Arc::clone(ctx)))
.collect();
Ok(readers.into_iter())
}
}
/// An individual connected reader
pub struct Reader<'ctx> {
/// Name of this reader
name: &'ctx CStr,
/// PC/SC context
ctx: Arc<Mutex<pcsc::Context>>,
}
impl<'ctx> Reader<'ctx> {
/// Create a new reader from its name and context
fn new(name: &'ctx CStr, ctx: Arc<Mutex<pcsc::Context>>) -> Self {
// TODO(tarcieri): open devices, determine they're YubiKeys, get serial?
Self { name, ctx }
}
/// Get this reader's name
pub fn name(&self) -> Cow<'_, str> {
// TODO(tarcieri): is lossy ok here? try to avoid lossiness?
self.name.to_string_lossy()
}
/// Open a connection to this reader, returning a `YubiKey` if successful
pub fn open(&self) -> Result<YubiKey, Error> {
self.try_into()
}
/// Connect to this reader, returning its `pcsc::Card`
pub(crate) fn connect(&self) -> Result<pcsc::Card, Error> {
let ctx = self.ctx.lock().unwrap();
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
}
}
-186
View File
@@ -1,186 +0,0 @@
//! 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()
}
}
+66 -96
View File
@@ -1,17 +1,22 @@
//! YubiKey PC/SC transactions //! YubiKey PC/SC transactions
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*, Buffer}; use crate::{
apdu::{Ins, APDU},
error::Error,
yubikey::*,
};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use crate::{ use crate::{
apdu::{Response, StatusWords},
consts::*,
key::{AlgorithmId, SlotId},
mgm::MgmKey, mgm::MgmKey,
response::{Response, StatusWords},
serialization::*, serialization::*,
ObjectId, Buffer, ObjectId,
}; };
use log::{error, trace}; use log::{error, trace};
use std::convert::TryInto; use std::convert::TryInto;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use std::ptr;
use zeroize::Zeroizing; use zeroize::Zeroizing;
/// Exclusive transaction with the YubiKey's PC/SC card. /// Exclusive transaction with the YubiKey's PC/SC card.
@@ -43,10 +48,10 @@ impl<'tx> Transaction<'tx> {
/// single APDU messages at a time. For larger messages that need to be /// single APDU messages at a time. For larger messages that need to be
/// split into multiple APDUs, use the [`Transaction::transfer_data`] /// split into multiple APDUs, use the [`Transaction::transfer_data`]
/// method instead. /// method instead.
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Buffer, Error> { pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Vec<u8>, Error> {
trace!(">>> {:?}", send_buffer); trace!(">>> {:?}", send_buffer);
let mut recv_buffer = Zeroizing::new(vec![0u8; recv_len]); let mut recv_buffer = vec![0u8; recv_len];
let len = self let len = self
.inner .inner
@@ -54,15 +59,12 @@ impl<'tx> Transaction<'tx> {
.len(); .len();
recv_buffer.truncate(len); recv_buffer.truncate(len);
trace!("<<< {:?}", recv_buffer.as_slice());
Ok(recv_buffer) Ok(recv_buffer)
} }
/// Select application. /// Select application.
pub fn select_application(&self) -> Result<(), Error> { pub fn select_application(&self) -> Result<(), Error> {
let response = APDU::new(YKPIV_INS_SELECT_APPLICATION) let response = APDU::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(&AID) .data(&AID)
.transmit(self, 0xFF) .transmit(self, 0xFF)
@@ -85,21 +87,17 @@ impl<'tx> Transaction<'tx> {
/// Get the version of the PIV application installed on the YubiKey /// Get the version of the PIV application installed on the YubiKey
pub fn get_version(&self) -> Result<Version, Error> { pub fn get_version(&self) -> Result<Version, Error> {
// get version from device // get version from device
let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?; let response = APDU::new(Ins::GetVersion).transmit(self, 261)?;
if !response.is_success() { if !response.is_success() {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
if response.buffer().len() < 3 { if response.data().len() < 3 {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
Ok(Version { Ok(Version::new(response.data()[..3].try_into().unwrap()))
major: response.buffer()[0],
minor: response.buffer()[1],
patch: response.buffer()[2],
})
} }
/// Get YubiKey device serial number /// Get YubiKey device serial number
@@ -108,7 +106,7 @@ impl<'tx> Transaction<'tx> {
let response = if version.major < 5 { let response = if version.major < 5 {
// get serial from neo/yk4 devices using the otp applet // get serial from neo/yk4 devices using the otp applet
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION) let sw = APDU::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(&yk_applet) .data(&yk_applet)
.transmit(self, 0xFF)? .transmit(self, 0xFF)?
@@ -130,7 +128,7 @@ impl<'tx> Transaction<'tx> {
} }
// reselect the PIV applet // reselect the PIV applet
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION) let sw = APDU::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(&AID) .data(&AID)
.transmit(self, 0xFF)? .transmit(self, 0xFF)?
@@ -144,7 +142,7 @@ impl<'tx> Transaction<'tx> {
resp resp
} else { } else {
// get serial from yk5 and later devices using the f8 command // get serial from yk5 and later devices using the f8 command
let resp = APDU::new(YKPIV_INS_GET_SERIAL).transmit(self, 0xFF)?; let resp = APDU::new(Ins::GetSerial).transmit(self, 0xFF)?;
if !resp.is_success() { if !resp.is_success() {
error!( error!(
@@ -157,7 +155,7 @@ impl<'tx> Transaction<'tx> {
resp resp
}; };
response.buffer()[..4] response.data()[..4]
.try_into() .try_into()
.map(|serial| Serial::from(u32::from_be_bytes(serial))) .map(|serial| Serial::from(u32::from_be_bytes(serial)))
.map_err(|_| Error::SizeError) .map_err(|_| Error::SizeError)
@@ -166,20 +164,28 @@ impl<'tx> Transaction<'tx> {
/// Verify device PIN. /// Verify device PIN.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> { 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 {
if pin.len() != CB_PIN_MAX {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
let response = APDU::new(YKPIV_INS_VERIFY) let mut query = APDU::new(Ins::Verify);
.params(0x00, 0x80) query.params(0x00, 0x80);
.data(pin)
.transmit(self, 261)?; // Empty pin means we are querying the number of retries. We set no data in this
// case; if we instead sent [0xff; CB_PIN_MAX] it would count as an attempt and
// decrease the retry counter.
if !pin.is_empty() {
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
data[0..pin.len()].copy_from_slice(pin);
query.data(data.as_ref());
}
let response = query.transmit(self, 261)?;
match response.status_words() { match response.status_words() {
StatusWords::Success => Ok(()), StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }), StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }), StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
_ => Err(Error::GenericError), _ => Err(Error::GenericError),
} }
} }
@@ -187,44 +193,21 @@ impl<'tx> Transaction<'tx> {
/// Change the PIN /// Change the PIN
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> { 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 templ = [0, Ins::ChangeReference.code(), 0, 0x80];
let mut indata = Zeroizing::new([0u8; 16]);
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX { if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
if action == CHREF_ACT_UNBLOCK_PIN { if action == CHREF_ACT_UNBLOCK_PIN {
templ[1] = YKPIV_INS_RESET_RETRY; templ[1] = Ins::ResetRetry.code();
} else if action == CHREF_ACT_CHANGE_PUK { } else if action == CHREF_ACT_CHANGE_PUK {
templ[3] = 0x81; templ[3] = 0x81;
} }
unsafe { let mut indata = Zeroizing::new([0xff; CB_PIN_MAX * 2]);
ptr::copy(current_pin.as_ptr(), indata.as_mut_ptr(), current_pin.len()); indata[0..current_pin.len()].copy_from_slice(current_pin);
indata[CB_PIN_MAX..CB_PIN_MAX + new_pin.len()].copy_from_slice(new_pin);
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 let status_words = self
.transfer_data(&templ, indata.as_ref(), 0xFF)? .transfer_data(&templ, indata.as_ref(), 0xFF)?
@@ -233,7 +216,7 @@ impl<'tx> Transaction<'tx> {
match status_words { match status_words {
StatusWords::Success => Ok(()), StatusWords::Success => Ok(()),
StatusWords::AuthBlockedError => Err(Error::PinLocked), StatusWords::AuthBlockedError => Err(Error::PinLocked),
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }), StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
_ => { _ => {
error!( error!(
"failed changing pin, token response code: {:x}.", "failed changing pin, token response code: {:x}.",
@@ -261,7 +244,7 @@ impl<'tx> Transaction<'tx> {
data[2] = DES_LEN_3DES as u8; data[2] = DES_LEN_3DES as u8;
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref()); data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
let status_words = APDU::new(YKPIV_INS_SET_MGMKEY) let status_words = APDU::new(Ins::SetMgmKey)
.params(0xff, p2) .params(0xff, p2)
.data(&data) .data(&data)
.transmit(self, 261)? .transmit(self, 261)?
@@ -284,20 +267,18 @@ impl<'tx> Transaction<'tx> {
pub(crate) fn authenticated_command( pub(crate) fn authenticated_command(
&self, &self,
sign_in: &[u8], sign_in: &[u8],
out: &mut [u8], algorithm: AlgorithmId,
out_len: &mut usize, key: SlotId,
algorithm: u8,
key: u8,
decipher: bool, decipher: bool,
) -> Result<(), Error> { ) -> Result<Buffer, Error> {
let in_len = sign_in.len(); let in_len = sign_in.len();
let mut indata = [0u8; 1024]; let mut indata = [0u8; 1024];
let templ = [0, YKPIV_INS_AUTHENTICATE, algorithm, key]; let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
let mut len: usize = 0; let mut len: usize = 0;
match algorithm { match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => { AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
let key_len = if algorithm == YKPIV_ALGO_RSA1024 { let key_len = if let AlgorithmId::Rsa1024 = algorithm {
128 128
} else { } else {
256 256
@@ -307,8 +288,8 @@ impl<'tx> Transaction<'tx> {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
} }
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => { AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let key_len = if algorithm == YKPIV_ALGO_ECCP256 { let key_len = if let AlgorithmId::EccP256 = algorithm {
32 32
} else { } else {
48 48
@@ -319,7 +300,6 @@ impl<'tx> Transaction<'tx> {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
} }
_ => return Err(Error::AlgorithmError),
} }
let bytes = if in_len < 0x80 { let bytes = if in_len < 0x80 {
@@ -334,11 +314,9 @@ impl<'tx> Transaction<'tx> {
let mut offset = 1 + set_length(&mut indata[1..], in_len + bytes + 3); let mut offset = 1 + set_length(&mut indata[1..], in_len + bytes + 3);
indata[offset] = 0x82; indata[offset] = 0x82;
indata[offset + 1] = 0x00; indata[offset + 1] = 0x00;
indata[offset + 2] = indata[offset + 2] = match (algorithm, decipher) {
if (algorithm == YKPIV_ALGO_ECCP256 || algorithm == YKPIV_ALGO_ECCP384) && decipher { (AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
0x85 _ => 0x81,
} else {
0x81
}; };
offset += 3; offset += 3;
@@ -363,7 +341,7 @@ impl<'tx> Transaction<'tx> {
} }
} }
let data = response.buffer(); let data = response.data();
// skip the first 7c tag // skip the first 7c tag
if data[0] != 0x7c { if data[0] != 0x7c {
@@ -381,15 +359,7 @@ impl<'tx> Transaction<'tx> {
offset += 1; offset += 1;
offset += get_length(&data[offset..], &mut len); offset += get_length(&data[offset..], &mut len);
Ok(Buffer::new(data[offset..(offset + len)].into()))
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 /// Send/receive large amounts of data to/from the YubiKey, splitting long
@@ -404,7 +374,7 @@ impl<'tx> Transaction<'tx> {
max_out: usize, max_out: usize,
) -> Result<Response, Error> { ) -> Result<Response, Error> {
let mut in_offset = 0; let mut in_offset = 0;
let mut out_data = Zeroizing::new(vec![]); let mut out_data = vec![];
let mut sw = 0; let mut sw = 0;
loop { loop {
@@ -432,17 +402,17 @@ impl<'tx> Transaction<'tx> {
sw = response.status_words().code(); sw = response.status_words().code();
if out_data.len() - response.buffer().len() - 2 > max_out { if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
error!( error!(
"output buffer too small: wanted to write {}, max was {}", "output buffer too small: wanted to write {}, max was {}",
out_data.len() - response.buffer().len() - 2, out_data.len() - response.data().len(),
max_out max_out
); );
return Err(Error::SizeError); return Err(Error::SizeError);
} }
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]); out_data.extend_from_slice(&response.data()[..response.data().len()]);
in_offset += this_size; in_offset += this_size;
if in_offset >= in_data.len() { if in_offset >= in_data.len() {
@@ -456,24 +426,24 @@ impl<'tx> Transaction<'tx> {
sw & 0xff sw & 0xff
); );
let response = APDU::new(YKPIV_INS_GET_RESPONSE_APDU).transmit(self, 261)?; let response = APDU::new(Ins::GetResponseApdu).transmit(self, 261)?;
sw = response.status_words().code(); sw = response.status_words().code();
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) { if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
return Ok(Response::new(sw.into(), Zeroizing::new(vec![]))); return Ok(Response::new(sw.into(), vec![]));
} }
if out_data.len() + response.buffer().len() - 2 > max_out { if out_data.len() + response.data().len() > max_out {
error!( error!(
"output buffer too small: wanted to write {}, max was {}", "output buffer too small: wanted to write {}, max was {}",
out_data.len() + response.buffer().len() - 2, out_data.len() + response.data().len(),
max_out max_out
); );
return Err(Error::SizeError); return Err(Error::SizeError);
} }
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]); out_data.extend_from_slice(&response.data()[..response.data().len()]);
} }
Ok(Response::new(sw.into(), out_data)) Ok(Response::new(sw.into(), out_data))
@@ -483,7 +453,7 @@ impl<'tx> Transaction<'tx> {
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> { pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
let mut indata = [0u8; 5]; let mut indata = [0u8; 5];
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff]; let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
let mut inlen = indata.len(); let mut inlen = indata.len();
let indata_remaining = set_object(object_id, &mut indata); let indata_remaining = set_object(object_id, &mut indata);
@@ -495,7 +465,7 @@ impl<'tx> Transaction<'tx> {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
let data = response.into_buffer(); let data = Buffer::new(response.data().into());
let mut outlen = 0; let mut outlen = 0;
if data.len() < 2 || !has_valid_length(&data[1..], data.len() - 1) { if data.len() < 2 || !has_valid_length(&data[1..], data.len() - 1) {
@@ -526,7 +496,7 @@ impl<'tx> Transaction<'tx> {
/// Save an object /// Save an object
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> { pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff]; let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
// TODO(tarcieri): replace with vector // TODO(tarcieri): replace with vector
let mut data = [0u8; CB_BUF_MAX]; let mut data = [0u8; CB_BUF_MAX];
+197 -264
View File
@@ -35,19 +35,32 @@
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use crate::{ use crate::{
apdu::APDU, key::SlotId, metadata, mgm::MgmKey, response::StatusWords, serialization::*, apdu::{Ins, StatusWords, APDU},
ObjectId, key::{AlgorithmId, SlotId},
metadata,
mgm::MgmKey,
serialization::*,
Buffer, ObjectId,
};
use crate::{
consts::*,
error::Error,
readers::{Reader, Readers},
transaction::Transaction,
}; };
use crate::{consts::*, error::Error, transaction::Transaction, Buffer};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use getrandom::getrandom; use getrandom::getrandom;
use log::{error, info, warn}; use log::{error, info, warn};
use pcsc::{Card, Context}; use pcsc::Card;
use std::fmt::{self, Display}; #[cfg(feature = "untested")]
use secrecy::ExposeSecret;
use std::{
convert::TryFrom,
fmt::{self, Display},
};
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use std::{ use std::{
convert::TryInto, convert::TryInto,
ptr, slice,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
@@ -60,6 +73,9 @@ pub const AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html> /// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
pub const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17]; pub const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
/// Cached YubiKey PIN
pub type CachedPin = secrecy::SecretVec<u8>;
/// YubiKey Serial Number /// YubiKey Serial Number
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Serial(pub u32); pub struct Serial(pub u32);
@@ -95,6 +111,17 @@ pub struct Version {
pub patch: u8, pub patch: u8,
} }
impl Version {
/// Parse a version from bytes
pub fn new(bytes: [u8; 3]) -> Version {
Version {
major: bytes[0],
minor: bytes[1],
patch: bytes[2],
}
}
}
/// YubiKey Device: this is the primary API for opening a session and /// YubiKey Device: this is the primary API for opening a session and
/// performing various operations. /// performing various operations.
/// ///
@@ -104,103 +131,35 @@ pub struct Version {
#[cfg_attr(not(feature = "untested"), allow(dead_code))] #[cfg_attr(not(feature = "untested"), allow(dead_code))]
pub struct YubiKey { pub struct YubiKey {
pub(crate) card: Card, pub(crate) card: Card,
pub(crate) pin: Option<Buffer>, pub(crate) pin: Option<CachedPin>,
pub(crate) is_neo: bool, pub(crate) is_neo: bool,
pub(crate) version: Version, pub(crate) version: Version,
pub(crate) serial: Serial, pub(crate) serial: Serial,
} }
impl YubiKey { impl YubiKey {
/// Open a connection to a YubiKey, optionally giving the name /// Open a connection to a YubiKey.
/// (needed if e.g. there are multiple YubiKeys connected). ///
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> { /// Returns an error if there is more than one YubiKey detected.
let context = Context::establish(pcsc::Scope::System)?; ///
let mut card = Self::connect(&context, name)?; /// If you need to operate in environments with more than one YubiKey
/// attached to the same system, use [`yubikey_piv::Readers`] to select
/// from the available PC/SC readers connected.
pub fn open() -> Result<Self, Error> {
let mut readers = Readers::open()?;
let mut reader_iter = readers.iter()?;
let mut is_neo = false; if let Some(reader) = reader_iter.next() {
let version: Version; if reader_iter.next().is_some() {
let serial: Serial; error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
{
let txn = Transaction::new(&mut card)?;
let mut atr_buf = [0; CB_ATR_MAX];
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
if atr == YKPIV_ATR_NEO_R3 {
is_neo = true;
} }
txn.select_application()?; return reader.open();
// now that the PIV application is selected, retrieve the version
// and serial number. Previously the NEO/YK4 required switching
// to the yk applet to retrieve the serial, YK5 implements this
// as a PIV applet command. Unfortunately, this change requires
// that we retrieve the version number first, so that get_serial
// can determine how to get the serial number, which for the NEO/Yk4
// will result in another selection of the PIV applet.
version = txn.get_version().map_err(|e| {
warn!("failed to retrieve version: '{}'", e);
e
})?;
serial = txn.get_serial(version).map_err(|e| {
warn!("failed to retrieve serial number: '{}'", e);
e
})?;
} }
let yubikey = YubiKey { error!("no YubiKey detected!");
card, Err(Error::GenericError)
pin: None,
is_neo,
version,
serial,
};
Ok(yubikey)
}
/// Connect to a YubiKey PC/SC card.
fn connect(context: &Context, name: Option<&[u8]>) -> Result<Card, Error> {
// ensure PC/SC context is valid
context.is_valid()?;
let buffer_len = context.list_readers_len()?;
let mut buffer = vec![0u8; buffer_len];
for reader in context.list_readers(&mut buffer)? {
if let Some(wanted) = name {
if reader.to_bytes() != wanted {
warn!(
"skipping reader '{}' since it doesn't match '{}'",
reader.to_string_lossy(),
String::from_utf8_lossy(wanted)
);
continue;
}
}
info!("trying to connect to reader '{}'", reader.to_string_lossy());
match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1) {
Ok(card) => {
info!("connected to '{}' successfully", reader.to_string_lossy());
return Ok(card);
}
Err(err) => {
error!(
"skipping '{}' due to connection error: {}",
reader.to_string_lossy(),
err
);
}
}
}
error!("error: no usable reader found");
Err(Error::PcscError { inner: None })
} }
/// Reconnect to a YubiKey /// Reconnect to a YubiKey
@@ -214,8 +173,10 @@ impl YubiKey {
pcsc::Disposition::ResetCard, pcsc::Disposition::ResetCard,
)?; )?;
// TODO(tarcieri): zeroize pin! let pin = self
let pin = self.pin.clone(); .pin
.as_ref()
.map(|p| Buffer::new(p.expose_secret().clone()));
let txn = Transaction::new(&mut self.card)?; let txn = Transaction::new(&mut self.card)?;
txn.select_application()?; txn.select_application()?;
@@ -266,17 +227,17 @@ impl YubiKey {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// get a challenge from the card // get a challenge from the card
let challenge = APDU::new(YKPIV_INS_AUTHENTICATE) let challenge = APDU::new(Ins::Authenticate)
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
.data(&[0x7c, 0x02, 0x80, 0x00]) .data(&[0x7c, 0x02, 0x80, 0x00])
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
if !challenge.is_success() || challenge.buffer().len() < 12 { if !challenge.is_success() || challenge.data().len() < 12 {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
// send a response to the cards challenge and a challenge of our own. // send a response to the cards challenge and a challenge of our own.
let response = mgm_key.decrypt(challenge.buffer()[4..12].try_into().unwrap()); let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
let mut data = [0u8; 22]; let mut data = [0u8; 22];
data[0] = 0x7c; data[0] = 0x7c;
@@ -295,7 +256,7 @@ impl YubiKey {
let mut challenge = [0u8; 8]; let mut challenge = [0u8; 8];
challenge.copy_from_slice(&data[14..22]); challenge.copy_from_slice(&data[14..22]);
let authentication = APDU::new(YKPIV_INS_AUTHENTICATE) let authentication = APDU::new(Ins::Authenticate)
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
.data(&data) .data(&data)
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
@@ -308,7 +269,7 @@ impl YubiKey {
let response = mgm_key.encrypt(&challenge); let response = mgm_key.encrypt(&challenge);
use subtle::ConstantTimeEq; use subtle::ConstantTimeEq;
if response.ct_eq(&authentication.buffer()[4..12]).unwrap_u8() != 1 { if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
@@ -320,7 +281,7 @@ impl YubiKey {
pub fn deauthenticate(&mut self) -> Result<(), Error> { pub fn deauthenticate(&mut self) -> Result<(), Error> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION) let status_words = APDU::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(MGMT_AID) .data(MGMT_AID)
.transmit(&txn, 255)? .transmit(&txn, 255)?
@@ -342,15 +303,13 @@ impl YubiKey {
pub fn sign_data( pub fn sign_data(
&mut self, &mut self,
raw_in: &[u8], raw_in: &[u8],
sign_out: &mut [u8], algorithm: AlgorithmId,
out_len: &mut usize,
algorithm: u8,
key: SlotId, key: SlotId,
) -> Result<(), Error> { ) -> Result<Buffer, Error> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS // don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
txn.authenticated_command(raw_in, sign_out, out_len, algorithm, key, false) txn.authenticated_command(raw_in, algorithm, key, false)
} }
/// Decrypt data using a PIV key /// Decrypt data using a PIV key
@@ -358,15 +317,13 @@ impl YubiKey {
pub fn decrypt_data( pub fn decrypt_data(
&mut self, &mut self,
input: &[u8], input: &[u8],
out: &mut [u8], algorithm: AlgorithmId,
out_len: &mut usize,
algorithm: u8,
key: SlotId, key: SlotId,
) -> Result<(), Error> { ) -> Result<Buffer, Error> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS // don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
txn.authenticated_command(input, out, out_len, algorithm, key, true) txn.authenticated_command(input, algorithm, key, true)
} }
/// Verify device PIN. /// Verify device PIN.
@@ -378,7 +335,7 @@ impl YubiKey {
} }
if !pin.is_empty() { if !pin.is_empty() {
self.pin = Some(Buffer::new(pin.into())) self.pin = Some(CachedPin::new(pin.into()))
} }
Ok(()) Ok(())
@@ -386,7 +343,7 @@ impl YubiKey {
/// Get the number of PIN retries /// Get the number of PIN retries
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn get_pin_retries(&mut self) -> Result<u32, Error> { pub fn get_pin_retries(&mut self) -> Result<u8, Error> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// Force a re-select to unverify, because once verified the spec dictates that // Force a re-select to unverify, because once verified the spec dictates that
@@ -404,24 +361,15 @@ impl YubiKey {
/// Set the number of PIN retries /// Set the number of PIN retries
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn set_pin_retries(&mut self, pin_tries: usize, puk_tries: usize) -> Result<(), Error> { pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<(), Error> {
// Special case: if either retry count is 0, it's a successful no-op // Special case: if either retry count is 0, it's a successful no-op
if pin_tries == 0 || puk_tries == 0 { if pin_tries == 0 || puk_tries == 0 {
return Ok(()); return Ok(());
} }
if pin_tries > 0xff || puk_tries > 0xff || pin_tries < 1 || puk_tries < 1 {
return Err(Error::RangeError);
}
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let templ = [ let templ = [0, Ins::SetPinRetries.code(), pin_tries, puk_tries];
0,
YKPIV_INS_SET_PIN_RETRIES,
pin_tries as u8,
puk_tries as u8,
];
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words(); let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
@@ -444,7 +392,7 @@ impl YubiKey {
} }
if !new_pin.is_empty() { if !new_pin.is_empty() {
self.pin = Some(Buffer::new(new_pin.into())); self.pin = Some(CachedPin::new(new_pin.into()));
} }
Ok(()) Ok(())
@@ -597,7 +545,7 @@ impl YubiKey {
pub fn import_private_key( pub fn import_private_key(
&mut self, &mut self,
key: SlotId, key: SlotId,
algorithm: u8, algorithm: AlgorithmId,
p: Option<&[u8]>, p: Option<&[u8]>,
q: Option<&[u8]>, q: Option<&[u8]>,
dp: Option<&[u8]>, dp: Option<&[u8]>,
@@ -607,177 +555,106 @@ impl YubiKey {
pin_policy: u8, pin_policy: u8,
touch_policy: u8, touch_policy: u8,
) -> Result<(), Error> { ) -> Result<(), Error> {
// TODO(tarcieri): get rid of legacy pointers
let (p, p_len) = match p {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let (q, q_len) = match q {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let (dp, dp_len) = match dp {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let (dq, dq_len) = match dq {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let (qinv, qinv_len) = match qinv {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let (ec_data, ec_data_len) = match ec_data {
Some(slice) => (slice.as_ptr(), slice.len()),
None => (ptr::null(), 0),
};
let mut key_data = Zeroizing::new(vec![0u8; 1024]); let mut key_data = Zeroizing::new(vec![0u8; 1024]);
let mut in_ptr: *mut u8 = key_data.as_mut_ptr(); let templ = [0, Ins::ImportKey.code(), algorithm.into(), key.into()];
let templ = [0, YKPIV_INS_IMPORT_KEY, algorithm, key];
let mut elem_len: u32 = 0;
let mut params: [*const u8; 5] = [ptr::null(); 5];
let mut lens = [0usize; 5];
let n_params: u8;
let param_tag: i32;
if key == YKPIV_KEY_CARDMGM
|| key < YKPIV_KEY_RETIRED1
|| key > YKPIV_KEY_RETIRED20 && (key < YKPIV_KEY_AUTHENTICATION)
|| key > YKPIV_KEY_CARDAUTH && (key != YKPIV_KEY_ATTESTATION)
{
return Err(Error::KeyError);
}
if pin_policy != YKPIV_PINPOLICY_DEFAULT if pin_policy != YKPIV_PINPOLICY_DEFAULT
&& (pin_policy != YKPIV_PINPOLICY_NEVER) && pin_policy != YKPIV_PINPOLICY_NEVER
&& (pin_policy != YKPIV_PINPOLICY_ONCE) && pin_policy != YKPIV_PINPOLICY_ONCE
&& (pin_policy != YKPIV_PINPOLICY_ALWAYS) && pin_policy != YKPIV_PINPOLICY_ALWAYS
{ {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT
&& (touch_policy != YKPIV_TOUCHPOLICY_NEVER) && touch_policy != YKPIV_TOUCHPOLICY_NEVER
&& (touch_policy != YKPIV_TOUCHPOLICY_ALWAYS) && touch_policy != YKPIV_TOUCHPOLICY_ALWAYS
&& (touch_policy != YKPIV_TOUCHPOLICY_CACHED) && touch_policy != YKPIV_TOUCHPOLICY_CACHED
{ {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
let (elem_len, params, param_tag) = match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => match (p, q, dp, dq, qinv) {
(Some(p), Some(q), Some(dp), Some(dq), Some(qinv)) => {
if p.len() + q.len() + dp.len() + dq.len() + qinv.len() >= key_data.len() {
return Err(Error::SizeError);
}
(
match algorithm { match algorithm {
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => { AlgorithmId::Rsa1024 => 64,
if p_len + q_len + dp_len + dq_len + qinv_len >= 1024 { AlgorithmId::Rsa2048 => 128,
return Err(Error::SizeError); _ => unreachable!(),
} else { },
if algorithm == YKPIV_ALGO_RSA1024 { vec![p, q, dp, dq, qinv],
elem_len = 64; 0x01,
)
} }
_ => return Err(Error::GenericError),
if algorithm == YKPIV_ALGO_RSA2048 { },
elem_len = 128; AlgorithmId::EccP256 | AlgorithmId::EccP384 => match ec_data {
} Some(ec_data) => {
if ec_data.len() >= key_data.len() {
if p.is_null() || q.is_null() || dp.is_null() || dq.is_null() || qinv.is_null() // This can never be true, but check to be explicit.
{
return Err(Error::GenericError);
}
params[0] = p;
lens[0] = p_len;
params[1] = q;
lens[1] = q_len;
params[2] = dp;
lens[2] = dp_len;
params[3] = dq;
lens[3] = dq_len;
params[4] = qinv;
lens[4] = qinv_len;
param_tag = 0x1;
n_params = 5u8;
}
}
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
if ec_data_len >= key_data.len() {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
if algorithm == YKPIV_ALGO_ECCP256 { (
elem_len = 32; match algorithm {
} else if algorithm == YKPIV_ALGO_ECCP384 { AlgorithmId::EccP256 => 32,
elem_len = 48; AlgorithmId::EccP384 => 48,
_ => unreachable!(),
},
vec![ec_data],
0x06,
)
} }
_ => return Err(Error::GenericError),
},
};
if ec_data.is_null() { let mut offset = 0;
return Err(Error::GenericError);
}
params[0] = ec_data; for (i, param) in params.into_iter().enumerate() {
lens[0] = ec_data_len; key_data[offset] = param_tag + i as u8;
param_tag = 0x6; offset += 1;
n_params = 1;
}
_ => return Err(Error::AlgorithmError),
}
for i in 0..n_params { offset += set_length(&mut key_data[offset..], elem_len);
unsafe {
*in_ptr = (param_tag + i as i32) as u8;
in_ptr = in_ptr.offset(1);
in_ptr = in_ptr.add(set_length( let padding = elem_len - param.len();
slice::from_raw_parts_mut( let remaining = key_data.len() - offset;
in_ptr,
key_data.as_mut_ptr() as usize - in_ptr as usize,
),
elem_len as usize,
));
}
let padding = elem_len as usize - lens[i as usize];
let remaining = (key_data.as_mut_ptr() as usize) + 1024 - in_ptr as usize;
if padding > remaining { if padding > remaining {
return Err(Error::AlgorithmError); return Err(Error::AlgorithmError);
} }
unsafe { for b in &mut key_data[offset..offset + padding] {
ptr::write_bytes(in_ptr, 0, padding); *b = 0;
in_ptr = in_ptr.add(padding);
ptr::copy(params[i as usize], in_ptr, lens[i as usize]);
in_ptr = in_ptr.add(lens[i as usize]);
} }
offset += padding;
key_data[offset..offset + param.len()].copy_from_slice(param);
offset += param.len();
} }
if pin_policy != YKPIV_PINPOLICY_DEFAULT { if pin_policy != YKPIV_PINPOLICY_DEFAULT {
unsafe { key_data[offset] = YKPIV_PINPOLICY_TAG;
*in_ptr = YKPIV_PINPOLICY_TAG; key_data[offset + 1] = 0x01;
*in_ptr.add(1) = 0x01; key_data[offset + 2] = pin_policy;
*in_ptr.add(2) = pin_policy; offset += 3;
in_ptr = in_ptr.add(3);
}
} }
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT { if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
unsafe { key_data[offset] = YKPIV_TOUCHPOLICY_TAG;
*in_ptr = YKPIV_TOUCHPOLICY_TAG; key_data[offset + 1] = 0x01;
*in_ptr.add(1) = 0x01; key_data[offset + 2] = touch_policy;
*in_ptr.add(2) = touch_policy; offset += 3;
in_ptr = in_ptr.add(3);
}
} }
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let len = in_ptr as usize - key_data.as_mut_ptr() as usize;
let status_words = txn let status_words = txn
.transfer_data(&templ, &key_data[..len], 256)? .transfer_data(&templ, &key_data[..offset], 256)?
.status_words(); .status_words();
match status_words { match status_words {
@@ -791,7 +668,7 @@ impl YubiKey {
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html> /// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn attest(&mut self, key: SlotId) -> Result<Buffer, Error> { pub fn attest(&mut self, key: SlotId) -> Result<Buffer, Error> {
let templ = [0, YKPIV_INS_ATTEST, key, 0]; let templ = [0, Ins::Attest.code(), key.into(), 0];
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?; let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
@@ -803,11 +680,11 @@ impl YubiKey {
} }
} }
if response.buffer()[0] != 0x30 { if response.data()[0] != 0x30 {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
Ok(response.into_buffer()) Ok(Buffer::new(response.data().into()))
} }
/// Get an auth challenge /// Get an auth challenge
@@ -815,7 +692,7 @@ impl YubiKey {
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> { pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let response = APDU::new(YKPIV_INS_AUTHENTICATE) let response = APDU::new(Ins::Authenticate)
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
.data(&[0x7c, 0x02, 0x81, 0x00]) .data(&[0x7c, 0x02, 0x81, 0x00])
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
@@ -824,7 +701,7 @@ impl YubiKey {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
Ok(response.buffer()[4..12].try_into().unwrap()) Ok(response.data()[4..12].try_into().unwrap())
} }
/// Verify an auth response /// Verify an auth response
@@ -840,7 +717,7 @@ impl YubiKey {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// send the response to the card and a challenge of our own. // send the response to the card and a challenge of our own.
let status_words = APDU::new(YKPIV_INS_AUTHENTICATE) let status_words = APDU::new(Ins::Authenticate)
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM) .params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
.data(&data) .data(&data)
.transmit(&txn, 261)? .transmit(&txn, 261)?
@@ -860,7 +737,7 @@ impl YubiKey {
/// The reset function is only available when both pins are blocked. /// The reset function is only available when both pins are blocked.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
pub fn reset_device(&mut self) -> Result<(), Error> { pub fn reset_device(&mut self) -> Result<(), Error> {
let templ = [0, YKPIV_INS_RESET, 0, 0]; let templ = [0, Ins::Reset.code(), 0, 0];
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words(); let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
@@ -881,3 +758,59 @@ impl YubiKey {
} }
} }
} }
impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self, Error> {
let mut card = reader.connect().map_err(|e| {
error!("error connecting to reader '{}': {}", reader.name(), e);
e
})?;
info!("connected to reader: {}", reader.name());
let mut is_neo = false;
let version: Version;
let serial: Serial;
{
let txn = Transaction::new(&mut card)?;
let mut atr_buf = [0; CB_ATR_MAX];
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
if atr == YKPIV_ATR_NEO_R3 {
is_neo = true;
}
txn.select_application()?;
// now that the PIV application is selected, retrieve the version
// and serial number. Previously the NEO/YK4 required switching
// to the yk applet to retrieve the serial, YK5 implements this
// as a PIV applet command. Unfortunately, this change requires
// that we retrieve the version number first, so that get_serial
// can determine how to get the serial number, which for the NEO/Yk4
// will result in another selection of the PIV applet.
version = txn.get_version().map_err(|e| {
warn!("failed to retrieve version: '{}'", e);
e
})?;
serial = txn.get_serial(version).map_err(|e| {
warn!("failed to retrieve serial number: '{}'", e);
e
})?;
}
let yubikey = YubiKey {
card,
pin: None,
is_neo,
version,
serial,
};
Ok(yubikey)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ fn connect() {
env_logger::builder().format_timestamp(None).init(); env_logger::builder().format_timestamp(None).init();
} }
let mut yubikey = YubiKey::open(None).unwrap(); let mut yubikey = YubiKey::open().unwrap();
dbg!(&yubikey.version()); dbg!(&yubikey.version());
dbg!(&yubikey.serial()); dbg!(&yubikey.serial());
} }