Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a41fdc3bc | |||
| c377f226e2 | |||
| da897b99bb | |||
| 9fa2d1c051 | |||
| 07f70bccb5 | |||
| 8e1469cff6 | |||
| 9ce2ffe938 | |||
| 589ca3de12 | |||
| ae071e706c | |||
| cd704c28d7 | |||
| 3a283aca40 | |||
| e72ee5c60e | |||
| 9ee1494c6f | |||
| d3e565ef55 | |||
| 2bdeca0069 | |||
| bc95d8b7b9 | |||
| 11c93d6421 | |||
| afca0fec0a | |||
| c8837d485f | |||
| 12b5bd1e3c | |||
| c3698dcffb | |||
| 6a16c59567 | |||
| 8e38cf6c4e | |||
| ac665f9ec9 | |||
| 77302af21e | |||
| 78288b4200 | |||
| a61a6fd94b | |||
| cfef291ad9 | |||
| 4b5cd8dd45 | |||
| 9fe363661e | |||
| 4af95edc74 | |||
| 7f3d821df2 | |||
| 2f963a15d0 | |||
| 4210571da3 | |||
| 1db929c10f | |||
| 8240575bb4 | |||
| 1935216cf3 | |||
| 7c08674fac | |||
| 8b86a0f578 | |||
| bd5669d9ef | |||
| afb6a9479e | |||
| 48d0a2ab04 | |||
| 82b4bbb35d | |||
| 13b350f822 | |||
| 0f1ef2f519 | |||
| d799e9c35b | |||
| 5bf27f5422 | |||
| ecea0081b5 | |||
| debde6e765 | |||
| 3fa5555943 | |||
| d3af2f2d80 | |||
| 5fab09e54d |
@@ -3,7 +3,7 @@
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: master
|
||||
branches: develop
|
||||
|
||||
name: Rust
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: test
|
||||
args: --release
|
||||
args: --all --release
|
||||
|
||||
- name: Run cargo build --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: build
|
||||
args: --all-features
|
||||
args: --all --all-features
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: test
|
||||
args: --release
|
||||
args: --all --release
|
||||
|
||||
- name: Run cargo build --all-features
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
with:
|
||||
command: build
|
||||
args: --all-features
|
||||
args: --all --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
args: --all --all-features -- -D warnings
|
||||
|
||||
# TODO: use actions-rs/audit-check
|
||||
security_audit:
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
||||
+40
-11
@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.3] (2019-12-02)
|
||||
### Added
|
||||
- Initial `Readers` enumerator for detecting YubiKeys ([#51])
|
||||
- Certificate parsing ([#45])
|
||||
|
||||
### Changed
|
||||
- Use `Reader` to connect to `YubiKey` ([#51])
|
||||
- Convert `SlotId` and `AlgorithmId` into enums ([#44])
|
||||
- Use `secrecy` crate for storing `CachedPin` ([#43])
|
||||
- Change `CHUID` struct to hold complete CHUID value ([#42])
|
||||
- Eliminate all usages of `unsafe` ([#37], [#39])
|
||||
- Make anonymous CHUID struct public ([#36])
|
||||
- Have `sign_data` and `decrypt_data` return a `Buffer` ([#34])
|
||||
- `Ins` (APDU instruction codes) enum ([#33])
|
||||
- Factor `Response` into `apdu` module; improved debugging ([#32])
|
||||
|
||||
[0.0.3]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/53
|
||||
[#51]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/51
|
||||
[#45]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/45
|
||||
[#44]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/44
|
||||
[#43]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/43
|
||||
[#42]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/42
|
||||
[#39]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/39
|
||||
[#37]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/37
|
||||
[#36]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/36
|
||||
[#34]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/34
|
||||
[#33]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/33
|
||||
[#32]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/32
|
||||
|
||||
## [0.0.2] (2019-11-25)
|
||||
### Added
|
||||
- `untested` Cargo feature to mark untested functionality ([#30])
|
||||
@@ -19,17 +48,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Use `log` crate for logging ([#7])
|
||||
- Replace `ErrorKind::Ok` with `Result` ([#6])
|
||||
|
||||
[0.0.2]: https://github.com/tarcieri/yubikey-piv.rs/pull/31
|
||||
[#30]: https://github.com/tarcieri/yubikey-piv.rs/pull/30
|
||||
[#19]: https://github.com/tarcieri/yubikey-piv.rs/pull/19
|
||||
[#17]: https://github.com/tarcieri/yubikey-piv.rs/pull/17
|
||||
[#15]: https://github.com/tarcieri/yubikey-piv.rs/pull/15
|
||||
[#13]: https://github.com/tarcieri/yubikey-piv.rs/pull/13
|
||||
[#10]: https://github.com/tarcieri/yubikey-piv.rs/pull/10
|
||||
[#9]: https://github.com/tarcieri/yubikey-piv.rs/pull/9
|
||||
[#8]: https://github.com/tarcieri/yubikey-piv.rs/pull/8
|
||||
[#7]: https://github.com/tarcieri/yubikey-piv.rs/pull/7
|
||||
[#6]: https://github.com/tarcieri/yubikey-piv.rs/pull/6
|
||||
[0.0.2]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/31
|
||||
[#30]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/30
|
||||
[#19]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/19
|
||||
[#17]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/17
|
||||
[#15]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/15
|
||||
[#13]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/13
|
||||
[#10]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/10
|
||||
[#9]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/9
|
||||
[#8]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/8
|
||||
[#7]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/7
|
||||
[#6]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/6
|
||||
|
||||
## 0.0.1 (2019-11-18)
|
||||
- It typechecks, ship it!
|
||||
|
||||
+8
-10
@@ -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
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
education, socio-economic status, nationality, personal appearance, race,
|
||||
religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
@@ -23,7 +23,7 @@ include:
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
@@ -55,8 +55,8 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [bascule@gmail.com]. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
reported by contacting the project team at [oss@iqlusion.io](mailto:oss@iqlusion.io).
|
||||
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
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
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
|
||||
members of the project's leadership.
|
||||
|
||||
[bascule@gmail.com]: mailto:bascule@gmail.com
|
||||
|
||||
## Attribution
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
+12
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "yubikey-piv"
|
||||
version = "0.0.2" # Also update html_root_url in lib.rs when bumping this
|
||||
version = "0.0.3" # Also update html_root_url in lib.rs when bumping this
|
||||
description = """
|
||||
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
||||
application providing general-purpose public-key signing and encryption
|
||||
@@ -10,23 +10,32 @@ algorithms (e.g, PKCS#1v1.5, ECDSA)
|
||||
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
|
||||
edition = "2018"
|
||||
license = "BSD-2-Clause"
|
||||
repository = "https://github.com/tarcieri/yubikey-piv.rs"
|
||||
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
|
||||
readme = "README.md"
|
||||
categories = ["api-bindings", "cryptography", "hardware-support"]
|
||||
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
|
||||
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "cli"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
|
||||
[dependencies]
|
||||
der-parser = "3"
|
||||
des = "0.3"
|
||||
ecdsa = "0.1"
|
||||
getrandom = "0.1"
|
||||
hmac = "0.7"
|
||||
log = "0.4"
|
||||
nom = "5"
|
||||
pbkdf2 = "0.3"
|
||||
pcsc = "2"
|
||||
rsa = "0.1.4"
|
||||
secrecy = "0.5"
|
||||
sha-1 = "0.8"
|
||||
subtle = "2"
|
||||
x509-parser = "0.6"
|
||||
zeroize = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
![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]
|
||||
|
||||
@@ -115,12 +116,15 @@ Application Protocol Data Unit (APDU) messages, use the `trace` log level:
|
||||
running 1 test
|
||||
[INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
||||
[INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
||||
[TRACE yubikey_piv::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] <<< [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] <<< [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, 115, 0, 178, 144, 0]
|
||||
[TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [0, 115, 0, 178] }
|
||||
test connect ... ok
|
||||
```
|
||||
|
||||
@@ -190,10 +194,12 @@ or conditions.
|
||||
[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
|
||||
[build-image]: https://github.com/tarcieri/yubikey-piv.rs/workflows/Rust/badge.svg
|
||||
[build-link]: https://github.com/tarcieri/yubikey-piv.rs/actions
|
||||
[gitter-image]: https://badges.gitter.im/yubihsm-piv-rs.svg
|
||||
[gitter-link]: https://gitter.im/yubikey-piv-rs/community
|
||||
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
||||
[build-image]: https://github.com/iqlusioninc/yubikey-piv.rs/workflows/Rust/badge.svg?branch=develop&event=push
|
||||
[build-link]: https://github.com/iqlusioninc/yubikey-piv.rs/actions
|
||||
[gitter-image]: https://badges.gitter.im/badge.svg
|
||||
[gitter-link]: https://gitter.im/iqlusioninc/community
|
||||
|
||||
[//]: # (general links)
|
||||
|
||||
@@ -206,18 +212,18 @@ or conditions.
|
||||
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
||||
[Corrode]: https://github.com/jameysharp/corrode
|
||||
[cc-web]: https://contributor-covenant.org/
|
||||
[cc-md]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
|
||||
[cc-md]: https://github.com/iqlusioninc/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
|
||||
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
||||
|
||||
[//]: # (github issues)
|
||||
|
||||
[#18]: https://github.com/tarcieri/yubikey-piv.rs/issues/18
|
||||
[#20]: https://github.com/tarcieri/yubikey-piv.rs/issues/20
|
||||
[#21]: https://github.com/tarcieri/yubikey-piv.rs/issues/21
|
||||
[#22]: https://github.com/tarcieri/yubikey-piv.rs/issues/22
|
||||
[#23]: https://github.com/tarcieri/yubikey-piv.rs/issues/23
|
||||
[#24]: https://github.com/tarcieri/yubikey-piv.rs/issues/24
|
||||
[#25]: https://github.com/tarcieri/yubikey-piv.rs/issues/25
|
||||
[#26]: https://github.com/tarcieri/yubikey-piv.rs/issues/26
|
||||
[#27]: https://github.com/tarcieri/yubikey-piv.rs/issues/27
|
||||
[#28]: https://github.com/tarcieri/yubikey-piv.rs/issues/28
|
||||
[#18]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/18
|
||||
[#20]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/20
|
||||
[#21]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/21
|
||||
[#22]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/22
|
||||
[#23]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/23
|
||||
[#24]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/24
|
||||
[#25]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/25
|
||||
[#26]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/26
|
||||
[#27]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/27
|
||||
[#28]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/28
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 0.0.1 (2019-12-02)
|
||||
- Initial release
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "yubikey-cli"
|
||||
version = "0.0.1"
|
||||
description = """
|
||||
Command-line interface for performing encryption and signing using RSA and/or
|
||||
ECC keys stored on YubiKey devices.
|
||||
"""
|
||||
authors = ["Tony Arcieri <bascule@gmail.com>"]
|
||||
edition = "2018"
|
||||
license = "BSD-2-Clause"
|
||||
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
|
||||
readme = "README.md"
|
||||
categories = ["command-line-utilities", "cryptography", "hardware-support"]
|
||||
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||
|
||||
[dependencies]
|
||||
gumdrop = "0.7"
|
||||
env_logger = "0.7"
|
||||
lazy_static = "1"
|
||||
termcolor = "1"
|
||||
yubikey-piv = { version = "0.0.3", path = ".." }
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<img src="https://raw.githubusercontent.com/tendermint/yubihsm-rs/develop/img/logo.png" width="150" height="110">
|
||||
|
||||
# yubikey-cli.rs
|
||||
|
||||
[![crate][crate-image]][crate-link]
|
||||
[![Docs][docs-image]][docs-link]
|
||||
![Apache2/MIT licensed][license-image]
|
||||
![Rust Version][rustc-image]
|
||||
![Maintenance Status: Experimental][maintenance-image]
|
||||
[![Safety Dance][safety-image]][safety-link]
|
||||
[![Build Status][build-image]][build-link]
|
||||
[![Gitter Chat][gitter-image]][gitter-link]
|
||||
|
||||
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][PIV] CLI
|
||||
utility with general-purpose public-key encryption and signing support.
|
||||
|
||||
[Documentation][docs-link]
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
- Rust **1.39+**
|
||||
|
||||
## Supported YubiKeys
|
||||
|
||||
- [YubiKey NEO] series (may be dropped in the future, see [#18])
|
||||
- [YubiKey 4] series
|
||||
- [YubiKey 5] series
|
||||
|
||||
NOTE: Nano and USB-C variants of the above are also supported
|
||||
|
||||
## Security Warning
|
||||
|
||||
No security audits of this crate have ever been performed. Presently it is in
|
||||
an experimental stage and may still contain high-severity issues.
|
||||
|
||||
USE AT YOUR OWN RISK!
|
||||
|
||||
## Status
|
||||
|
||||
WIP. Check back later.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
||||
|
||||
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you shall be licensed under the
|
||||
[2-Clause BSD License][BSDL] as shown above, without any additional terms
|
||||
or conditions.
|
||||
|
||||
[//]: # (badges)
|
||||
|
||||
[crate-image]: https://img.shields.io/crates/v/yubikey-cli.svg
|
||||
[crate-link]: https://crates.io/crates/yubikey-cli
|
||||
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
|
||||
[docs-link]: https://docs.rs/yubikey-cli/
|
||||
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
||||
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
|
||||
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
|
||||
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
||||
[build-image]: https://github.com/iqlusioninc/yubikey-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
|
||||
@@ -0,0 +1,16 @@
|
||||
//! `yubikey` command-line utility
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
use gumdrop::Options;
|
||||
use yubikey_cli::commands::YubikeyCli;
|
||||
|
||||
fn main() {
|
||||
YubikeyCli::parse_args_default_or_exit().run();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Commands of the CLI application
|
||||
|
||||
pub mod list;
|
||||
|
||||
use self::list::ListCmd;
|
||||
use crate::status;
|
||||
use gumdrop::Options;
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
use termcolor::ColorChoice;
|
||||
|
||||
/// The `yubikey` CLI utility
|
||||
#[derive(Debug, Options)]
|
||||
pub struct YubikeyCli {
|
||||
/// Obtain help about the current command
|
||||
#[options(short = "h", help = "print help message")]
|
||||
pub help: bool,
|
||||
|
||||
/// Subcommand to execute.
|
||||
#[options(command)]
|
||||
pub command: Option<Commands>,
|
||||
}
|
||||
|
||||
impl YubikeyCli {
|
||||
/// Run the underlying command type or print usage info and exit
|
||||
pub fn run(&self) {
|
||||
// TODO(tarcieri): make this more configurable
|
||||
status::set_color_choice(ColorChoice::Auto);
|
||||
|
||||
// Only show logs if `RUST_LOG` is set
|
||||
if env::var("RUST_LOG").is_ok() {
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
}
|
||||
|
||||
match &self.command {
|
||||
Some(cmd) => cmd.run(),
|
||||
None => println!("{}", Commands::usage()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subcommands of this application
|
||||
#[derive(Debug, Options)]
|
||||
pub enum Commands {
|
||||
/// `help` subcommand
|
||||
#[options(help = "show help for a command")]
|
||||
Help(HelpOpts),
|
||||
|
||||
/// `version` subcommand
|
||||
#[options(help = "display version information")]
|
||||
Version(VersionOpts),
|
||||
|
||||
/// `list` subcommand
|
||||
#[options(help = "list detected readers")]
|
||||
List(ListCmd),
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
/// Run the given command
|
||||
pub fn run(&self) {
|
||||
match self {
|
||||
Commands::Help(help) => help.run(),
|
||||
Commands::Version(version) => version.run(),
|
||||
Commands::List(list) => list.run(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Help options
|
||||
#[derive(Debug, Options)]
|
||||
pub struct HelpOpts {
|
||||
#[options(free, help = "subcommand to get help for")]
|
||||
free: Vec<String>,
|
||||
}
|
||||
|
||||
impl HelpOpts {
|
||||
fn run(&self) {
|
||||
if let Some(command) = self.free.first() {
|
||||
if let Some(usage) = Commands::command_usage(command) {
|
||||
println!("{}", usage);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", Commands::usage());
|
||||
}
|
||||
}
|
||||
|
||||
/// Version options
|
||||
#[derive(Debug, Options)]
|
||||
pub struct VersionOpts {}
|
||||
|
||||
impl VersionOpts {
|
||||
/// Display version information
|
||||
pub fn run(&self) {
|
||||
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! List detected readers
|
||||
|
||||
use gumdrop::Options;
|
||||
use std::process::exit;
|
||||
use yubikey_piv::readers::Readers;
|
||||
|
||||
/// The `list` subcommand
|
||||
#[derive(Debug, Options)]
|
||||
pub struct ListCmd {}
|
||||
|
||||
impl ListCmd {
|
||||
/// Run the `list` subcommand
|
||||
pub fn run(&self) {
|
||||
let mut readers = Readers::open().unwrap_or_else(|e| {
|
||||
status_err!("couldn't open PC/SC context: {}", e);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let readers_iter = readers.iter().unwrap_or_else(|e| {
|
||||
status_err!("couldn't enumerate PC/SC readers: {}", e);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
if readers_iter.len() == 0 {
|
||||
status_err!("no YubiKeys detected!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
for (i, reader) in readers_iter.enumerate() {
|
||||
let name = reader.name();
|
||||
let mut yubikey = match reader.open() {
|
||||
Ok(yk) => yk,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let serial = yubikey.serial();
|
||||
println!("{}: {} (serial: {})", i + 1, name, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! `yubikey` command-line utility
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
unused_lifetimes,
|
||||
unused_qualifications
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
pub mod status;
|
||||
|
||||
pub mod commands;
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Status messages
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Mutex;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
|
||||
/// Print a success status message (in green if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_ok {
|
||||
($status:expr, $msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.justified()
|
||||
.bold()
|
||||
.color(termcolor::Color::Green)
|
||||
.status($status)
|
||||
.print_stdout($msg);
|
||||
};
|
||||
($status:expr, $fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_ok!($status, format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
/// Print a warning status message (in yellow if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_warn {
|
||||
($msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.bold()
|
||||
.color(termcolor::Color::Yellow)
|
||||
.status("warning:")
|
||||
.print_stdout($msg);
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_warn!(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
/// Print an error message (in red if colors are enabled)
|
||||
#[macro_export]
|
||||
macro_rules! status_err {
|
||||
($msg:expr) => {
|
||||
$crate::status::Status::new()
|
||||
.bold()
|
||||
.color(termcolor::Color::Red)
|
||||
.status("error:")
|
||||
.print_stderr($msg);
|
||||
};
|
||||
($fmt:expr, $($arg:tt)+) => {
|
||||
$crate::status_err!(format!($fmt, $($arg)+));
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Color configuration
|
||||
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
|
||||
|
||||
/// Standard output
|
||||
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice());
|
||||
|
||||
/// Standard error
|
||||
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice());
|
||||
}
|
||||
|
||||
/// Obtain the color configuration.
|
||||
///
|
||||
/// Panics if no configuration has been provided.
|
||||
fn get_color_choice() -> ColorChoice {
|
||||
let choice = COLOR_CHOICE.lock().unwrap();
|
||||
*choice
|
||||
.as_ref()
|
||||
.expect("terminal stream accessed before initialized!")
|
||||
}
|
||||
|
||||
/// Set the color configuration.
|
||||
///
|
||||
/// Panics if the terminal has already been configured.
|
||||
pub(super) fn set_color_choice(color_choice: ColorChoice) {
|
||||
let mut choice = COLOR_CHOICE.lock().unwrap();
|
||||
assert!(choice.is_none(), "terminal colors already configured!");
|
||||
*choice = Some(color_choice);
|
||||
}
|
||||
|
||||
/// Status message builder
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Status {
|
||||
/// Should the status be justified?
|
||||
justified: bool,
|
||||
|
||||
/// Should colors be bold?
|
||||
bold: bool,
|
||||
|
||||
/// Color in which status should be displayed
|
||||
color: Option<Color>,
|
||||
|
||||
/// Prefix of the status message (e.g. `Success`)
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Create a new status message with default settings
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Justify status on display
|
||||
pub fn justified(mut self) -> Self {
|
||||
self.justified = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Make colors bold
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the colors used to display this message
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a status message to display
|
||||
pub fn status<S>(mut self, msg: S) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
{
|
||||
self.status = Some(msg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Print the given message to stdout
|
||||
pub fn print_stdout(self, msg: impl AsRef<str>) {
|
||||
self.print(&*STDOUT, msg)
|
||||
.expect("error printing to stdout!")
|
||||
}
|
||||
|
||||
/// Print the given message to stderr
|
||||
pub fn print_stderr(self, msg: impl AsRef<str>) {
|
||||
self.print(&*STDERR, msg)
|
||||
.expect("error printing to stderr!")
|
||||
}
|
||||
|
||||
/// Print the given message
|
||||
fn print(self, stream: &StandardStream, msg: impl AsRef<str>) -> Result<(), io::Error> {
|
||||
let mut s = stream.lock();
|
||||
s.reset()?;
|
||||
s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?;
|
||||
|
||||
if let Some(status) = self.status {
|
||||
if self.justified {
|
||||
write!(s, "{:>12}", status)?;
|
||||
} else {
|
||||
write!(s, "{}", status)?;
|
||||
}
|
||||
}
|
||||
|
||||
s.reset()?;
|
||||
writeln!(s, " {}", msg.as_ref())?;
|
||||
s.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+335
-29
@@ -30,8 +30,8 @@
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
use crate::{error::Error, response::Response, transaction::Transaction, Buffer};
|
||||
use std::fmt::{self, Debug};
|
||||
use crate::{error::Error, transaction::Transaction, Buffer};
|
||||
use log::trace;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// These messages are packets used to communicate with the YubiKey.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct APDU {
|
||||
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
|
||||
cla: u8,
|
||||
|
||||
/// Instruction code: indicates the specific command (e.g. "write data")
|
||||
ins: u8,
|
||||
ins: Ins,
|
||||
|
||||
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
|
||||
p1: u8,
|
||||
@@ -60,10 +60,10 @@ pub(crate) struct APDU {
|
||||
|
||||
impl APDU {
|
||||
/// Create a new APDU with the given instruction code
|
||||
pub fn new(ins: u8) -> Self {
|
||||
pub fn new(ins: impl Into<Ins>) -> Self {
|
||||
Self {
|
||||
cla: 0,
|
||||
ins,
|
||||
ins: ins.into(),
|
||||
p1: 0,
|
||||
p2: 0,
|
||||
data: vec![],
|
||||
@@ -112,15 +112,17 @@ impl APDU {
|
||||
|
||||
/// Transmit this APDU using the given card transaction
|
||||
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> {
|
||||
let response_bytes = txn.transmit(&self.to_bytes(), recv_len)?;
|
||||
Ok(Response::from_bytes(response_bytes))
|
||||
trace!(">>> {:?}", self);
|
||||
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 {
|
||||
let mut bytes = Vec::with_capacity(5 + self.data.len());
|
||||
bytes.push(self.cla);
|
||||
bytes.push(self.ins);
|
||||
bytes.push(self.ins.code());
|
||||
bytes.push(self.p1);
|
||||
bytes.push(self.p2);
|
||||
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 {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize();
|
||||
@@ -152,10 +139,329 @@ impl Drop for APDU {
|
||||
|
||||
impl Zeroize for APDU {
|
||||
fn zeroize(&mut self) {
|
||||
self.cla.zeroize();
|
||||
self.ins.zeroize();
|
||||
self.p1.zeroize();
|
||||
self.p2.zeroize();
|
||||
// Only `data` may contain secrets
|
||||
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
@@ -50,7 +50,7 @@ const CCC_TMPL: &[u8] = &[
|
||||
|
||||
/// Cardholder Capability Container (CCC) Identifier
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct CCCID([u8; YKPIV_CCCID_SIZE]);
|
||||
pub struct CCCID(pub [u8; YKPIV_CCCID_SIZE]);
|
||||
|
||||
impl CCCID {
|
||||
/// Generate a random CCCID
|
||||
|
||||
+230
-14
@@ -33,19 +33,141 @@
|
||||
use crate::{
|
||||
consts::*,
|
||||
error::Error,
|
||||
key::{self, SlotId},
|
||||
key::{AlgorithmId, SlotId},
|
||||
serialization::*,
|
||||
transaction::Transaction,
|
||||
yubikey::YubiKey,
|
||||
Buffer,
|
||||
};
|
||||
use ecdsa::{
|
||||
curve::{CompressedCurvePoint, NistP256, NistP384, UncompressedCurvePoint},
|
||||
generic_array::GenericArray,
|
||||
};
|
||||
use log::error;
|
||||
use std::ptr;
|
||||
use rsa::{PublicKey, RSAPublicKey};
|
||||
use std::fmt;
|
||||
use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo};
|
||||
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
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Certificate(Buffer);
|
||||
pub struct Certificate {
|
||||
subject: String,
|
||||
subject_pki: PublicKeyInfo,
|
||||
data: Buffer,
|
||||
}
|
||||
|
||||
impl Certificate {
|
||||
/// Read a certificate from the given slot in the YubiKey
|
||||
@@ -57,14 +179,14 @@ impl Certificate {
|
||||
return Err(Error::InvalidObject);
|
||||
}
|
||||
|
||||
Ok(Certificate(buf))
|
||||
Certificate::new(buf)
|
||||
}
|
||||
|
||||
/// Write this certificate into the YubiKey in the given slot
|
||||
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> {
|
||||
let max_size = yubikey.obj_size_max();
|
||||
let txn = yubikey.begin_transaction()?;
|
||||
write_certificate(&txn, slot, Some(&self.0), certinfo, max_size)
|
||||
write_certificate(&txn, slot, Some(&self.data), certinfo, max_size)
|
||||
}
|
||||
|
||||
/// Delete a certificate located at the given slot of the given YubiKey
|
||||
@@ -83,25 +205,47 @@ impl Certificate {
|
||||
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
|
||||
pub fn into_buffer(self) -> Buffer {
|
||||
self.0
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Certificate {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
self.data.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Read certificate
|
||||
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> {
|
||||
let mut len: usize = 0;
|
||||
let object_id = key::slot_object(slot)?;
|
||||
let object_id = slot.object_id();
|
||||
|
||||
let mut buf = match txn.fetch_object(object_id) {
|
||||
Ok(b) => b,
|
||||
@@ -124,10 +268,7 @@ pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Bu
|
||||
return Ok(Zeroizing::new(vec![]));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy(buf.as_ptr().add(offset), buf.as_mut_ptr(), len);
|
||||
}
|
||||
|
||||
buf.copy_within(offset..offset + len, 0);
|
||||
buf.truncate(len);
|
||||
}
|
||||
|
||||
@@ -145,7 +286,7 @@ pub(crate) fn write_certificate(
|
||||
let mut buf = [0u8; CB_OBJ_MAX];
|
||||
let mut offset = 0;
|
||||
|
||||
let object_id = key::slot_object(slot)?;
|
||||
let object_id = slot.object_id();
|
||||
|
||||
if data.is_none() {
|
||||
return txn.save_object(object_id, &[]);
|
||||
@@ -184,3 +325,78 @@ pub(crate) fn write_certificate(
|
||||
|
||||
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
@@ -55,20 +55,47 @@ const CHUID_TMPL: &[u8] = &[
|
||||
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)]
|
||||
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 {
|
||||
/// 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)
|
||||
pub fn generate() -> Result<Self, Error> {
|
||||
pub fn generate() -> Result<ChuidUuid, Error> {
|
||||
let mut id = [0u8; YKPIV_CARDID_SIZE];
|
||||
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
|
||||
Ok(CHUID(id))
|
||||
Ok(ChuidUuid(id))
|
||||
}
|
||||
|
||||
/// 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 response = txn.fetch_object(YKPIV_OBJ_CHUID)?;
|
||||
|
||||
@@ -76,15 +103,16 @@ impl CHUID {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
let mut cardid = [0u8; YKPIV_CARDID_SIZE];
|
||||
cardid.copy_from_slice(&response[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + YKPIV_CARDID_SIZE)]);
|
||||
Ok(CHUID(cardid))
|
||||
let mut chuid = [0u8; YKPIV_CHUID_SIZE];
|
||||
chuid.copy_from_slice(&response[0..YKPIV_CHUID_SIZE]);
|
||||
let retval = CHUID { 0: chuid };
|
||||
Ok(retval)
|
||||
}
|
||||
|
||||
/// Set Cardholder Unique Identifier (CHUID)
|
||||
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||
let mut buf = CHUID_TMPL.to_vec();
|
||||
buf[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + self.0.len())].copy_from_slice(&self.0);
|
||||
buf[0..self.0.len()].copy_from_slice(&self.0);
|
||||
|
||||
let txn = yubikey.begin_transaction()?;
|
||||
txn.save_object(YKPIV_OBJ_CHUID, &buf)
|
||||
|
||||
+5
-75
@@ -63,7 +63,9 @@ pub const CB_PIN_MAX: usize = 8;
|
||||
|
||||
pub const CCC_ID_OFFS: usize = 9;
|
||||
|
||||
pub const CHUID_FASCN_OFFS: usize = 2;
|
||||
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_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_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_CHUID_SIZE: usize = 59;
|
||||
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_CERTINFO_UNCOMPRESSED: u8 = 0;
|
||||
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_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_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_SECURITY: u32 = 0x005f_c106;
|
||||
pub const YKPIV_OBJ_FACIAL: u32 = 0x005f_c108;
|
||||
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_KEY_HISTORY: u32 = 0x005f_c10c;
|
||||
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
|
||||
|
||||
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_MSROOTS1: u32 = 0x005f_ff11;
|
||||
pub const YKPIV_OBJ_MSROOTS2: u32 = 0x005f_ff12;
|
||||
|
||||
+3
-3
@@ -158,7 +158,7 @@ impl Container {
|
||||
|
||||
Ok(Container {
|
||||
name,
|
||||
slot: bytes[name_bytes_len],
|
||||
slot: bytes[name_bytes_len].try_into()?,
|
||||
key_spec: bytes[name_bytes_len + 1],
|
||||
key_size_bits: u16::from_le_bytes(
|
||||
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.push(self.slot);
|
||||
bytes.push(self.slot.into());
|
||||
bytes.push(self.key_spec);
|
||||
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
|
||||
bytes.push(self.flags);
|
||||
@@ -204,7 +204,7 @@ impl Debug for Container {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
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: {:?} }}",
|
||||
&self.name[..],
|
||||
self.slot,
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ pub enum Error {
|
||||
/// Wrong PIN
|
||||
WrongPin {
|
||||
/// Number of tries remaining
|
||||
tries: u32,
|
||||
tries: u8,
|
||||
},
|
||||
|
||||
/// Invalid object
|
||||
|
||||
+269
-63
@@ -38,68 +38,279 @@
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
use crate::{
|
||||
apdu::{Ins, StatusWords},
|
||||
certificate::{self, Certificate},
|
||||
consts::*,
|
||||
error::Error,
|
||||
response::StatusWords,
|
||||
serialization::*,
|
||||
settings,
|
||||
yubikey::YubiKey,
|
||||
AlgorithmId, ObjectId,
|
||||
Buffer, ObjectId,
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Slot identifiers.
|
||||
/// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html>
|
||||
// TODO(tarcieri): replace these with enums
|
||||
pub type SlotId = u8;
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
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`]
|
||||
// TODO(tarcieri): factor this into a slot ID enum
|
||||
pub(crate) fn slot_object(slot: SlotId) -> Result<ObjectId, Error> {
|
||||
let id = match slot {
|
||||
YKPIV_KEY_AUTHENTICATION => YKPIV_OBJ_AUTHENTICATION,
|
||||
YKPIV_KEY_SIGNATURE => YKPIV_OBJ_SIGNATURE,
|
||||
YKPIV_KEY_KEYMGM => YKPIV_OBJ_KEY_MANAGEMENT,
|
||||
YKPIV_KEY_CARDAUTH => YKPIV_OBJ_CARD_AUTH,
|
||||
YKPIV_KEY_ATTESTATION => YKPIV_OBJ_ATTESTATION,
|
||||
slot if slot >= YKPIV_KEY_RETIRED1 && (slot <= YKPIV_KEY_RETIRED20) => {
|
||||
YKPIV_OBJ_RETIRED1 + (slot - YKPIV_KEY_RETIRED1) as u32
|
||||
/// This certificate and its associated private key is used for digital signatures for
|
||||
/// the purpose of document signing, or signing files and executables. The end user
|
||||
/// PIN is required to perform any private key operations. The PIN must be submitted
|
||||
/// every time immediately before a sign operation, to ensure cardholder participation
|
||||
/// for every digital signature generated.
|
||||
Signature,
|
||||
|
||||
/// This certificate and its associated private key is used for encryption for the
|
||||
/// purpose of confidentiality. This slot is used for things like encrypting e-mails
|
||||
/// or files. 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.
|
||||
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,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
_ => return Err(Error::InvalidObject),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(id)
|
||||
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
|
||||
pub const SLOTS: [u8; 24] = [
|
||||
YKPIV_KEY_AUTHENTICATION,
|
||||
YKPIV_KEY_SIGNATURE,
|
||||
YKPIV_KEY_KEYMGM,
|
||||
YKPIV_KEY_RETIRED1,
|
||||
YKPIV_KEY_RETIRED2,
|
||||
YKPIV_KEY_RETIRED3,
|
||||
YKPIV_KEY_RETIRED4,
|
||||
YKPIV_KEY_RETIRED5,
|
||||
YKPIV_KEY_RETIRED6,
|
||||
YKPIV_KEY_RETIRED7,
|
||||
YKPIV_KEY_RETIRED8,
|
||||
YKPIV_KEY_RETIRED9,
|
||||
YKPIV_KEY_RETIRED10,
|
||||
YKPIV_KEY_RETIRED11,
|
||||
YKPIV_KEY_RETIRED12,
|
||||
YKPIV_KEY_RETIRED13,
|
||||
YKPIV_KEY_RETIRED14,
|
||||
YKPIV_KEY_RETIRED15,
|
||||
YKPIV_KEY_RETIRED16,
|
||||
YKPIV_KEY_RETIRED17,
|
||||
YKPIV_KEY_RETIRED18,
|
||||
YKPIV_KEY_RETIRED19,
|
||||
YKPIV_KEY_RETIRED20,
|
||||
YKPIV_KEY_CARDAUTH,
|
||||
pub const SLOTS: [SlotId; 24] = [
|
||||
SlotId::Authentication,
|
||||
SlotId::Signature,
|
||||
SlotId::KeyManagement,
|
||||
SlotId::Retired(RetiredSlotId::R1),
|
||||
SlotId::Retired(RetiredSlotId::R2),
|
||||
SlotId::Retired(RetiredSlotId::R3),
|
||||
SlotId::Retired(RetiredSlotId::R4),
|
||||
SlotId::Retired(RetiredSlotId::R5),
|
||||
SlotId::Retired(RetiredSlotId::R6),
|
||||
SlotId::Retired(RetiredSlotId::R7),
|
||||
SlotId::Retired(RetiredSlotId::R8),
|
||||
SlotId::Retired(RetiredSlotId::R9),
|
||||
SlotId::Retired(RetiredSlotId::R10),
|
||||
SlotId::Retired(RetiredSlotId::R11),
|
||||
SlotId::Retired(RetiredSlotId::R12),
|
||||
SlotId::Retired(RetiredSlotId::R13),
|
||||
SlotId::Retired(RetiredSlotId::R14),
|
||||
SlotId::Retired(RetiredSlotId::R15),
|
||||
SlotId::Retired(RetiredSlotId::R16),
|
||||
SlotId::Retired(RetiredSlotId::R17),
|
||||
SlotId::Retired(RetiredSlotId::R18),
|
||||
SlotId::Retired(RetiredSlotId::R19),
|
||||
SlotId::Retired(RetiredSlotId::R20),
|
||||
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
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Key {
|
||||
@@ -120,14 +331,16 @@ impl Key {
|
||||
let buf = match certificate::read_certificate(&txn, slot) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
debug!("error reading certificate in slot {}: {}", slot, e);
|
||||
debug!("error reading certificate in slot {:?}: {}", slot, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !buf.is_empty() {
|
||||
let cert = Certificate::new(buf)?;
|
||||
keys.push(Key { slot, cert });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
@@ -199,13 +412,15 @@ pub fn generate(
|
||||
touch_policy: u8,
|
||||
) -> Result<GeneratedKey, Error> {
|
||||
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;
|
||||
|
||||
match algorithm {
|
||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||
if yubikey.device_model() == DEVTYPE_YK4
|
||||
&& (algorithm == YKPIV_ALGO_RSA1024 || algorithm == YKPIV_ALGO_RSA2048)
|
||||
&& yubikey.version.major == 4
|
||||
&& (yubikey.version.minor < 3 || yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
||||
&& (yubikey.version.minor < 3
|
||||
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
||||
{
|
||||
setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true);
|
||||
|
||||
@@ -239,18 +454,13 @@ pub fn generate(
|
||||
return Err(Error::NotSupported);
|
||||
}
|
||||
}
|
||||
|
||||
match algorithm {
|
||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 | YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => (),
|
||||
_ => {
|
||||
error!("invalid algorithm specified");
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let txn = yubikey.begin_transaction()?;
|
||||
|
||||
templ[3] = slot;
|
||||
templ[3] = slot.into();
|
||||
|
||||
let mut offset = 5;
|
||||
in_data[..offset].copy_from_slice(&[
|
||||
@@ -258,7 +468,7 @@ pub fn generate(
|
||||
3, // length sans this 2-byte header
|
||||
YKPIV_ALGO_TAG,
|
||||
1,
|
||||
algorithm,
|
||||
algorithm.into(),
|
||||
]);
|
||||
|
||||
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 {
|
||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
|
||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||
let mut offset = 5;
|
||||
let mut len = 0;
|
||||
|
||||
@@ -340,10 +550,10 @@ pub fn generate(
|
||||
exp,
|
||||
})
|
||||
}
|
||||
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
|
||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||
let mut offset = 3;
|
||||
|
||||
let len = if algorithm == YKPIV_ALGO_ECCP256 {
|
||||
let len = if let AlgorithmId::EccP256 = algorithm {
|
||||
CB_ECC_POINTP256
|
||||
} else {
|
||||
CB_ECC_POINTP384
|
||||
@@ -367,9 +577,5 @@ pub fn generate(
|
||||
let point = data[offset..(offset + len)].to_vec();
|
||||
Ok(GeneratedKey::Ecc { algorithm, point })
|
||||
}
|
||||
_ => {
|
||||
error!("wrong algorithm");
|
||||
Err(Error::AlgorithmError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -123,8 +123,9 @@
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
|
||||
html_root_url = "https://docs.rs/yubikey-piv/0.0.2"
|
||||
html_root_url = "https://docs.rs/yubikey-piv/0.0.3"
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
@@ -155,7 +156,7 @@ mod metadata;
|
||||
pub mod mgm;
|
||||
#[cfg(feature = "untested")]
|
||||
pub mod msroots;
|
||||
mod response;
|
||||
pub mod readers;
|
||||
#[cfg(feature = "untested")]
|
||||
mod serialization;
|
||||
#[cfg(feature = "untested")]
|
||||
@@ -163,13 +164,10 @@ pub mod settings;
|
||||
mod transaction;
|
||||
pub mod yubikey;
|
||||
|
||||
pub use self::{readers::Readers, yubikey::YubiKey};
|
||||
|
||||
#[cfg(feature = "untested")]
|
||||
pub use self::{key::Key, mgm::MgmKey};
|
||||
pub use yubikey::YubiKey;
|
||||
|
||||
/// Algorithm identifiers
|
||||
// TODO(tarcieri): make this an enum
|
||||
pub type AlgorithmId = u8;
|
||||
|
||||
/// Object identifiers
|
||||
pub type ObjectId = u32;
|
||||
|
||||
+48
-85
@@ -31,48 +31,36 @@
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
use crate::{consts::*, error::Error, serialization::*, transaction::Transaction, Buffer};
|
||||
use std::{ptr, slice};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// Get metadata item
|
||||
pub(crate) fn get_item(data: &[u8], tag: u8) -> Result<&[u8], Error> {
|
||||
let mut p_temp: *const u8 = data.as_ptr();
|
||||
let mut cb_temp: usize = 0;
|
||||
let mut tag_temp: u8;
|
||||
let mut offset = 0;
|
||||
|
||||
unsafe {
|
||||
while p_temp < data.as_ptr().add(data.len()) {
|
||||
tag_temp = *p_temp;
|
||||
p_temp = p_temp.add(1);
|
||||
while offset < data.len() {
|
||||
let tag_temp = data[offset];
|
||||
offset += 1;
|
||||
|
||||
let p_slice = slice::from_raw_parts(
|
||||
p_temp,
|
||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
||||
);
|
||||
|
||||
if !has_valid_length(
|
||||
p_slice,
|
||||
data.as_ptr().add(data.len()) as usize - p_temp as usize,
|
||||
) {
|
||||
if !has_valid_length(&data[offset..], data.len() - 1) {
|
||||
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 {
|
||||
// found tag
|
||||
break;
|
||||
}
|
||||
|
||||
p_temp = p_temp.add(cb_temp);
|
||||
}
|
||||
|
||||
if p_temp < data.as_ptr().add(data.len()) {
|
||||
return Ok(slice::from_raw_parts(p_temp, cb_temp));
|
||||
}
|
||||
offset += cb_temp;
|
||||
}
|
||||
|
||||
if offset < data.len() {
|
||||
Ok(&data[offset..offset + cb_temp])
|
||||
} else {
|
||||
Err(Error::GenericError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set metadata item
|
||||
@@ -83,35 +71,25 @@ pub(crate) fn set_item(
|
||||
tag: u8,
|
||||
p_item: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let mut p_temp: *mut u8 = data.as_mut_ptr();
|
||||
let mut cb_temp: usize = 0;
|
||||
let mut tag_temp: u8 = 0;
|
||||
let mut cb_len: usize = 0;
|
||||
let cb_item = p_item.len();
|
||||
// Must be signed to have negative offsets
|
||||
let cb_moved: isize;
|
||||
let p_next: *mut u8;
|
||||
|
||||
while p_temp < data[*pcb_data..].as_mut_ptr() {
|
||||
unsafe {
|
||||
tag_temp = *p_temp;
|
||||
p_temp = p_temp.add(1);
|
||||
let mut offset = 0;
|
||||
|
||||
cb_len = get_length(
|
||||
slice::from_raw_parts(
|
||||
p_temp,
|
||||
data.as_mut_ptr() as usize + data.len() - p_temp as usize,
|
||||
),
|
||||
&mut cb_temp,
|
||||
);
|
||||
p_temp = p_temp.add(cb_len);
|
||||
while offset < *pcb_data {
|
||||
tag_temp = data[offset];
|
||||
offset += 1;
|
||||
|
||||
cb_len = get_length(&data[offset..], &mut cb_temp);
|
||||
offset += cb_len;
|
||||
|
||||
if tag_temp == tag {
|
||||
break;
|
||||
}
|
||||
|
||||
p_temp = p_temp.add(cb_temp);
|
||||
}
|
||||
offset += cb_temp;
|
||||
}
|
||||
|
||||
if tag_temp != tag {
|
||||
@@ -120,75 +98,63 @@ pub(crate) fn set_item(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
p_temp = data.as_mut_ptr().add(*pcb_data);
|
||||
// We did not find an existing tag, append
|
||||
offset = *pcb_data;
|
||||
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 {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
*p_temp = tag;
|
||||
p_temp = p_temp.add(1);
|
||||
p_temp = p_temp.add(set_length(
|
||||
slice::from_raw_parts_mut(
|
||||
p_temp,
|
||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
||||
),
|
||||
cb_item,
|
||||
));
|
||||
|
||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
||||
}
|
||||
data[offset] = tag;
|
||||
offset += 1;
|
||||
offset += set_length(&mut data[offset..], cb_item);
|
||||
data[offset..offset + cb_item].copy_from_slice(p_item);
|
||||
|
||||
*pcb_data += 1 + cb_len + cb_item;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if cb_temp == cb_item {
|
||||
unsafe {
|
||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
||||
}
|
||||
// Found tag
|
||||
|
||||
// Check length, if it matches, overwrite
|
||||
if cb_temp == cb_item {
|
||||
data[offset..offset + cb_item].copy_from_slice(p_item);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
p_next = unsafe { p_temp.add(cb_temp) };
|
||||
cb_moved = (cb_item as isize - cb_temp as isize)
|
||||
// Length doesn't match, expand/shrink to fit
|
||||
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 {
|
||||
get_length_size(cb_item) as isize
|
||||
} else {
|
||||
// For tag, if deleting
|
||||
-1
|
||||
}
|
||||
// Accounts for different length encoding
|
||||
- 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);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy(
|
||||
p_next,
|
||||
p_next.offset(cb_moved),
|
||||
*pcb_data - p_next as usize - data.as_ptr() as usize,
|
||||
// Move remaining data
|
||||
data.copy_within(
|
||||
next_offset..*pcb_data,
|
||||
(next_offset as isize + cb_moved) as usize,
|
||||
);
|
||||
}
|
||||
|
||||
*pcb_data += cb_moved as usize;
|
||||
*pcb_data = (*pcb_data as isize + cb_moved) as usize;
|
||||
|
||||
// Re-encode item and insert
|
||||
if cb_item != 0 {
|
||||
unsafe {
|
||||
p_temp = p_temp.offset(-(cb_len as isize));
|
||||
p_temp = p_temp.add(set_length(
|
||||
slice::from_raw_parts_mut(
|
||||
p_temp,
|
||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
||||
),
|
||||
cb_item,
|
||||
));
|
||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
||||
}
|
||||
offset -= cb_len;
|
||||
offset += set_length(&mut data[offset..], cb_item);
|
||||
data[offset..offset + cb_item].copy_from_slice(p_item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -219,10 +185,7 @@ pub(crate) fn read(txn: &Transaction<'_>, tag: u8) -> Result<Buffer, Error> {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy(data.as_ptr().add(offset), data.as_mut_ptr(), pcb_data);
|
||||
}
|
||||
|
||||
data.copy_within(offset..offset + pcb_data, 0);
|
||||
data.truncate(pcb_data);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
+15
-37
@@ -39,7 +39,6 @@
|
||||
|
||||
use crate::{consts::*, error::Error, serialization::*, yubikey::YubiKey};
|
||||
use log::error;
|
||||
use std::{ptr, slice};
|
||||
|
||||
/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots
|
||||
pub struct MsRoots(Vec<u8>);
|
||||
@@ -51,33 +50,22 @@ impl MsRoots {
|
||||
}
|
||||
|
||||
/// Read `msroots` file from YubiKey
|
||||
pub fn read(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
|
||||
let mut len: usize = 0;
|
||||
let mut ptr: *mut u8;
|
||||
let mut tag: u8;
|
||||
let mut offset: usize = 0;
|
||||
|
||||
let mut results = vec![];
|
||||
pub fn read(yubikey: &mut YubiKey) -> Result<Option<Self>, Error> {
|
||||
let cb_data = yubikey.obj_size_max();
|
||||
let txn = yubikey.begin_transaction()?;
|
||||
|
||||
// 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 {
|
||||
let mut buf = txn.fetch_object(object_id)?;
|
||||
let buf = txn.fetch_object(object_id)?;
|
||||
let cb_buf = buf.len();
|
||||
|
||||
ptr = buf.as_mut_ptr();
|
||||
|
||||
if cb_buf < CB_OBJ_TAG_MIN {
|
||||
return Ok(results);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
tag = *ptr;
|
||||
ptr = ptr.add(1);
|
||||
}
|
||||
let tag = buf[0];
|
||||
|
||||
if (TAG_MSROOTS_MID != tag || YKPIV_OBJ_MSROOTS5 == object_id)
|
||||
&& (TAG_MSROOTS_END != tag)
|
||||
@@ -85,38 +73,28 @@ impl MsRoots {
|
||||
// the current object doesn't contain a valid part of a msroots file
|
||||
|
||||
// treat condition as object isn't found
|
||||
return Ok(results);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr = ptr.add(get_length(
|
||||
slice::from_raw_parts(ptr, buf.as_ptr() as usize + buf.len() - ptr as usize),
|
||||
&mut len,
|
||||
));
|
||||
}
|
||||
let mut len: usize = 0;
|
||||
let offset = 1 + get_length(&buf[1..], &mut len);
|
||||
|
||||
// check that decoded length represents object contents
|
||||
if len > cb_buf - (ptr as isize - buf.as_mut_ptr() as isize) as usize {
|
||||
return Ok(results);
|
||||
if len > cb_buf - offset {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy(ptr, p_data.as_mut_ptr().add(offset), len);
|
||||
}
|
||||
|
||||
offset += len;
|
||||
|
||||
match MsRoots::new(&p_data[..offset]) {
|
||||
Ok(msroots) => results.push(msroots),
|
||||
Err(res) => error!("error parsing msroots: {:?}", res),
|
||||
}
|
||||
data.extend_from_slice(&buf[offset..offset + len]);
|
||||
|
||||
if tag == TAG_MSROOTS_END {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
MsRoots::new(&data).map(Some).map_err(|e| {
|
||||
error!("error parsing msroots: {:?}", e);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
/// Write `msroots` file to YubiKey
|
||||
|
||||
@@ -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
@@ -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
@@ -1,17 +1,22 @@
|
||||
//! YubiKey PC/SC transactions
|
||||
|
||||
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*, Buffer};
|
||||
use crate::{
|
||||
apdu::{Ins, APDU},
|
||||
error::Error,
|
||||
yubikey::*,
|
||||
};
|
||||
#[cfg(feature = "untested")]
|
||||
use crate::{
|
||||
apdu::{Response, StatusWords},
|
||||
consts::*,
|
||||
key::{AlgorithmId, SlotId},
|
||||
mgm::MgmKey,
|
||||
response::{Response, StatusWords},
|
||||
serialization::*,
|
||||
ObjectId,
|
||||
Buffer, ObjectId,
|
||||
};
|
||||
use log::{error, trace};
|
||||
use std::convert::TryInto;
|
||||
#[cfg(feature = "untested")]
|
||||
use std::ptr;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// Exclusive transaction with the YubiKey's PC/SC card.
|
||||
@@ -43,10 +48,10 @@ impl<'tx> Transaction<'tx> {
|
||||
/// single APDU messages at a time. For larger messages that need to be
|
||||
/// split into multiple APDUs, use the [`Transaction::transfer_data`]
|
||||
/// method instead.
|
||||
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Buffer, Error> {
|
||||
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Vec<u8>, Error> {
|
||||
trace!(">>> {:?}", send_buffer);
|
||||
|
||||
let mut recv_buffer = Zeroizing::new(vec![0u8; recv_len]);
|
||||
let mut recv_buffer = vec![0u8; recv_len];
|
||||
|
||||
let len = self
|
||||
.inner
|
||||
@@ -54,15 +59,12 @@ impl<'tx> Transaction<'tx> {
|
||||
.len();
|
||||
|
||||
recv_buffer.truncate(len);
|
||||
|
||||
trace!("<<< {:?}", recv_buffer.as_slice());
|
||||
|
||||
Ok(recv_buffer)
|
||||
}
|
||||
|
||||
/// Select application.
|
||||
pub fn select_application(&self) -> Result<(), Error> {
|
||||
let response = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let response = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(&AID)
|
||||
.transmit(self, 0xFF)
|
||||
@@ -85,21 +87,17 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Get the version of the PIV application installed on the YubiKey
|
||||
pub fn get_version(&self) -> Result<Version, Error> {
|
||||
// get version from device
|
||||
let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?;
|
||||
let response = APDU::new(Ins::GetVersion).transmit(self, 261)?;
|
||||
|
||||
if !response.is_success() {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
if response.buffer().len() < 3 {
|
||||
if response.data().len() < 3 {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
Ok(Version {
|
||||
major: response.buffer()[0],
|
||||
minor: response.buffer()[1],
|
||||
patch: response.buffer()[2],
|
||||
})
|
||||
Ok(Version::new(response.data()[..3].try_into().unwrap()))
|
||||
}
|
||||
|
||||
/// Get YubiKey device serial number
|
||||
@@ -108,7 +106,7 @@ impl<'tx> Transaction<'tx> {
|
||||
|
||||
let response = if version.major < 5 {
|
||||
// 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)
|
||||
.data(&yk_applet)
|
||||
.transmit(self, 0xFF)?
|
||||
@@ -130,7 +128,7 @@ impl<'tx> Transaction<'tx> {
|
||||
}
|
||||
|
||||
// reselect the PIV applet
|
||||
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let sw = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(&AID)
|
||||
.transmit(self, 0xFF)?
|
||||
@@ -144,7 +142,7 @@ impl<'tx> Transaction<'tx> {
|
||||
resp
|
||||
} else {
|
||||
// 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() {
|
||||
error!(
|
||||
@@ -157,7 +155,7 @@ impl<'tx> Transaction<'tx> {
|
||||
resp
|
||||
};
|
||||
|
||||
response.buffer()[..4]
|
||||
response.data()[..4]
|
||||
.try_into()
|
||||
.map(|serial| Serial::from(u32::from_be_bytes(serial)))
|
||||
.map_err(|_| Error::SizeError)
|
||||
@@ -166,20 +164,28 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Verify device PIN.
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> {
|
||||
// TODO(tarcieri): allow unpadded (with `0xFF`) PIN shorter than CB_PIN_MAX?
|
||||
if pin.len() != CB_PIN_MAX {
|
||||
if pin.len() > CB_PIN_MAX {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
let response = APDU::new(YKPIV_INS_VERIFY)
|
||||
.params(0x00, 0x80)
|
||||
.data(pin)
|
||||
.transmit(self, 261)?;
|
||||
let mut query = APDU::new(Ins::Verify);
|
||||
query.params(0x00, 0x80);
|
||||
|
||||
// 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() {
|
||||
StatusWords::Success => Ok(()),
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -187,44 +193,21 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Change the PIN
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
||||
let mut templ = [0, YKPIV_INS_CHANGE_REFERENCE, 0, 0x80];
|
||||
let mut indata = Zeroizing::new([0u8; 16]);
|
||||
let mut templ = [0, Ins::ChangeReference.code(), 0, 0x80];
|
||||
|
||||
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
if action == CHREF_ACT_UNBLOCK_PIN {
|
||||
templ[1] = YKPIV_INS_RESET_RETRY;
|
||||
templ[1] = Ins::ResetRetry.code();
|
||||
} else if action == CHREF_ACT_CHANGE_PUK {
|
||||
templ[3] = 0x81;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy(current_pin.as_ptr(), indata.as_mut_ptr(), current_pin.len());
|
||||
|
||||
if current_pin.len() < CB_PIN_MAX {
|
||||
ptr::write_bytes(
|
||||
indata.as_mut_ptr().add(current_pin.len()),
|
||||
0xff,
|
||||
CB_PIN_MAX - current_pin.len(),
|
||||
);
|
||||
}
|
||||
|
||||
ptr::copy(
|
||||
new_pin.as_ptr(),
|
||||
indata.as_mut_ptr().offset(8),
|
||||
new_pin.len(),
|
||||
);
|
||||
|
||||
if new_pin.len() < CB_PIN_MAX {
|
||||
ptr::write_bytes(
|
||||
indata.as_mut_ptr().offset(8).add(new_pin.len()),
|
||||
0xff,
|
||||
CB_PIN_MAX - new_pin.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut indata = Zeroizing::new([0xff; CB_PIN_MAX * 2]);
|
||||
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);
|
||||
|
||||
let status_words = self
|
||||
.transfer_data(&templ, indata.as_ref(), 0xFF)?
|
||||
@@ -233,7 +216,7 @@ impl<'tx> Transaction<'tx> {
|
||||
match status_words {
|
||||
StatusWords::Success => Ok(()),
|
||||
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!(
|
||||
"failed changing pin, token response code: {:x}.",
|
||||
@@ -261,7 +244,7 @@ impl<'tx> Transaction<'tx> {
|
||||
data[2] = DES_LEN_3DES as u8;
|
||||
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
|
||||
|
||||
let status_words = APDU::new(YKPIV_INS_SET_MGMKEY)
|
||||
let status_words = APDU::new(Ins::SetMgmKey)
|
||||
.params(0xff, p2)
|
||||
.data(&data)
|
||||
.transmit(self, 261)?
|
||||
@@ -284,20 +267,18 @@ impl<'tx> Transaction<'tx> {
|
||||
pub(crate) fn authenticated_command(
|
||||
&self,
|
||||
sign_in: &[u8],
|
||||
out: &mut [u8],
|
||||
out_len: &mut usize,
|
||||
algorithm: u8,
|
||||
key: u8,
|
||||
algorithm: AlgorithmId,
|
||||
key: SlotId,
|
||||
decipher: bool,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Buffer, Error> {
|
||||
let in_len = sign_in.len();
|
||||
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;
|
||||
|
||||
match algorithm {
|
||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
|
||||
let key_len = if algorithm == YKPIV_ALGO_RSA1024 {
|
||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||
let key_len = if let AlgorithmId::Rsa1024 = algorithm {
|
||||
128
|
||||
} else {
|
||||
256
|
||||
@@ -307,8 +288,8 @@ impl<'tx> Transaction<'tx> {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
}
|
||||
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
|
||||
let key_len = if algorithm == YKPIV_ALGO_ECCP256 {
|
||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||
let key_len = if let AlgorithmId::EccP256 = algorithm {
|
||||
32
|
||||
} else {
|
||||
48
|
||||
@@ -319,7 +300,6 @@ impl<'tx> Transaction<'tx> {
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::AlgorithmError),
|
||||
}
|
||||
|
||||
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);
|
||||
indata[offset] = 0x82;
|
||||
indata[offset + 1] = 0x00;
|
||||
indata[offset + 2] =
|
||||
if (algorithm == YKPIV_ALGO_ECCP256 || algorithm == YKPIV_ALGO_ECCP384) && decipher {
|
||||
0x85
|
||||
} else {
|
||||
0x81
|
||||
indata[offset + 2] = match (algorithm, decipher) {
|
||||
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
|
||||
_ => 0x81,
|
||||
};
|
||||
|
||||
offset += 3;
|
||||
@@ -363,7 +341,7 @@ impl<'tx> Transaction<'tx> {
|
||||
}
|
||||
}
|
||||
|
||||
let data = response.buffer();
|
||||
let data = response.data();
|
||||
|
||||
// skip the first 7c tag
|
||||
if data[0] != 0x7c {
|
||||
@@ -381,15 +359,7 @@ impl<'tx> Transaction<'tx> {
|
||||
|
||||
offset += 1;
|
||||
offset += get_length(&data[offset..], &mut len);
|
||||
|
||||
if len > *out_len {
|
||||
error!("wrong size on output buffer");
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
*out_len = len;
|
||||
out[..len].copy_from_slice(&data[offset..(offset + len)]);
|
||||
Ok(())
|
||||
Ok(Buffer::new(data[offset..(offset + len)].into()))
|
||||
}
|
||||
|
||||
/// Send/receive large amounts of data to/from the YubiKey, splitting long
|
||||
@@ -404,7 +374,7 @@ impl<'tx> Transaction<'tx> {
|
||||
max_out: usize,
|
||||
) -> Result<Response, Error> {
|
||||
let mut in_offset = 0;
|
||||
let mut out_data = Zeroizing::new(vec![]);
|
||||
let mut out_data = vec![];
|
||||
let mut sw = 0;
|
||||
|
||||
loop {
|
||||
@@ -432,17 +402,17 @@ impl<'tx> Transaction<'tx> {
|
||||
|
||||
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!(
|
||||
"output buffer too small: wanted to write {}, max was {}",
|
||||
out_data.len() - response.buffer().len() - 2,
|
||||
out_data.len() - response.data().len(),
|
||||
max_out
|
||||
);
|
||||
|
||||
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;
|
||||
if in_offset >= in_data.len() {
|
||||
@@ -456,24 +426,24 @@ impl<'tx> Transaction<'tx> {
|
||||
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();
|
||||
|
||||
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!(
|
||||
"output buffer too small: wanted to write {}, max was {}",
|
||||
out_data.len() + response.buffer().len() - 2,
|
||||
out_data.len() + response.data().len(),
|
||||
max_out
|
||||
);
|
||||
|
||||
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))
|
||||
@@ -483,7 +453,7 @@ impl<'tx> Transaction<'tx> {
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
|
||||
let mut indata = [0u8; 5];
|
||||
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff];
|
||||
let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
|
||||
|
||||
let mut inlen = indata.len();
|
||||
let indata_remaining = set_object(object_id, &mut indata);
|
||||
@@ -495,7 +465,7 @@ impl<'tx> Transaction<'tx> {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
let data = response.into_buffer();
|
||||
let data = Buffer::new(response.data().into());
|
||||
let mut outlen = 0;
|
||||
|
||||
if data.len() < 2 || !has_valid_length(&data[1..], data.len() - 1) {
|
||||
@@ -526,7 +496,7 @@ impl<'tx> Transaction<'tx> {
|
||||
/// Save an object
|
||||
#[cfg(feature = "untested")]
|
||||
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
|
||||
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff];
|
||||
let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
|
||||
|
||||
// TODO(tarcieri): replace with vector
|
||||
let mut data = [0u8; CB_BUF_MAX];
|
||||
|
||||
+197
-264
@@ -35,19 +35,32 @@
|
||||
|
||||
#[cfg(feature = "untested")]
|
||||
use crate::{
|
||||
apdu::APDU, key::SlotId, metadata, mgm::MgmKey, response::StatusWords, serialization::*,
|
||||
ObjectId,
|
||||
apdu::{Ins, StatusWords, APDU},
|
||||
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")]
|
||||
use getrandom::getrandom;
|
||||
use log::{error, info, warn};
|
||||
use pcsc::{Card, Context};
|
||||
use std::fmt::{self, Display};
|
||||
use pcsc::Card;
|
||||
#[cfg(feature = "untested")]
|
||||
use secrecy::ExposeSecret;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
#[cfg(feature = "untested")]
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
ptr, slice,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
#[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>
|
||||
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
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub struct Serial(pub u32);
|
||||
@@ -95,6 +111,17 @@ pub struct Version {
|
||||
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
|
||||
/// performing various operations.
|
||||
///
|
||||
@@ -104,103 +131,35 @@ pub struct Version {
|
||||
#[cfg_attr(not(feature = "untested"), allow(dead_code))]
|
||||
pub struct YubiKey {
|
||||
pub(crate) card: Card,
|
||||
pub(crate) pin: Option<Buffer>,
|
||||
pub(crate) pin: Option<CachedPin>,
|
||||
pub(crate) is_neo: bool,
|
||||
pub(crate) version: Version,
|
||||
pub(crate) serial: Serial,
|
||||
}
|
||||
|
||||
impl YubiKey {
|
||||
/// Open a connection to a YubiKey, optionally giving the name
|
||||
/// (needed if e.g. there are multiple YubiKeys connected).
|
||||
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> {
|
||||
let context = Context::establish(pcsc::Scope::System)?;
|
||||
let mut card = Self::connect(&context, name)?;
|
||||
/// Open a connection to a YubiKey.
|
||||
///
|
||||
/// Returns an error if there is more than one YubiKey detected.
|
||||
///
|
||||
/// If you need to operate in environments with more than one YubiKey
|
||||
/// attached to the same system, use [`yubikey_piv::Readers`] to select
|
||||
/// from the available PC/SC readers connected.
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let mut readers = Readers::open()?;
|
||||
let mut reader_iter = readers.iter()?;
|
||||
|
||||
let mut is_neo = false;
|
||||
let version: Version;
|
||||
let serial: Serial;
|
||||
|
||||
{
|
||||
let txn = Transaction::new(&mut card)?;
|
||||
let mut atr_buf = [0; CB_ATR_MAX];
|
||||
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
|
||||
if atr == YKPIV_ATR_NEO_R3 {
|
||||
is_neo = true;
|
||||
if let Some(reader) = reader_iter.next() {
|
||||
if reader_iter.next().is_some() {
|
||||
error!("multiple YubiKeys detected!");
|
||||
return Err(Error::PcscError { inner: None });
|
||||
}
|
||||
|
||||
txn.select_application()?;
|
||||
|
||||
// now that the PIV application is selected, retrieve the version
|
||||
// and serial number. Previously the NEO/YK4 required switching
|
||||
// to the yk applet to retrieve the serial, YK5 implements this
|
||||
// as a PIV applet command. Unfortunately, this change requires
|
||||
// that we retrieve the version number first, so that get_serial
|
||||
// can determine how to get the serial number, which for the NEO/Yk4
|
||||
// will result in another selection of the PIV applet.
|
||||
|
||||
version = txn.get_version().map_err(|e| {
|
||||
warn!("failed to retrieve version: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
serial = txn.get_serial(version).map_err(|e| {
|
||||
warn!("failed to retrieve serial number: '{}'", e);
|
||||
e
|
||||
})?;
|
||||
return reader.open();
|
||||
}
|
||||
|
||||
let yubikey = YubiKey {
|
||||
card,
|
||||
pin: None,
|
||||
is_neo,
|
||||
version,
|
||||
serial,
|
||||
};
|
||||
|
||||
Ok(yubikey)
|
||||
}
|
||||
|
||||
/// Connect to a YubiKey PC/SC card.
|
||||
fn connect(context: &Context, name: Option<&[u8]>) -> Result<Card, Error> {
|
||||
// ensure PC/SC context is valid
|
||||
context.is_valid()?;
|
||||
|
||||
let buffer_len = context.list_readers_len()?;
|
||||
let mut buffer = vec![0u8; buffer_len];
|
||||
|
||||
for reader in context.list_readers(&mut buffer)? {
|
||||
if let Some(wanted) = name {
|
||||
if reader.to_bytes() != wanted {
|
||||
warn!(
|
||||
"skipping reader '{}' since it doesn't match '{}'",
|
||||
reader.to_string_lossy(),
|
||||
String::from_utf8_lossy(wanted)
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
info!("trying to connect to reader '{}'", reader.to_string_lossy());
|
||||
|
||||
match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1) {
|
||||
Ok(card) => {
|
||||
info!("connected to '{}' successfully", reader.to_string_lossy());
|
||||
return Ok(card);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"skipping '{}' due to connection error: {}",
|
||||
reader.to_string_lossy(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("error: no usable reader found");
|
||||
Err(Error::PcscError { inner: None })
|
||||
error!("no YubiKey detected!");
|
||||
Err(Error::GenericError)
|
||||
}
|
||||
|
||||
/// Reconnect to a YubiKey
|
||||
@@ -214,8 +173,10 @@ impl YubiKey {
|
||||
pcsc::Disposition::ResetCard,
|
||||
)?;
|
||||
|
||||
// TODO(tarcieri): zeroize pin!
|
||||
let pin = self.pin.clone();
|
||||
let pin = self
|
||||
.pin
|
||||
.as_ref()
|
||||
.map(|p| Buffer::new(p.expose_secret().clone()));
|
||||
|
||||
let txn = Transaction::new(&mut self.card)?;
|
||||
txn.select_application()?;
|
||||
@@ -266,17 +227,17 @@ impl YubiKey {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// 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)
|
||||
.data(&[0x7c, 0x02, 0x80, 0x00])
|
||||
.transmit(&txn, 261)?;
|
||||
|
||||
if !challenge.is_success() || challenge.buffer().len() < 12 {
|
||||
if !challenge.is_success() || challenge.data().len() < 12 {
|
||||
return Err(Error::AuthenticationError);
|
||||
}
|
||||
|
||||
// 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];
|
||||
data[0] = 0x7c;
|
||||
@@ -295,7 +256,7 @@ impl YubiKey {
|
||||
let mut challenge = [0u8; 8];
|
||||
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)
|
||||
.data(&data)
|
||||
.transmit(&txn, 261)?;
|
||||
@@ -308,7 +269,7 @@ impl YubiKey {
|
||||
let response = mgm_key.encrypt(&challenge);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -320,7 +281,7 @@ impl YubiKey {
|
||||
pub fn deauthenticate(&mut self) -> Result<(), Error> {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
||||
let status_words = APDU::new(Ins::SelectApplication)
|
||||
.p1(0x04)
|
||||
.data(MGMT_AID)
|
||||
.transmit(&txn, 255)?
|
||||
@@ -342,15 +303,13 @@ impl YubiKey {
|
||||
pub fn sign_data(
|
||||
&mut self,
|
||||
raw_in: &[u8],
|
||||
sign_out: &mut [u8],
|
||||
out_len: &mut usize,
|
||||
algorithm: u8,
|
||||
algorithm: AlgorithmId,
|
||||
key: SlotId,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Buffer, Error> {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// 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
|
||||
@@ -358,15 +317,13 @@ impl YubiKey {
|
||||
pub fn decrypt_data(
|
||||
&mut self,
|
||||
input: &[u8],
|
||||
out: &mut [u8],
|
||||
out_len: &mut usize,
|
||||
algorithm: u8,
|
||||
algorithm: AlgorithmId,
|
||||
key: SlotId,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Buffer, Error> {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// 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.
|
||||
@@ -378,7 +335,7 @@ impl YubiKey {
|
||||
}
|
||||
|
||||
if !pin.is_empty() {
|
||||
self.pin = Some(Buffer::new(pin.into()))
|
||||
self.pin = Some(CachedPin::new(pin.into()))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -386,7 +343,7 @@ impl YubiKey {
|
||||
|
||||
/// Get the number of PIN retries
|
||||
#[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()?;
|
||||
|
||||
// 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
|
||||
#[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
|
||||
if pin_tries == 0 || puk_tries == 0 {
|
||||
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 templ = [
|
||||
0,
|
||||
YKPIV_INS_SET_PIN_RETRIES,
|
||||
pin_tries as u8,
|
||||
puk_tries as u8,
|
||||
];
|
||||
let templ = [0, Ins::SetPinRetries.code(), pin_tries, puk_tries];
|
||||
|
||||
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
||||
|
||||
@@ -444,7 +392,7 @@ impl YubiKey {
|
||||
}
|
||||
|
||||
if !new_pin.is_empty() {
|
||||
self.pin = Some(Buffer::new(new_pin.into()));
|
||||
self.pin = Some(CachedPin::new(new_pin.into()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -597,7 +545,7 @@ impl YubiKey {
|
||||
pub fn import_private_key(
|
||||
&mut self,
|
||||
key: SlotId,
|
||||
algorithm: u8,
|
||||
algorithm: AlgorithmId,
|
||||
p: Option<&[u8]>,
|
||||
q: Option<&[u8]>,
|
||||
dp: Option<&[u8]>,
|
||||
@@ -607,177 +555,106 @@ impl YubiKey {
|
||||
pin_policy: u8,
|
||||
touch_policy: u8,
|
||||
) -> 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 in_ptr: *mut u8 = key_data.as_mut_ptr();
|
||||
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);
|
||||
}
|
||||
let templ = [0, Ins::ImportKey.code(), algorithm.into(), key.into()];
|
||||
|
||||
if pin_policy != YKPIV_PINPOLICY_DEFAULT
|
||||
&& (pin_policy != YKPIV_PINPOLICY_NEVER)
|
||||
&& (pin_policy != YKPIV_PINPOLICY_ONCE)
|
||||
&& (pin_policy != YKPIV_PINPOLICY_ALWAYS)
|
||||
&& pin_policy != YKPIV_PINPOLICY_NEVER
|
||||
&& pin_policy != YKPIV_PINPOLICY_ONCE
|
||||
&& pin_policy != YKPIV_PINPOLICY_ALWAYS
|
||||
{
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
|
||||
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT
|
||||
&& (touch_policy != YKPIV_TOUCHPOLICY_NEVER)
|
||||
&& (touch_policy != YKPIV_TOUCHPOLICY_ALWAYS)
|
||||
&& (touch_policy != YKPIV_TOUCHPOLICY_CACHED)
|
||||
&& touch_policy != YKPIV_TOUCHPOLICY_NEVER
|
||||
&& touch_policy != YKPIV_TOUCHPOLICY_ALWAYS
|
||||
&& touch_policy != YKPIV_TOUCHPOLICY_CACHED
|
||||
{
|
||||
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 {
|
||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
|
||||
if p_len + q_len + dp_len + dq_len + qinv_len >= 1024 {
|
||||
return Err(Error::SizeError);
|
||||
} else {
|
||||
if algorithm == YKPIV_ALGO_RSA1024 {
|
||||
elem_len = 64;
|
||||
AlgorithmId::Rsa1024 => 64,
|
||||
AlgorithmId::Rsa2048 => 128,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
vec![p, q, dp, dq, qinv],
|
||||
0x01,
|
||||
)
|
||||
}
|
||||
|
||||
if algorithm == YKPIV_ALGO_RSA2048 {
|
||||
elem_len = 128;
|
||||
}
|
||||
|
||||
if p.is_null() || q.is_null() || dp.is_null() || dq.is_null() || qinv.is_null()
|
||||
{
|
||||
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::GenericError),
|
||||
},
|
||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => match ec_data {
|
||||
Some(ec_data) => {
|
||||
if ec_data.len() >= key_data.len() {
|
||||
// This can never be true, but check to be explicit.
|
||||
return Err(Error::SizeError);
|
||||
}
|
||||
|
||||
if algorithm == YKPIV_ALGO_ECCP256 {
|
||||
elem_len = 32;
|
||||
} else if algorithm == YKPIV_ALGO_ECCP384 {
|
||||
elem_len = 48;
|
||||
(
|
||||
match algorithm {
|
||||
AlgorithmId::EccP256 => 32,
|
||||
AlgorithmId::EccP384 => 48,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
vec![ec_data],
|
||||
0x06,
|
||||
)
|
||||
}
|
||||
_ => return Err(Error::GenericError),
|
||||
},
|
||||
};
|
||||
|
||||
if ec_data.is_null() {
|
||||
return Err(Error::GenericError);
|
||||
}
|
||||
let mut offset = 0;
|
||||
|
||||
params[0] = ec_data;
|
||||
lens[0] = ec_data_len;
|
||||
param_tag = 0x6;
|
||||
n_params = 1;
|
||||
}
|
||||
_ => return Err(Error::AlgorithmError),
|
||||
}
|
||||
for (i, param) in params.into_iter().enumerate() {
|
||||
key_data[offset] = param_tag + i as u8;
|
||||
offset += 1;
|
||||
|
||||
for i in 0..n_params {
|
||||
unsafe {
|
||||
*in_ptr = (param_tag + i as i32) as u8;
|
||||
in_ptr = in_ptr.offset(1);
|
||||
offset += set_length(&mut key_data[offset..], elem_len);
|
||||
|
||||
in_ptr = in_ptr.add(set_length(
|
||||
slice::from_raw_parts_mut(
|
||||
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;
|
||||
let padding = elem_len - param.len();
|
||||
let remaining = key_data.len() - offset;
|
||||
|
||||
if padding > remaining {
|
||||
return Err(Error::AlgorithmError);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::write_bytes(in_ptr, 0, padding);
|
||||
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]);
|
||||
for b in &mut key_data[offset..offset + padding] {
|
||||
*b = 0;
|
||||
}
|
||||
offset += padding;
|
||||
key_data[offset..offset + param.len()].copy_from_slice(param);
|
||||
offset += param.len();
|
||||
}
|
||||
|
||||
if pin_policy != YKPIV_PINPOLICY_DEFAULT {
|
||||
unsafe {
|
||||
*in_ptr = YKPIV_PINPOLICY_TAG;
|
||||
*in_ptr.add(1) = 0x01;
|
||||
*in_ptr.add(2) = pin_policy;
|
||||
in_ptr = in_ptr.add(3);
|
||||
}
|
||||
key_data[offset] = YKPIV_PINPOLICY_TAG;
|
||||
key_data[offset + 1] = 0x01;
|
||||
key_data[offset + 2] = pin_policy;
|
||||
offset += 3;
|
||||
}
|
||||
|
||||
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
|
||||
unsafe {
|
||||
*in_ptr = YKPIV_TOUCHPOLICY_TAG;
|
||||
*in_ptr.add(1) = 0x01;
|
||||
*in_ptr.add(2) = touch_policy;
|
||||
in_ptr = in_ptr.add(3);
|
||||
}
|
||||
key_data[offset] = YKPIV_TOUCHPOLICY_TAG;
|
||||
key_data[offset + 1] = 0x01;
|
||||
key_data[offset + 2] = touch_policy;
|
||||
offset += 3;
|
||||
}
|
||||
|
||||
let txn = self.begin_transaction()?;
|
||||
let len = in_ptr as usize - key_data.as_mut_ptr() as usize;
|
||||
|
||||
let status_words = txn
|
||||
.transfer_data(&templ, &key_data[..len], 256)?
|
||||
.transfer_data(&templ, &key_data[..offset], 256)?
|
||||
.status_words();
|
||||
|
||||
match status_words {
|
||||
@@ -791,7 +668,7 @@ impl YubiKey {
|
||||
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
||||
#[cfg(feature = "untested")]
|
||||
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 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);
|
||||
}
|
||||
|
||||
Ok(response.into_buffer())
|
||||
Ok(Buffer::new(response.data().into()))
|
||||
}
|
||||
|
||||
/// Get an auth challenge
|
||||
@@ -815,7 +692,7 @@ impl YubiKey {
|
||||
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
|
||||
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)
|
||||
.data(&[0x7c, 0x02, 0x81, 0x00])
|
||||
.transmit(&txn, 261)?;
|
||||
@@ -824,7 +701,7 @@ impl YubiKey {
|
||||
return Err(Error::AuthenticationError);
|
||||
}
|
||||
|
||||
Ok(response.buffer()[4..12].try_into().unwrap())
|
||||
Ok(response.data()[4..12].try_into().unwrap())
|
||||
}
|
||||
|
||||
/// Verify an auth response
|
||||
@@ -840,7 +717,7 @@ impl YubiKey {
|
||||
let txn = self.begin_transaction()?;
|
||||
|
||||
// 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)
|
||||
.data(&data)
|
||||
.transmit(&txn, 261)?
|
||||
@@ -860,7 +737,7 @@ impl YubiKey {
|
||||
/// The reset function is only available when both pins are blocked.
|
||||
#[cfg(feature = "untested")]
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fn connect() {
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
}
|
||||
|
||||
let mut yubikey = YubiKey::open(None).unwrap();
|
||||
let mut yubikey = YubiKey::open().unwrap();
|
||||
dbg!(&yubikey.version());
|
||||
dbg!(&yubikey.serial());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user