Compare commits
245 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7628ebf605 | |||
| 0688dbf30d | |||
| 7e3d0bc838 | |||
| fc62fc286d | |||
| 199496ab01 | |||
| ab11e037cc | |||
| 41d4d1b332 | |||
| b21c4bd307 | |||
| 64aea57a63 | |||
| 17ae87f741 | |||
| 811a7d5373 | |||
| 5b2e025aa4 | |||
| 6313e2ec3f | |||
| cbe60413cb | |||
| 08220032db | |||
| 152a096a6b | |||
| ddb712cc48 | |||
| 32dcdfb67f | |||
| c396971496 | |||
| f906e6a2d7 | |||
| 35fa940a37 | |||
| 05a3b85934 | |||
| 23d0f96adc | |||
| 4435a54435 | |||
| 55b960501a | |||
| 860c163eb9 | |||
| 15f9e265e6 | |||
| 556b9cb671 | |||
| 842198b566 | |||
| 812b4d588f | |||
| 6e3560c10f | |||
| 0f907ebd5c | |||
| acc96e988f | |||
| 4037dfe19d | |||
| a9e27eaadb | |||
| 9208cc3862 | |||
| 2d4f2fa750 | |||
| 3a5411d989 | |||
| ed66d399ca | |||
| a018728f7a | |||
| 39d2b0982a | |||
| e501cce644 | |||
| 5e52f93f4a | |||
| c8cdff2bbf | |||
| 9d18a2cb08 | |||
| 3aeaa7e1da | |||
| 38cce09a78 | |||
| 6f3d65cfc0 | |||
| 14cba00dcd | |||
| ce9b32a27e | |||
| 9042454404 | |||
| bf376516c9 | |||
| 46685da68a | |||
| 3c70c77e95 | |||
| 154d8bb97a | |||
| 31617bf9a8 | |||
| 2144750f6f | |||
| cbbbdd6d8c | |||
| 9478cd392f | |||
| a70013d048 | |||
| e4418edb32 | |||
| 89086bb1ac | |||
| 3a0262ce90 | |||
| f4b1e8b6e2 | |||
| 926450b573 | |||
| fec43589af | |||
| 4617ce053d | |||
| 8bc7b471f6 | |||
| a1ef807dea | |||
| d67ae3b7e3 | |||
| 268b3709fd | |||
| b8eee17284 | |||
| 9c566c9130 | |||
| 3c07ee55f7 | |||
| 264e6e0684 | |||
| fa5a87269b | |||
| 33d6330186 | |||
| e0fbd57937 | |||
| fada2a5b78 | |||
| 2f4c1cfb11 | |||
| 85deed78ec | |||
| ed5134e9a2 | |||
| 4770505de0 | |||
| fdb7b891ad | |||
| 548d39d138 | |||
| 15aee107ad | |||
| 8c65f37d97 | |||
| 8802ecb689 | |||
| 27504890d7 | |||
| 4999139b9d | |||
| b3490b4f39 | |||
| b4a43f21d0 | |||
| ee84134f26 | |||
| 3da9b3001b | |||
| 61054ae74a | |||
| 7cebce6156 | |||
| 81e9c54a2c | |||
| ae20e6d950 | |||
| 741ba528a4 | |||
| aaaf3b142e | |||
| b5e774cf2b | |||
| 15a590dd93 | |||
| e6d9003d09 | |||
| 4dd0f7b31e | |||
| 2d57b8e2e1 | |||
| fe18a1d2e4 | |||
| d4838f2652 | |||
| 1dff6eca32 | |||
| 90a44b38cb | |||
| 4cc785fee9 | |||
| 422f89b3e9 | |||
| 976af45e22 | |||
| ae4e098a41 | |||
| 985b1d272c | |||
| 58acfe6330 | |||
| 02ade49288 | |||
| 1a95a5f921 | |||
| 620f2bcc74 | |||
| 0e14110e17 | |||
| 8ac78cafb8 | |||
| 5e8a014be2 | |||
| d44a32453c | |||
| 220c045dcb | |||
| 6174b62a77 | |||
| 36408ac658 | |||
| 16a9a1a2c6 | |||
| cee7f1cef8 | |||
| cb104f3df6 | |||
| ac338cf17a | |||
| a8ea3ec8b7 | |||
| d113c1f4b9 | |||
| 2eff313064 | |||
| 41b10d1f23 | |||
| 4c2ecea721 | |||
| e73607e662 | |||
| 17839da94f | |||
| 08897ec7c9 | |||
| 26c777b6ec | |||
| 1bf3b13e52 | |||
| 8385dda201 | |||
| 363bdc4351 | |||
| da828abe3c | |||
| 339fb69e30 | |||
| 78d5f33695 | |||
| 283e6fe363 | |||
| 55d077dd80 | |||
| fd77ba6e74 | |||
| 855f2ecb24 | |||
| 6436d9afcb | |||
| 4663cffb96 | |||
| fb7e95e6d1 | |||
| 0a100acdd2 | |||
| 39a81fc300 | |||
| 31efd4e78c | |||
| 86b8c6a6db | |||
| 104020d518 | |||
| 4dfac56753 | |||
| 9482ae62ab | |||
| 2587a4ac1e | |||
| 3cf3c0867f | |||
| b2f11f5058 | |||
| cdecfd92dd | |||
| 509c438330 | |||
| f6915ce5df | |||
| 962089dbf8 | |||
| d6cd0130d3 | |||
| 7d01dba11d | |||
| d1d384d304 | |||
| cb9d5221b2 | |||
| c30cf5b83a | |||
| 3c88f1be13 | |||
| 0551263286 | |||
| 63fbc1dcf2 | |||
| 82c2d08aec | |||
| f25e14c52c | |||
| b1e8702059 | |||
| f4f7041626 | |||
| d6ad70f7d1 | |||
| 76c093e68e | |||
| ada3454d26 | |||
| 370a90f800 | |||
| 7bcd8664a4 | |||
| 3a4515d902 | |||
| 7b70ea0f91 | |||
| 9bc28f4f75 | |||
| 140016bbd7 | |||
| 3a41fdc3bc | |||
| c377f226e2 | |||
| da897b99bb | |||
| 9fa2d1c051 | |||
| 07f70bccb5 | |||
| 8e1469cff6 | |||
| 9ce2ffe938 | |||
| 589ca3de12 | |||
| a9e0363d09 | |||
| bfd728d1ac | |||
| a110289910 | |||
| b9d6057d4e | |||
| 2087e53109 | |||
| 5f5844ccb4 | |||
| ae071e706c | |||
| cd704c28d7 | |||
| 3a283aca40 | |||
| e72ee5c60e | |||
| 9ee1494c6f | |||
| d3e565ef55 | |||
| 2bdeca0069 | |||
| bc95d8b7b9 | |||
| 11c93d6421 | |||
| afca0fec0a | |||
| c8837d485f | |||
| 12b5bd1e3c | |||
| c3698dcffb | |||
| 6a16c59567 | |||
| c8e5c96398 | |||
| 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 |
@@ -0,0 +1,8 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '13:00'
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches: develop
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUSTFLAGS: "-Dwarnings"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- run: sudo apt-get install libpcsclite-dev
|
||||||
|
- run: cargo check
|
||||||
|
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: ubuntu-latest
|
||||||
|
toolchain: stable
|
||||||
|
deps: sudo apt-get install libpcsclite-dev
|
||||||
|
- platform: windows-latest
|
||||||
|
toolchain: stable
|
||||||
|
deps: true
|
||||||
|
- platform: macos-latest
|
||||||
|
toolchain: stable
|
||||||
|
deps: true
|
||||||
|
- platform: ubuntu-latest
|
||||||
|
toolchain: 1.44.0 # MSRV
|
||||||
|
deps: sudo apt-get install libpcsclite-dev
|
||||||
|
- platform: windows-latest
|
||||||
|
toolchain: 1.44.0 # MSRV
|
||||||
|
deps: true
|
||||||
|
- platform: macos-latest
|
||||||
|
toolchain: 1.44.0 # MSRV
|
||||||
|
deps: true
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
override: true
|
||||||
|
- run: ${{ matrix.deps }}
|
||||||
|
- run: cargo build --all --all-features --release
|
||||||
|
- run: cargo test --all --all-features --release
|
||||||
|
|
||||||
|
rustfmt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install stable toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: Run cargo fmt
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: 1.44.0 # MSRV
|
||||||
|
components: clippy
|
||||||
|
- run: sudo apt-get install libpcsclite-dev
|
||||||
|
- run: cargo clippy --all --exclude crypto_box --all-features -- -D warnings
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
push:
|
|
||||||
branches: master
|
|
||||||
|
|
||||||
name: Rust
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install libpcsclite-dev
|
|
||||||
run: sudo apt-get install libpcsclite-dev
|
|
||||||
|
|
||||||
- name: Run cargo check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
|
|
||||||
# Need to install `libpscslite-dev` on Linux
|
|
||||||
linux:
|
|
||||||
name: Test Suite
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
toolchain:
|
|
||||||
- 1.39.0
|
|
||||||
- stable
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install libpcsclite-dev
|
|
||||||
run: sudo apt-get install libpcsclite-dev
|
|
||||||
|
|
||||||
- name: Run cargo test
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --release
|
|
||||||
|
|
||||||
- name: Run cargo build --all-features
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
args: --all-features
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test Suite
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- macos-latest
|
|
||||||
- windows-latest
|
|
||||||
toolchain:
|
|
||||||
- 1.39.0
|
|
||||||
- stable
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Run cargo test
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --release
|
|
||||||
|
|
||||||
- name: Run cargo build --all-features
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
args: --all-features
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
name: Rustfmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install rustfmt
|
|
||||||
run: rustup component add rustfmt
|
|
||||||
|
|
||||||
- name: Run cargo fmt
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
|
|
||||||
clippy:
|
|
||||||
name: Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install libpcsclite-dev
|
|
||||||
run: sudo apt-get install libpcsclite-dev
|
|
||||||
|
|
||||||
- name: Install clippy
|
|
||||||
run: rustup component add clippy
|
|
||||||
|
|
||||||
- name: Run cargo clippy
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
args: -- -D warnings
|
|
||||||
|
|
||||||
# TODO: use actions-rs/audit-check
|
|
||||||
security_audit:
|
|
||||||
name: Security Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
|
|
||||||
- name: Install stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Install cargo audit
|
|
||||||
run: cargo install cargo-audit
|
|
||||||
|
|
||||||
- name: Run cargo audit
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: audit
|
|
||||||
args: --deny-warnings
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Security Audit
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths: Cargo.lock
|
||||||
|
push:
|
||||||
|
branches: develop
|
||||||
|
paths: Cargo.lock
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# TODO: use actions-rs/audit-check
|
||||||
|
security_audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install stable toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo audit
|
||||||
|
run: cargo install cargo-audit
|
||||||
|
|
||||||
|
- name: Run cargo audit
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: audit
|
||||||
|
args: --deny-warnings --ignore RUSTSEC-2019-0031 # spin
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
|
||||||
|
|||||||
+82
-13
@@ -4,7 +4,77 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.0.2] (2019-11-25)
|
## 0.1.0 (2020-10-19)
|
||||||
|
### Added
|
||||||
|
- `Certificate::generate_self_signed` ([#80])
|
||||||
|
- `YubiKey::open_by_serial` ([#69])
|
||||||
|
- CCCID/CHUID tests and cleanups ([#65])
|
||||||
|
- Test `Config::get` ([#64])
|
||||||
|
- Test `Key::list` ([#61])
|
||||||
|
- Test `YubiKey::verify_pin` ([#60])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bump `crypto-mac`, `des`, `hmac`, `pbkdf2` ([#177])
|
||||||
|
- Bump `p256` to v0.5; `p384` to v0.4; MSRV 1.44+ ([#175])
|
||||||
|
- Refactor key import function ([#128])
|
||||||
|
- Extract `ChangeRefAction` enum ([#82])
|
||||||
|
- TLV extraction ([#73])
|
||||||
|
- Rename `container` to `mscmap` ([#68])
|
||||||
|
- Finish eliminating `consts` module ([#67])
|
||||||
|
- Move `sign`/`decrypt`/`import`/`attest` to the `key` module ([#62])
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `pcsc::Error::NoReadersAvailable` -> `Error::NotFound` in `YubiKey::open*` ([#88])
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- YubiKey NEO support ([#63])
|
||||||
|
|
||||||
|
[#177]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/177
|
||||||
|
[#175]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/175
|
||||||
|
[#128]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/128
|
||||||
|
[#82]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/82
|
||||||
|
[#73]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/73
|
||||||
|
[#88]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/88
|
||||||
|
[#80]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/80
|
||||||
|
[#69]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/69
|
||||||
|
[#68]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/68
|
||||||
|
[#67]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/67
|
||||||
|
[#65]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/65
|
||||||
|
[#64]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/64
|
||||||
|
[#63]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/63
|
||||||
|
[#62]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/62
|
||||||
|
[#61]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/61
|
||||||
|
[#60]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/60
|
||||||
|
|
||||||
|
## 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])
|
||||||
|
|
||||||
|
[#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
|
### Added
|
||||||
- `untested` Cargo feature to mark untested functionality ([#30])
|
- `untested` Cargo feature to mark untested functionality ([#30])
|
||||||
- Initial connect test and docs ([#19])
|
- Initial connect test and docs ([#19])
|
||||||
@@ -19,17 +89,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Use `log` crate for logging ([#7])
|
- Use `log` crate for logging ([#7])
|
||||||
- Replace `ErrorKind::Ok` with `Result` ([#6])
|
- Replace `ErrorKind::Ok` with `Result` ([#6])
|
||||||
|
|
||||||
[0.0.2]: https://github.com/tarcieri/yubikey-piv.rs/pull/31
|
[#30]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/30
|
||||||
[#30]: https://github.com/tarcieri/yubikey-piv.rs/pull/30
|
[#19]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/19
|
||||||
[#19]: https://github.com/tarcieri/yubikey-piv.rs/pull/19
|
[#17]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/17
|
||||||
[#17]: https://github.com/tarcieri/yubikey-piv.rs/pull/17
|
[#15]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/15
|
||||||
[#15]: https://github.com/tarcieri/yubikey-piv.rs/pull/15
|
[#13]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/13
|
||||||
[#13]: https://github.com/tarcieri/yubikey-piv.rs/pull/13
|
[#10]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/10
|
||||||
[#10]: https://github.com/tarcieri/yubikey-piv.rs/pull/10
|
[#9]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/9
|
||||||
[#9]: https://github.com/tarcieri/yubikey-piv.rs/pull/9
|
[#8]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/8
|
||||||
[#8]: https://github.com/tarcieri/yubikey-piv.rs/pull/8
|
[#7]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/7
|
||||||
[#7]: https://github.com/tarcieri/yubikey-piv.rs/pull/7
|
[#6]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/6
|
||||||
[#6]: https://github.com/tarcieri/yubikey-piv.rs/pull/6
|
|
||||||
|
|
||||||
## 0.0.1 (2019-11-18)
|
## 0.0.1 (2019-11-18)
|
||||||
- It typechecks, ship it!
|
- Initial release
|
||||||
|
|||||||
+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
|
contributors and maintainers pledge to making participation in our project and
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||||
nationality, personal appearance, race, religion, or sexual identity and
|
education, socio-economic status, nationality, personal appearance, race,
|
||||||
orientation.
|
religion, or sexual identity and orientation.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ include:
|
|||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
advances
|
advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or electronic
|
||||||
@@ -55,8 +55,8 @@ further defined and clarified by project maintainers.
|
|||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at [bascule@gmail.com]. All
|
reported by contacting the project team at [oss@iqlusion.io](mailto:oss@iqlusion.io).
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
All complaints will be reviewed and investigated and will result in a response that
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
Further details of specific enforcement policies may be posted separately.
|
Further details of specific enforcement policies may be posted separately.
|
||||||
@@ -65,12 +65,10 @@ Project maintainers who do not follow or enforce the Code of Conduct in good
|
|||||||
faith may face temporary or permanent repercussions as determined by other
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
members of the project's leadership.
|
members of the project's leadership.
|
||||||
|
|
||||||
[bascule@gmail.com]: mailto:bascule@gmail.com
|
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
available at [http://contributor-covenant.org/version/1/4][version]
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
|
|||||||
Generated
+1097
File diff suppressed because it is too large
Load Diff
+30
-9
@@ -1,36 +1,57 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey-piv"
|
name = "yubikey-piv"
|
||||||
version = "0.0.2" # Also update html_root_url in lib.rs when bumping this
|
version = "0.1.0" # Also update html_root_url in lib.rs when bumping this
|
||||||
description = """
|
description = """
|
||||||
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
||||||
application providing general-purpose public-key signing and encryption
|
application providing general-purpose public-key signing and encryption
|
||||||
with hardware-backed private keys for RSA (2048/1024) and ECC (P-256/P-384)
|
with hardware-backed private keys for RSA (2048/1024) and ECC (P-256/P-384)
|
||||||
algorithms (e.g, PKCS#1v1.5, ECDSA)
|
algorithms (e.g, PKCS#1v1.5, ECDSA)
|
||||||
"""
|
"""
|
||||||
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
|
authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
repository = "https://github.com/tarcieri/yubikey-piv.rs"
|
repository = "https://github.com/iqlusioninc/yubikey-piv.rs"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["api-bindings", "cryptography", "hardware-support"]
|
categories = ["api-bindings", "cryptography", "hardware-support"]
|
||||||
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
|
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [".", "cli"]
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
maintenance = { status = "experimental" }
|
maintenance = { status = "experimental" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
des = "0.3"
|
chrono = "0.4"
|
||||||
|
cookie-factory = "0.3"
|
||||||
|
der-parser = "4"
|
||||||
|
des = "0.6"
|
||||||
|
elliptic-curve = "0.6"
|
||||||
getrandom = "0.1"
|
getrandom = "0.1"
|
||||||
hmac = "0.7"
|
hmac = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pbkdf2 = "0.3"
|
nom = "5"
|
||||||
|
num-bigint = { version = "0.6", features = ["rand"], package = "num-bigint-dig" }
|
||||||
|
num-traits = "0.2"
|
||||||
|
num-integer = "0.1"
|
||||||
|
pbkdf2 = "0.6"
|
||||||
|
p256 = "0.5"
|
||||||
|
p384 = "0.4"
|
||||||
pcsc = "2"
|
pcsc = "2"
|
||||||
sha-1 = "0.8"
|
rsa = "0.3"
|
||||||
|
secrecy = "0.7"
|
||||||
|
sha-1 = "0.9"
|
||||||
|
sha2 = "0.9"
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
|
subtle-encoding = "0.5"
|
||||||
|
x509 = "0.1.2"
|
||||||
|
x509-parser = "0.8"
|
||||||
zeroize = "1"
|
zeroize = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.7"
|
env_logger = "0.8"
|
||||||
|
ring = "0.16.15"
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
untested = []
|
untested = []
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<img src="https://raw.githubusercontent.com/tendermint/yubihsm-rs/develop/img/logo.png" width="150" height="110">
|
<img src="https://raw.githubusercontent.com/iqlusioninc/yubikey-piv.rs/develop/img/logo.png" width="150" height="110">
|
||||||
|
|
||||||
# yubikey-piv.rs
|
# yubikey-piv.rs
|
||||||
|
|
||||||
[![crate][crate-image]][crate-link]
|
[![crate][crate-image]][crate-link]
|
||||||
[![Docs][docs-image]][docs-link]
|
[![Docs][docs-image]][docs-link]
|
||||||
![Apache2/MIT licensed][license-image]
|
[![2-Clause BSD Licensed][license-image]][license-link]
|
||||||
![Rust Version][rustc-image]
|
![Rust Version][rustc-image]
|
||||||
![Maintenance Status: Experimental][maintenance-image]
|
![Maintenance Status: Experimental][maintenance-image]
|
||||||
|
[![Safety Dance][safety-image]][safety-link]
|
||||||
[![Build Status][build-image]][build-link]
|
[![Build Status][build-image]][build-link]
|
||||||
[![Gitter Chat][gitter-image]][gitter-link]
|
[![Gitter Chat][gitter-image]][gitter-link]
|
||||||
|
|
||||||
@@ -35,15 +36,15 @@ endorsed by Yubico.
|
|||||||
|
|
||||||
## Minimum Supported Rust Version
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
- Rust **1.39+**
|
- Rust **1.44** or newer
|
||||||
|
|
||||||
## Supported YubiKeys
|
## Supported YubiKeys
|
||||||
|
|
||||||
- [YubiKey NEO] series (may be dropped in the future, see [#18])
|
|
||||||
- [YubiKey 4] series
|
- [YubiKey 4] series
|
||||||
- [YubiKey 5] series
|
- [YubiKey 5] series
|
||||||
|
|
||||||
NOTE: Nano and USB-C variants of the above are also supported
|
NOTE: Nano and USB-C variants of the above are also supported.
|
||||||
|
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
|
||||||
|
|
||||||
## Security Warning
|
## Security Warning
|
||||||
|
|
||||||
@@ -64,19 +65,20 @@ functions of the YubiKey:
|
|||||||
| | Module | Issue | Description |
|
| | Module | Issue | Description |
|
||||||
|----|---------------|-------|-------------|
|
|----|---------------|-------|-------------|
|
||||||
| 🚧 | `yubikey` | [#20] | Core functionality: auth, keys, PIN/PUK, encrypt, sign, attest |
|
| 🚧 | `yubikey` | [#20] | Core functionality: auth, keys, PIN/PUK, encrypt, sign, attest |
|
||||||
| ⚠️ | `cccid` | [#21] | Cardholder Capability Container (CCC) IDs |
|
| 🚧 | `cccid` | [#21] | Cardholder Capability Container (CCC) IDs |
|
||||||
| ⚠️ | `certificate` | [#22] | Certificates for stored keys |
|
| 🚧️ | `certificate` | [#22] | Certificates for stored keys |
|
||||||
| ⚠️ | `chuid` | [#23] | Cardholder Unique Identifier (CHUID) |
|
| 🚧 | `chuid` | [#23] | Cardholder Unique Identifier (CHUID) |
|
||||||
| ⚠️ | `config` | [#24] | Support for reading on-key configuration |
|
| ✅️ | `config` | [#24] | Support for reading on-key configuration |
|
||||||
| ⚠️ | `container` | [#25] | MS Container Map Records |
|
| 🚧 | `key` | [#26] | Crypto key management: list, generate, import |
|
||||||
| ⚠️ | `key` | [#26] | Crypto key management: list, generate, import |
|
| 🚧 | `mgm` | [#26] | Management Key (MGM) support: set, get, derive |
|
||||||
| ⚠️ | `mgm` | [#26] | Management Key (MGM) support: set, get, derive
|
| ⚠️ | `mscmap` | [#25] | MS Container Map Records |
|
||||||
| ⚠️ | `msroots` | [#28] | `msroots` file: PKCS#7 formatted certificate store for enterprise trusted roots |
|
| ⚠️ | `msroots` | [#28] | `msroots` file: PKCS#7 formatted certificate store for enterprise trusted roots |
|
||||||
|
|
||||||
Legend:
|
Legend:
|
||||||
|
|
||||||
| | Description |
|
| | Description |
|
||||||
|----|------------------------------------|
|
|----|------------------------------------|
|
||||||
|
| ✅ | Working |
|
||||||
| 🚧 | Testing and validation in progress |
|
| 🚧 | Testing and validation in progress |
|
||||||
| ⚠️ | Untested support |
|
| ⚠️ | Untested support |
|
||||||
|
|
||||||
@@ -115,12 +117,15 @@ Application Protocol Data Unit (APDU) messages, use the `trace` log level:
|
|||||||
running 1 test
|
running 1 test
|
||||||
[INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
[INFO yubikey_piv::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
||||||
[INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
[INFO yubikey_piv::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
||||||
|
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: SelectApplication, p1: 4, p2: 0, data: [160, 0, 0, 3, 8] }
|
||||||
[TRACE yubikey_piv::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8]
|
[TRACE yubikey_piv::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8]
|
||||||
[TRACE yubikey_piv::transaction] <<< [97, 17, 79, 6, 0, 0, 16, 0, 1, 0, 121, 7, 79, 5, 160, 0, 0, 3, 8, 144, 0]
|
[TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [97, 17, 79, 6, 0, 0, 16, 0, 1, 0, 121, 7, 79, 5, 160, 0, 0, 3, 8] }
|
||||||
|
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetVersion, p1: 0, p2: 0, data: [] }
|
||||||
[TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0]
|
[TRACE yubikey_piv::transaction] >>> [0, 253, 0, 0, 0]
|
||||||
[TRACE yubikey_piv::transaction] <<< [5, 1, 2, 144, 0]
|
[TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [5, 1, 2] }
|
||||||
|
[TRACE yubikey_piv::apdu] >>> APDU { cla: 0, ins: GetSerial, p1: 0, p2: 0, data: [] }
|
||||||
[TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0]
|
[TRACE yubikey_piv::transaction] >>> [0, 248, 0, 0, 0]
|
||||||
[TRACE yubikey_piv::transaction] <<< [0, 115, 0, 178, 144, 0]
|
[TRACE yubikey_piv::apdu] <<< Response { status_words: Success, data: [0, 115, 0, 178] }
|
||||||
test connect ... ok
|
test connect ... ok
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ Yubico's [yubico-piv-tool], a C library/CLI program. The original library
|
|||||||
was licensed under a [2-Clause BSD License][BSDL], which this library inherits
|
was licensed under a [2-Clause BSD License][BSDL], which this library inherits
|
||||||
as a derived work.
|
as a derived work.
|
||||||
|
|
||||||
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
|
Copyright (c) 2014-2020 Yubico AB, Tony Arcieri
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -188,12 +193,15 @@ or conditions.
|
|||||||
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
||||||
[docs-link]: https://docs.rs/yubikey-piv/
|
[docs-link]: https://docs.rs/yubikey-piv/
|
||||||
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
||||||
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
|
[license-link]: https://github.com/iqlusioninc/yubikey-piv.rs/blob/develop/COPYING
|
||||||
|
[rustc-image]: https://img.shields.io/badge/rustc-1.44+-blue.svg
|
||||||
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
|
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
|
||||||
[build-image]: https://github.com/tarcieri/yubikey-piv.rs/workflows/Rust/badge.svg
|
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||||
[build-link]: https://github.com/tarcieri/yubikey-piv.rs/actions
|
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
||||||
[gitter-image]: https://badges.gitter.im/yubihsm-piv-rs.svg
|
[build-image]: https://github.com/iqlusioninc/yubikey-piv.rs/workflows/CI/badge.svg?branch=develop&event=push
|
||||||
[gitter-link]: https://gitter.im/yubikey-piv-rs/community
|
[build-link]: https://github.com/iqlusioninc/yubikey-piv.rs/actions
|
||||||
|
[gitter-image]: https://badges.gitter.im/badge.svg
|
||||||
|
[gitter-link]: https://gitter.im/iqlusioninc/community
|
||||||
|
|
||||||
[//]: # (general links)
|
[//]: # (general links)
|
||||||
|
|
||||||
@@ -206,18 +214,18 @@ or conditions.
|
|||||||
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
||||||
[Corrode]: https://github.com/jameysharp/corrode
|
[Corrode]: https://github.com/jameysharp/corrode
|
||||||
[cc-web]: https://contributor-covenant.org/
|
[cc-web]: https://contributor-covenant.org/
|
||||||
[cc-md]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
|
[cc-md]: https://github.com/iqlusioninc/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
|
||||||
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
||||||
|
|
||||||
[//]: # (github issues)
|
[//]: # (github issues)
|
||||||
|
|
||||||
[#18]: https://github.com/tarcieri/yubikey-piv.rs/issues/18
|
[#18]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/18
|
||||||
[#20]: https://github.com/tarcieri/yubikey-piv.rs/issues/20
|
[#20]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/20
|
||||||
[#21]: https://github.com/tarcieri/yubikey-piv.rs/issues/21
|
[#21]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/21
|
||||||
[#22]: https://github.com/tarcieri/yubikey-piv.rs/issues/22
|
[#22]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/22
|
||||||
[#23]: https://github.com/tarcieri/yubikey-piv.rs/issues/23
|
[#23]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/23
|
||||||
[#24]: https://github.com/tarcieri/yubikey-piv.rs/issues/24
|
[#24]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/24
|
||||||
[#25]: https://github.com/tarcieri/yubikey-piv.rs/issues/25
|
[#25]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/25
|
||||||
[#26]: https://github.com/tarcieri/yubikey-piv.rs/issues/26
|
[#26]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/26
|
||||||
[#27]: https://github.com/tarcieri/yubikey-piv.rs/issues/27
|
[#27]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/27
|
||||||
[#28]: https://github.com/tarcieri/yubikey-piv.rs/issues/28
|
[#28]: https://github.com/iqlusioninc/yubikey-piv.rs/issues/28
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 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.1.0 (2020-10-19)
|
||||||
|
### Added
|
||||||
|
- `status` command ([#72], [#74])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bump `yubikey-piv` to v0.1.0 ([#180])
|
||||||
|
- Bump `x509-parser` to v0.8 ([#181])
|
||||||
|
- Bump `sha2` to v0.9 ([#182])
|
||||||
|
- Rename `list` command to `readers`; improve usage ([#71])
|
||||||
|
|
||||||
|
[#182]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/182
|
||||||
|
[#181]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/181
|
||||||
|
[#180]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/180
|
||||||
|
[#74]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/74
|
||||||
|
[#72]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/72
|
||||||
|
[#71]: https://github.com/iqlusioninc/yubikey-piv.rs/pull/71
|
||||||
|
|
||||||
|
## 0.0.1 (2019-12-02)
|
||||||
|
- Initial release
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "yubikey-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = """
|
||||||
|
Command-line interface for performing encryption and signing using RSA/ECC keys
|
||||||
|
stored on YubiKey devices.
|
||||||
|
"""
|
||||||
|
authors = ["Tony Arcieri <tony@iqlusion.io>"]
|
||||||
|
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.8"
|
||||||
|
env_logger = "0.8"
|
||||||
|
lazy_static = "1"
|
||||||
|
log = "0.4"
|
||||||
|
sha2 = "0.9"
|
||||||
|
subtle-encoding = "0.5"
|
||||||
|
termcolor = "1"
|
||||||
|
x509-parser = "0.8"
|
||||||
|
yubikey-piv = { version = "0.1", path = ".." }
|
||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
<img src="https://raw.githubusercontent.com/iqlusioninc/yubikey-piv.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 4] series
|
||||||
|
- [YubiKey 5] series
|
||||||
|
|
||||||
|
NOTE: Nano and USB-C variants of the above are also supported.
|
||||||
|
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
|
||||||
|
|
||||||
|
## Security Warning
|
||||||
|
|
||||||
|
No security audits of this crate have ever been performed. Presently it is in
|
||||||
|
an experimental stage and may still contain high-severity issues.
|
||||||
|
|
||||||
|
USE AT YOUR OWN RISK!
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
WIP. Check back later.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
||||||
|
|
||||||
|
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
### Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally
|
||||||
|
submitted for inclusion in the work by you shall be licensed under the
|
||||||
|
[2-Clause BSD License][BSDL] as shown above, without any additional terms
|
||||||
|
or conditions.
|
||||||
|
|
||||||
|
[//]: # (badges)
|
||||||
|
|
||||||
|
[crate-image]: https://img.shields.io/crates/v/yubikey-cli.svg
|
||||||
|
[crate-link]: https://crates.io/crates/yubikey-cli
|
||||||
|
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
|
||||||
|
[docs-link]: https://docs.rs/yubikey-cli/
|
||||||
|
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
||||||
|
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
|
||||||
|
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
|
||||||
|
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||||
|
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
||||||
|
[build-image]: https://github.com/iqlusioninc/yubikey-piv.rs/workflows/Rust/badge.svg?branch=develop&event=push
|
||||||
|
[build-link]: https://github.com/iqlusioninc/yubikey-piv.rs/actions
|
||||||
|
[gitter-image]: https://badges.gitter.im/badge.svg
|
||||||
|
[gitter-link]: https://gitter.im/iqlusioninc/community
|
||||||
|
|
||||||
|
[//]: # (general links)
|
||||||
|
|
||||||
|
[PIV]: https://piv.idmanagement.gov/
|
||||||
|
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
||||||
|
[Yubico]: https://www.yubico.com/
|
||||||
|
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
|
||||||
|
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
|
||||||
|
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
||||||
|
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
||||||
|
[Corrode]: https://github.com/jameysharp/corrode
|
||||||
|
[cc-web]: https://contributor-covenant.org/
|
||||||
|
[cc-md]: https://github.com/iqlusioninc/yubikey-cli.rs/blob/develop/CODE_OF_CONDUCT.md
|
||||||
|
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//! `yubikey` command-line utility
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(
|
||||||
|
missing_docs,
|
||||||
|
rust_2018_idioms,
|
||||||
|
unused_lifetimes,
|
||||||
|
unused_qualifications
|
||||||
|
)]
|
||||||
|
|
||||||
|
use gumdrop::Options;
|
||||||
|
use yubikey_cli::commands::YubiKeyCli;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
YubiKeyCli::parse_args_default_or_exit().run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
//! Commands of the CLI application
|
||||||
|
|
||||||
|
pub mod readers;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
|
use self::{readers::ReadersCmd, status::StatusCmd};
|
||||||
|
use crate::terminal::{self, STDOUT};
|
||||||
|
use gumdrop::Options;
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
io::{self, Write},
|
||||||
|
process::exit,
|
||||||
|
};
|
||||||
|
use termcolor::{ColorChoice, ColorSpec, WriteColor};
|
||||||
|
use yubikey_piv::{Serial, YubiKey};
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Specify the serial number of the YubiKey to connect to
|
||||||
|
#[options(
|
||||||
|
short = "s",
|
||||||
|
long = "serial",
|
||||||
|
help = "serial number of the YubiKey to connect to"
|
||||||
|
)]
|
||||||
|
pub serial: Option<Serial>,
|
||||||
|
|
||||||
|
/// Subcommand to execute.
|
||||||
|
#[options(command)]
|
||||||
|
pub command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YubiKeyCli {
|
||||||
|
/// Print usage information
|
||||||
|
pub fn print_usage() -> Result<(), io::Error> {
|
||||||
|
let mut stdout = STDOUT.lock();
|
||||||
|
stdout.reset()?;
|
||||||
|
|
||||||
|
let mut bold = ColorSpec::new();
|
||||||
|
bold.set_bold(true);
|
||||||
|
|
||||||
|
stdout.set_color(&bold)?;
|
||||||
|
writeln!(
|
||||||
|
stdout,
|
||||||
|
"{} {}",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
)?;
|
||||||
|
stdout.reset()?;
|
||||||
|
|
||||||
|
writeln!(stdout, "{}", env!("CARGO_PKG_AUTHORS"))?;
|
||||||
|
writeln!(stdout, "{}", env!("CARGO_PKG_DESCRIPTION").trim())?;
|
||||||
|
writeln!(stdout)?;
|
||||||
|
writeln!(stdout, "{}", Commands::usage())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the underlying command type or print usage info and exit
|
||||||
|
pub fn run(&self) {
|
||||||
|
// TODO(tarcieri): make this more configurable
|
||||||
|
terminal::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(self.yubikey_init()),
|
||||||
|
None => Self::print_usage().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the YubiKey client driver
|
||||||
|
fn yubikey_init(&self) -> YubiKey {
|
||||||
|
match self.serial {
|
||||||
|
Some(serial) => YubiKey::open_by_serial(serial).unwrap_or_else(|e| {
|
||||||
|
status_err!("couldn't open YubiKey (serial #{}): {}", serial, e);
|
||||||
|
exit(1);
|
||||||
|
}),
|
||||||
|
None => YubiKey::open().unwrap_or_else(|e| {
|
||||||
|
status_err!("couldn't open default YubiKey: {}", e);
|
||||||
|
exit(1);
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
|
||||||
|
/// `readers` subcommand
|
||||||
|
#[options(help = "list detected readers")]
|
||||||
|
Readers(ReadersCmd),
|
||||||
|
|
||||||
|
/// `status` subcommand
|
||||||
|
#[options(help = "show yubikey status")]
|
||||||
|
Status(StatusCmd),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commands {
|
||||||
|
/// Run the given command
|
||||||
|
pub fn run(&self, yubikey: YubiKey) {
|
||||||
|
match self {
|
||||||
|
Commands::Help(help) => help.run(),
|
||||||
|
Commands::Version(version) => version.run(),
|
||||||
|
Commands::Readers(list) => list.run(),
|
||||||
|
Commands::Status(status) => status.run(yubikey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,64 @@
|
|||||||
|
//! List detected readers
|
||||||
|
|
||||||
|
use crate::terminal::STDOUT;
|
||||||
|
use gumdrop::Options;
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
process::exit,
|
||||||
|
};
|
||||||
|
use termcolor::{ColorSpec, StandardStreamLock, WriteColor};
|
||||||
|
use yubikey_piv::{Readers, Serial};
|
||||||
|
|
||||||
|
/// The `readers` subcommand
|
||||||
|
#[derive(Debug, Options)]
|
||||||
|
pub struct ReadersCmd {}
|
||||||
|
|
||||||
|
impl ReadersCmd {
|
||||||
|
/// Run the `readers` 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = STDOUT.lock();
|
||||||
|
s.reset().unwrap();
|
||||||
|
|
||||||
|
for (i, reader) in readers_iter.enumerate() {
|
||||||
|
let name = reader.name();
|
||||||
|
let yubikey = match reader.open() {
|
||||||
|
Ok(yk) => yk,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serial = yubikey.serial();
|
||||||
|
self.print_reader(&mut s, i + 1, &name, serial).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a reader
|
||||||
|
fn print_reader(
|
||||||
|
&self,
|
||||||
|
stream: &mut StandardStreamLock<'_>,
|
||||||
|
index: usize,
|
||||||
|
name: &str,
|
||||||
|
serial: Serial,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
stream.set_color(ColorSpec::new().set_bold(true))?;
|
||||||
|
write!(stream, "{:>3}:", index)?;
|
||||||
|
stream.reset()?;
|
||||||
|
writeln!(stream, " {} (serial: {})", name, serial)?;
|
||||||
|
stream.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//! Print device status
|
||||||
|
|
||||||
|
use crate::terminal::STDOUT;
|
||||||
|
use gumdrop::Options;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use termcolor::{ColorSpec, StandardStreamLock, WriteColor};
|
||||||
|
use yubikey_piv::{key::*, YubiKey};
|
||||||
|
|
||||||
|
use crate::print_cert_info;
|
||||||
|
|
||||||
|
// String to use for `None`
|
||||||
|
const NONE_STR: &str = "<none>";
|
||||||
|
|
||||||
|
/// The `status` subcommand
|
||||||
|
#[derive(Debug, Options)]
|
||||||
|
pub struct StatusCmd {}
|
||||||
|
|
||||||
|
impl StatusCmd {
|
||||||
|
/// Run the `status` subcommand
|
||||||
|
pub fn run(&self, mut yk: YubiKey) {
|
||||||
|
let mut s = STDOUT.lock();
|
||||||
|
s.reset().unwrap();
|
||||||
|
|
||||||
|
self.attr(&mut s, "name", yk.name()).unwrap();
|
||||||
|
self.attr(&mut s, "version", yk.version()).unwrap();
|
||||||
|
self.attr(&mut s, "serial", yk.serial()).unwrap();
|
||||||
|
|
||||||
|
if let Ok(chuid) = yk.chuid() {
|
||||||
|
self.attr(&mut s, "CHUID", chuid).unwrap();
|
||||||
|
} else {
|
||||||
|
self.attr(&mut s, "CHUID", NONE_STR).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(chuid) = yk.cccid() {
|
||||||
|
self.attr(&mut s, "CCC", chuid).unwrap();
|
||||||
|
} else {
|
||||||
|
self.attr(&mut s, "CCC", NONE_STR).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.attr(&mut s, "PIN retries", yk.get_pin_retries().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for slot in SLOTS.iter().cloned() {
|
||||||
|
print_cert_info(&mut yk, slot, &mut s).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a status attribute
|
||||||
|
fn attr(
|
||||||
|
&self,
|
||||||
|
stream: &mut StandardStreamLock<'_>,
|
||||||
|
name: &str,
|
||||||
|
value: impl ToString,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
stream.set_color(ColorSpec::new().set_bold(true))?;
|
||||||
|
write!(stream, "{:>12}:", name)?;
|
||||||
|
stream.reset()?;
|
||||||
|
writeln!(stream, " {}", value.to_string())?;
|
||||||
|
stream.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! `yubikey` command-line utility
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(
|
||||||
|
missing_docs,
|
||||||
|
rust_2018_idioms,
|
||||||
|
unused_lifetimes,
|
||||||
|
unused_qualifications
|
||||||
|
)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
pub mod terminal;
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::str;
|
||||||
|
use subtle_encoding::hex;
|
||||||
|
use termcolor::{ColorSpec, StandardStreamLock, WriteColor};
|
||||||
|
use x509_parser::parse_x509_der;
|
||||||
|
use yubikey_piv::{certificate::Certificate, key::*, YubiKey};
|
||||||
|
|
||||||
|
///Write information about certificate found in slot a la yubico-piv-tool output.
|
||||||
|
pub fn print_cert_info(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
stream: &mut StandardStreamLock<'_>,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
let cert = match Certificate::read(yubikey, slot) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error reading certificate in slot {:?}: {}", slot, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let buf = cert.into_buffer();
|
||||||
|
|
||||||
|
if !buf.is_empty() {
|
||||||
|
let fingerprint = Sha256::digest(&buf);
|
||||||
|
let slot_id: u8 = slot.into();
|
||||||
|
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
|
||||||
|
match parse_x509_der(&buf) {
|
||||||
|
Ok((_rem, cert)) => {
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Algorithm",
|
||||||
|
cert.tbs_certificate.subject_pki.algorithm.algorithm,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?;
|
||||||
|
print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Fingerprint",
|
||||||
|
str::from_utf8(hex::encode(fingerprint).as_slice()).unwrap(),
|
||||||
|
)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Not Before",
|
||||||
|
cert.tbs_certificate.validity.not_before.to_rfc2822(),
|
||||||
|
)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Not After",
|
||||||
|
cert.tbs_certificate.validity.not_after.to_rfc2822(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("Failed to parse certificate");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a status attribute
|
||||||
|
fn print_cert_attr(
|
||||||
|
stream: &mut StandardStreamLock<'_>,
|
||||||
|
name: &str,
|
||||||
|
value: impl ToString,
|
||||||
|
) -> Result<(), io::Error> {
|
||||||
|
stream.set_color(ColorSpec::new().set_bold(true))?;
|
||||||
|
write!(stream, "{:>12}:", name)?;
|
||||||
|
stream.reset()?;
|
||||||
|
writeln!(stream, " {}", value.to_string())?;
|
||||||
|
stream.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -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::terminal::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::terminal::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::terminal::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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+333
-31
@@ -30,8 +30,8 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{error::Error, response::Response, transaction::Transaction, Buffer};
|
use crate::{error::Error, transaction::Transaction, Buffer};
|
||||||
use std::fmt::{self, Debug};
|
use log::trace;
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
/// Maximum amount of command data that can be included in an APDU
|
/// Maximum amount of command data that can be included in an APDU
|
||||||
@@ -40,13 +40,13 @@ const APDU_DATA_MAX: usize = 0xFF;
|
|||||||
/// Application Protocol Data Unit (APDU).
|
/// Application Protocol Data Unit (APDU).
|
||||||
///
|
///
|
||||||
/// These messages are packets used to communicate with the YubiKey.
|
/// These messages are packets used to communicate with the YubiKey.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub(crate) struct APDU {
|
pub(crate) struct APDU {
|
||||||
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
|
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
|
||||||
cla: u8,
|
cla: u8,
|
||||||
|
|
||||||
/// Instruction code: indicates the specific command (e.g. "write data")
|
/// Instruction code: indicates the specific command (e.g. "write data")
|
||||||
ins: u8,
|
ins: Ins,
|
||||||
|
|
||||||
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
|
/// Instruction parameter 1 for the command (e.g. offset into file at which to write the data)
|
||||||
p1: u8,
|
p1: u8,
|
||||||
@@ -60,10 +60,10 @@ pub(crate) struct APDU {
|
|||||||
|
|
||||||
impl APDU {
|
impl APDU {
|
||||||
/// Create a new APDU with the given instruction code
|
/// Create a new APDU with the given instruction code
|
||||||
pub fn new(ins: u8) -> Self {
|
pub fn new(ins: impl Into<Ins>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cla: 0,
|
cla: 0,
|
||||||
ins,
|
ins: ins.into(),
|
||||||
p1: 0,
|
p1: 0,
|
||||||
p2: 0,
|
p2: 0,
|
||||||
data: vec![],
|
data: vec![],
|
||||||
@@ -71,7 +71,6 @@ impl APDU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set this APDU's class
|
/// Set this APDU's class
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn cla(&mut self, value: u8) -> &mut Self {
|
pub fn cla(&mut self, value: u8) -> &mut Self {
|
||||||
self.cla = value;
|
self.cla = value;
|
||||||
self
|
self
|
||||||
@@ -84,7 +83,6 @@ impl APDU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set both parameters for this APDU
|
/// Set both parameters for this APDU
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
|
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
|
||||||
self.p1 = p1;
|
self.p1 = p1;
|
||||||
self.p2 = p2;
|
self.p2 = p2;
|
||||||
@@ -112,15 +110,17 @@ impl APDU {
|
|||||||
|
|
||||||
/// Transmit this APDU using the given card transaction
|
/// Transmit this APDU using the given card transaction
|
||||||
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> {
|
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response, Error> {
|
||||||
let response_bytes = txn.transmit(&self.to_bytes(), recv_len)?;
|
trace!(">>> {:?}", self);
|
||||||
Ok(Response::from_bytes(response_bytes))
|
let response = Response::from(txn.transmit(&self.to_bytes(), recv_len)?);
|
||||||
|
trace!("<<< {:?}", &response);
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume this APDU and return a self-zeroizing buffer
|
/// Serialize this APDU as a self-zeroizing byte buffer
|
||||||
pub fn to_bytes(&self) -> Buffer {
|
pub fn to_bytes(&self) -> Buffer {
|
||||||
let mut bytes = Vec::with_capacity(5 + self.data.len());
|
let mut bytes = Vec::with_capacity(5 + self.data.len());
|
||||||
bytes.push(self.cla);
|
bytes.push(self.cla);
|
||||||
bytes.push(self.ins);
|
bytes.push(self.ins.code());
|
||||||
bytes.push(self.p1);
|
bytes.push(self.p1);
|
||||||
bytes.push(self.p2);
|
bytes.push(self.p2);
|
||||||
bytes.push(self.data.len() as u8);
|
bytes.push(self.data.len() as u8);
|
||||||
@@ -129,21 +129,6 @@ impl APDU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for APDU {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"APDU {{ cla: {}, ins: {}, p1: {}, p2: {}, lc: {}, data: {:?} }}",
|
|
||||||
self.cla,
|
|
||||||
self.ins,
|
|
||||||
self.p1,
|
|
||||||
self.p2,
|
|
||||||
self.data.len(),
|
|
||||||
self.data.as_slice()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for APDU {
|
impl Drop for APDU {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.zeroize();
|
self.zeroize();
|
||||||
@@ -152,10 +137,327 @@ impl Drop for APDU {
|
|||||||
|
|
||||||
impl Zeroize for APDU {
|
impl Zeroize for APDU {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.cla.zeroize();
|
// Only `data` may contain secrets
|
||||||
self.ins.zeroize();
|
|
||||||
self.p1.zeroize();
|
|
||||||
self.p2.zeroize();
|
|
||||||
self.data.zeroize();
|
self.data.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// APDU instruction codes
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Ins {
|
||||||
|
/// Verify
|
||||||
|
Verify,
|
||||||
|
|
||||||
|
/// Change reference
|
||||||
|
ChangeReference,
|
||||||
|
|
||||||
|
/// Reset retry
|
||||||
|
ResetRetry,
|
||||||
|
|
||||||
|
/// Generate asymmetric
|
||||||
|
GenerateAsymmetric,
|
||||||
|
|
||||||
|
/// Authenticate
|
||||||
|
Authenticate,
|
||||||
|
|
||||||
|
/// Get data
|
||||||
|
GetData,
|
||||||
|
|
||||||
|
/// Put data
|
||||||
|
PutData,
|
||||||
|
|
||||||
|
/// Select application
|
||||||
|
SelectApplication,
|
||||||
|
|
||||||
|
/// Get response APDU
|
||||||
|
GetResponseApdu,
|
||||||
|
|
||||||
|
// Yubico vendor specific instructions
|
||||||
|
// <https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html>
|
||||||
|
/// Set MGM key
|
||||||
|
SetMgmKey,
|
||||||
|
|
||||||
|
/// Import key
|
||||||
|
ImportKey,
|
||||||
|
|
||||||
|
/// Get version
|
||||||
|
GetVersion,
|
||||||
|
|
||||||
|
/// Reset device
|
||||||
|
Reset,
|
||||||
|
|
||||||
|
/// Set PIN retries
|
||||||
|
SetPinRetries,
|
||||||
|
|
||||||
|
/// Generate attestation certificate for asymmetric key
|
||||||
|
Attest,
|
||||||
|
|
||||||
|
/// Get device serial
|
||||||
|
GetSerial,
|
||||||
|
|
||||||
|
/// Other/unrecognized instruction codes
|
||||||
|
Other(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ins {
|
||||||
|
/// Get the code that corresponds to this instruction
|
||||||
|
pub fn code(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Ins::Verify => 0x20,
|
||||||
|
Ins::ChangeReference => 0x24,
|
||||||
|
Ins::ResetRetry => 0x2c,
|
||||||
|
Ins::GenerateAsymmetric => 0x47,
|
||||||
|
Ins::Authenticate => 0x87,
|
||||||
|
Ins::GetData => 0xcb,
|
||||||
|
Ins::PutData => 0xdb,
|
||||||
|
Ins::SelectApplication => 0xa4,
|
||||||
|
Ins::GetResponseApdu => 0xc0,
|
||||||
|
Ins::SetMgmKey => 0xff,
|
||||||
|
Ins::ImportKey => 0xfe,
|
||||||
|
Ins::GetVersion => 0xfd,
|
||||||
|
Ins::Reset => 0xfb,
|
||||||
|
Ins::SetPinRetries => 0xfa,
|
||||||
|
Ins::Attest => 0xf9,
|
||||||
|
Ins::GetSerial => 0xf8,
|
||||||
|
Ins::Other(code) => code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for Ins {
|
||||||
|
fn from(code: u8) -> Self {
|
||||||
|
match code {
|
||||||
|
0x20 => Ins::Verify,
|
||||||
|
0x24 => Ins::ChangeReference,
|
||||||
|
0x2c => Ins::ResetRetry,
|
||||||
|
0x47 => Ins::GenerateAsymmetric,
|
||||||
|
0x87 => Ins::Authenticate,
|
||||||
|
0xcb => Ins::GetData,
|
||||||
|
0xdb => Ins::PutData,
|
||||||
|
0xa4 => Ins::SelectApplication,
|
||||||
|
0xc0 => Ins::GetResponseApdu,
|
||||||
|
0xff => Ins::SetMgmKey,
|
||||||
|
0xfe => Ins::ImportKey,
|
||||||
|
0xfd => Ins::GetVersion,
|
||||||
|
0xfb => Ins::Reset,
|
||||||
|
0xfa => Ins::SetPinRetries,
|
||||||
|
0xf9 => Ins::Attest,
|
||||||
|
0xf8 => Ins::GetSerial,
|
||||||
|
code => Ins::Other(code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ins> for u8 {
|
||||||
|
fn from(ins: Ins) -> u8 {
|
||||||
|
ins.code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// APDU responses
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct Response {
|
||||||
|
/// Status words
|
||||||
|
status_words: StatusWords,
|
||||||
|
|
||||||
|
/// Buffer
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
/// Create a new response from the given status words and buffer
|
||||||
|
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.
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+57
-13
@@ -30,8 +30,22 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, yubikey::YubiKey};
|
use crate::{error::Error, yubikey::YubiKey};
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
|
use subtle_encoding::hex;
|
||||||
|
|
||||||
|
/// CCCID size
|
||||||
|
pub const CCCID_SIZE: usize = 14;
|
||||||
|
|
||||||
|
/// CCC size
|
||||||
|
pub const CCC_SIZE: usize = 51;
|
||||||
|
|
||||||
|
/// CCCID offset
|
||||||
|
const CCC_ID_OFFS: usize = 9;
|
||||||
|
|
||||||
|
/// CCC Object ID
|
||||||
|
const OBJ_CAPABILITY: u32 = 0x005f_c107;
|
||||||
|
|
||||||
/// Cardholder Capability Container (CCC) Template
|
/// Cardholder Capability Container (CCC) Template
|
||||||
///
|
///
|
||||||
@@ -48,38 +62,68 @@ const CCC_TMPL: &[u8] = &[
|
|||||||
0x00, 0xfe, 0x00,
|
0x00, 0xfe, 0x00,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Cardholder Capability Container (CCC) Identifier
|
/// Cardholder Capability Container (CCC) Identifier Card ID
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct CCCID([u8; YKPIV_CCCID_SIZE]);
|
pub struct CardId(pub [u8; CCCID_SIZE]);
|
||||||
|
|
||||||
impl CCCID {
|
impl CardId {
|
||||||
/// Generate a random CCCID
|
/// Generate a random CCC Card ID
|
||||||
pub fn generate() -> Result<Self, Error> {
|
pub fn generate() -> Result<Self, Error> {
|
||||||
let mut id = [0u8; YKPIV_CCCID_SIZE];
|
let mut id = [0u8; CCCID_SIZE];
|
||||||
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
|
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
|
||||||
Ok(CCCID(id))
|
Ok(Self(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cardholder Capability Container (CCC) Identifier
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct CCC(pub [u8; CCC_SIZE]);
|
||||||
|
|
||||||
|
impl CCC {
|
||||||
|
/// Return CardId component of CCC
|
||||||
|
pub fn cccid(&self) -> Result<CardId, Error> {
|
||||||
|
let mut cccid = [0u8; CCCID_SIZE];
|
||||||
|
cccid.copy_from_slice(&self.0[CCC_ID_OFFS..(CCC_ID_OFFS + CCCID_SIZE)]);
|
||||||
|
Ok(CardId(cccid))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Cardholder Capability Container (CCC) ID
|
/// Get Cardholder Capability Container (CCC) ID
|
||||||
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> {
|
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let response = txn.fetch_object(YKPIV_OBJ_CAPABILITY)?;
|
let response = txn.fetch_object(OBJ_CAPABILITY)?;
|
||||||
|
|
||||||
if response.len() != CCC_TMPL.len() {
|
if response.len() != CCC_TMPL.len() {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cccid = [0u8; YKPIV_CCCID_SIZE];
|
let mut ccc = [0u8; CCC_SIZE];
|
||||||
cccid.copy_from_slice(&response[CCC_ID_OFFS..(CCC_ID_OFFS + YKPIV_CCCID_SIZE)]);
|
ccc.copy_from_slice(&response[0..CCC_SIZE]);
|
||||||
Ok(CCCID(cccid))
|
Ok(Self(ccc))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Cardholder Capability Container (CCC) ID
|
/// Get Cardholder Capability Container (CCC) ID
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||||
let mut buf = CCC_TMPL.to_vec();
|
let mut buf = CCC_TMPL.to_vec();
|
||||||
buf[CCC_ID_OFFS..(CCC_ID_OFFS + self.0.len())].copy_from_slice(&self.0);
|
buf[0..self.0.len()].copy_from_slice(&self.0);
|
||||||
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
txn.save_object(YKPIV_OBJ_CAPABILITY, &buf)
|
txn.save_object(OBJ_CAPABILITY, &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for CCC {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "CCC({:?})", &self.0[..])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CCC {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
String::from_utf8(hex::encode(&self.0[..])).unwrap()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+556
-67
@@ -31,23 +31,407 @@
|
|||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::*,
|
|
||||||
error::Error,
|
error::Error,
|
||||||
key::{self, SlotId},
|
key::{sign_data, AlgorithmId, SlotId},
|
||||||
serialization::*,
|
serialization::*,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
yubikey::YubiKey,
|
yubikey::YubiKey,
|
||||||
Buffer,
|
Buffer,
|
||||||
};
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::ptr;
|
use num_bigint::BigUint;
|
||||||
|
use p256::NistP256;
|
||||||
|
use p384::NistP384;
|
||||||
|
use rsa::{PublicKeyParts, RSAPublicKey};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
use x509_parser::{parse_x509_der, x509::SubjectPublicKeyInfo};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::CB_OBJ_MAX;
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
|
||||||
|
const TAG_CERT: u8 = 0x70;
|
||||||
|
const TAG_CERT_COMPRESS: u8 = 0x71;
|
||||||
|
const TAG_CERT_LRC: u8 = 0xFE;
|
||||||
|
|
||||||
|
/// A serial number for a [`Certificate`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Serial(BigUint);
|
||||||
|
|
||||||
|
impl From<BigUint> for Serial {
|
||||||
|
fn from(num: BigUint) -> Serial {
|
||||||
|
Serial(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 20]> for Serial {
|
||||||
|
fn from(bytes: [u8; 20]) -> Serial {
|
||||||
|
Serial(BigUint::from_bytes_be(&bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for Serial {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(bytes: &[u8]) -> Result<Serial, ()> {
|
||||||
|
if bytes.len() <= 20 {
|
||||||
|
Ok(Serial(BigUint::from_bytes_be(&bytes)))
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serial {
|
||||||
|
fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
self.0.to_bytes_be()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about how a [`Certificate`] is stored within a YubiKey.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum CertInfo {
|
||||||
|
/// The certificate is uncompressed.
|
||||||
|
Uncompressed,
|
||||||
|
|
||||||
|
/// The certificate is gzip-compressed.
|
||||||
|
Gzip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for CertInfo {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x00 => Ok(CertInfo::Uncompressed),
|
||||||
|
0x01 => Ok(CertInfo::Gzip),
|
||||||
|
_ => Err(Error::InvalidObject),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CertInfo> for u8 {
|
||||||
|
fn from(certinfo: CertInfo) -> u8 {
|
||||||
|
match certinfo {
|
||||||
|
CertInfo::Uncompressed => 0x00,
|
||||||
|
CertInfo::Gzip => 0x01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl x509::AlgorithmIdentifier for AlgorithmId {
|
||||||
|
type AlgorithmOid = &'static [u64];
|
||||||
|
|
||||||
|
fn algorithm(&self) -> Self::AlgorithmOid {
|
||||||
|
match self {
|
||||||
|
// RSA encryption
|
||||||
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => &[1, 2, 840, 113_549, 1, 1, 1],
|
||||||
|
// EC Public Key
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => &[1, 2, 840, 10045, 2, 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters<W: std::io::Write>(
|
||||||
|
&self,
|
||||||
|
w: cookie_factory::WriteContext<W>,
|
||||||
|
) -> cookie_factory::GenResult<W> {
|
||||||
|
use x509::der::write::der_oid;
|
||||||
|
|
||||||
|
// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
|
||||||
|
// ```text
|
||||||
|
// ECParameters ::= CHOICE {
|
||||||
|
// namedCurve OBJECT IDENTIFIER
|
||||||
|
// -- implicitCurve NULL
|
||||||
|
// -- specifiedCurve SpecifiedECDomain
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
match self {
|
||||||
|
AlgorithmId::EccP256 => der_oid(&[1, 2, 840, 10045, 3, 1, 7][..])(w),
|
||||||
|
AlgorithmId::EccP384 => der_oid(&[1, 3, 132, 0, 34][..])(w),
|
||||||
|
_ => Ok(w),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(EcPublicKey<NistP256>),
|
||||||
|
|
||||||
|
/// EC P-384 keys
|
||||||
|
EcP384(EcPublicKey<NistP384>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => EcPublicKey::from_bytes(key_bytes)
|
||||||
|
.map(PublicKeyInfo::EcP256)
|
||||||
|
.map_err(|_| Error::InvalidObject),
|
||||||
|
AlgorithmId::EccP384 => EcPublicKey::from_bytes(key_bytes)
|
||||||
|
.map(PublicKeyInfo::EcP384)
|
||||||
|
.map_err(|_| 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl x509::SubjectPublicKeyInfo for PublicKeyInfo {
|
||||||
|
type AlgorithmId = AlgorithmId;
|
||||||
|
type SubjectPublicKey = Vec<u8>;
|
||||||
|
|
||||||
|
fn algorithm_id(&self) -> AlgorithmId {
|
||||||
|
self.algorithm()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
PublicKeyInfo::Rsa { pubkey, .. } => {
|
||||||
|
cookie_factory::gen_simple(write_pki::rsa_pubkey(pubkey), vec![])
|
||||||
|
.expect("can write to Vec")
|
||||||
|
}
|
||||||
|
PublicKeyInfo::EcP256(pubkey) => pubkey.as_bytes().to_vec(),
|
||||||
|
PublicKeyInfo::EcP384(pubkey) => pubkey.as_bytes().to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Digest algorithms.
|
||||||
|
///
|
||||||
|
/// See RFC 4055 and RFC 8017.
|
||||||
|
enum DigestId {
|
||||||
|
/// Secure Hash Algorithm 256 (SHA256)
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl x509::AlgorithmIdentifier for DigestId {
|
||||||
|
type AlgorithmOid = &'static [u64];
|
||||||
|
|
||||||
|
fn algorithm(&self) -> Self::AlgorithmOid {
|
||||||
|
match self {
|
||||||
|
// See https://tools.ietf.org/html/rfc4055#section-2.1
|
||||||
|
DigestId::Sha256 => &[2, 16, 840, 1, 101, 3, 4, 2, 1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters<W: std::io::Write>(
|
||||||
|
&self,
|
||||||
|
w: cookie_factory::WriteContext<W>,
|
||||||
|
) -> cookie_factory::GenResult<W> {
|
||||||
|
// Parameters are an explicit NULL
|
||||||
|
// See https://tools.ietf.org/html/rfc8017#appendix-A.2.4
|
||||||
|
x509::der::write::der_null()(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SignatureId {
|
||||||
|
/// Public-Key Cryptography Standards (PKCS) #1 version 1.5 signature algorithm with
|
||||||
|
/// Secure Hash Algorithm 256 (SHA256) and Rivest, Shamir and Adleman (RSA) encryption
|
||||||
|
///
|
||||||
|
/// See RFC 4055 and RFC 8017.
|
||||||
|
Sha256WithRsaEncryption,
|
||||||
|
|
||||||
|
/// Elliptic Curve Digital Signature Algorithm (DSA) coupled with the Secure Hash
|
||||||
|
/// Algorithm 256 (SHA256) algorithm
|
||||||
|
///
|
||||||
|
/// See RFC 5758.
|
||||||
|
EcdsaWithSha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl x509::AlgorithmIdentifier for SignatureId {
|
||||||
|
type AlgorithmOid = &'static [u64];
|
||||||
|
|
||||||
|
fn algorithm(&self) -> Self::AlgorithmOid {
|
||||||
|
match self {
|
||||||
|
SignatureId::Sha256WithRsaEncryption => &[1, 2, 840, 113_549, 1, 1, 11],
|
||||||
|
SignatureId::EcdsaWithSha256 => &[1, 2, 840, 10045, 4, 3, 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters<W: std::io::Write>(
|
||||||
|
&self,
|
||||||
|
w: cookie_factory::WriteContext<W>,
|
||||||
|
) -> cookie_factory::GenResult<W> {
|
||||||
|
// No parameters for any SignatureId
|
||||||
|
Ok(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Certificates
|
/// Certificates
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Certificate(Buffer);
|
pub struct Certificate {
|
||||||
|
serial: Serial,
|
||||||
|
issuer: String,
|
||||||
|
subject: String,
|
||||||
|
subject_pki: PublicKeyInfo,
|
||||||
|
data: Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for Certificate {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(bytes: &'a [u8]) -> Result<Self, Error> {
|
||||||
|
Self::from_bytes(bytes.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Certificate {
|
impl Certificate {
|
||||||
|
/// Creates a new self-signed certificate for the given key. Writes the resulting
|
||||||
|
/// certificate to the slot before returning it.
|
||||||
|
pub fn generate_self_signed(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
key: SlotId,
|
||||||
|
serial: impl Into<Serial>,
|
||||||
|
not_after: Option<DateTime<Utc>>,
|
||||||
|
subject: String,
|
||||||
|
subject_pki: PublicKeyInfo,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let serial = serial.into();
|
||||||
|
|
||||||
|
// Issuer and subject are the same in self-signed certificates
|
||||||
|
let issuer = subject.clone();
|
||||||
|
|
||||||
|
let mut tbs_cert = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
|
||||||
|
|
||||||
|
let signature_algorithm = match subject_pki.algorithm() {
|
||||||
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => SignatureId::Sha256WithRsaEncryption,
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => SignatureId::EcdsaWithSha256,
|
||||||
|
};
|
||||||
|
|
||||||
|
cookie_factory::gen(
|
||||||
|
x509::write::tbs_certificate(
|
||||||
|
&serial.to_bytes(),
|
||||||
|
&signature_algorithm,
|
||||||
|
&issuer,
|
||||||
|
Utc::now(),
|
||||||
|
not_after,
|
||||||
|
&subject,
|
||||||
|
&subject_pki,
|
||||||
|
),
|
||||||
|
tbs_cert.deref_mut(),
|
||||||
|
)
|
||||||
|
.expect("can serialize to Vec");
|
||||||
|
|
||||||
|
let signature = match signature_algorithm {
|
||||||
|
SignatureId::Sha256WithRsaEncryption => {
|
||||||
|
use cookie_factory::{combinator::slice, sequence::tuple};
|
||||||
|
use x509::{
|
||||||
|
der::write::{der_octet_string, der_sequence},
|
||||||
|
write::algorithm_identifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
let em_len = if let AlgorithmId::Rsa1024 = subject_pki.algorithm() {
|
||||||
|
128
|
||||||
|
} else {
|
||||||
|
256
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = Sha256::digest(&tbs_cert);
|
||||||
|
|
||||||
|
let t = cookie_factory::gen_simple(
|
||||||
|
der_sequence((
|
||||||
|
algorithm_identifier(&DigestId::Sha256),
|
||||||
|
der_octet_string(&h),
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.expect("can serialize into Vec");
|
||||||
|
|
||||||
|
let em = cookie_factory::gen_simple(
|
||||||
|
tuple((
|
||||||
|
slice(&[0x00, 0x01]),
|
||||||
|
slice(&vec![0xff; em_len - t.len() - 3]),
|
||||||
|
slice(&[0x00]),
|
||||||
|
slice(t),
|
||||||
|
)),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.expect("can serialize to Vec");
|
||||||
|
|
||||||
|
sign_data(yubikey, &em, subject_pki.algorithm(), key)
|
||||||
|
}
|
||||||
|
SignatureId::EcdsaWithSha256 => sign_data(
|
||||||
|
yubikey,
|
||||||
|
&Sha256::digest(&tbs_cert),
|
||||||
|
subject_pki.algorithm(),
|
||||||
|
key,
|
||||||
|
),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let mut data = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
|
||||||
|
|
||||||
|
cookie_factory::gen(
|
||||||
|
x509::write::certificate(&tbs_cert, &signature_algorithm, &signature),
|
||||||
|
data.deref_mut(),
|
||||||
|
)
|
||||||
|
.expect("can serialize to Vec");
|
||||||
|
|
||||||
|
let cert = Certificate {
|
||||||
|
serial,
|
||||||
|
issuer,
|
||||||
|
subject,
|
||||||
|
subject_pki,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
||||||
|
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read a certificate from the given slot in the YubiKey
|
/// Read a certificate from the given slot in the YubiKey
|
||||||
pub fn read(yubikey: &mut YubiKey, slot: SlotId) -> Result<Self, Error> {
|
pub fn read(yubikey: &mut YubiKey, slot: SlotId) -> Result<Self, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
@@ -57,25 +441,29 @@ impl Certificate {
|
|||||||
return Err(Error::InvalidObject);
|
return Err(Error::InvalidObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Certificate(buf))
|
Certificate::from_bytes(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write this certificate into the YubiKey in the given slot
|
/// Write this certificate into the YubiKey in the given slot
|
||||||
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: u8) -> Result<(), Error> {
|
pub fn write(
|
||||||
let max_size = yubikey.obj_size_max();
|
&self,
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
certinfo: CertInfo,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
write_certificate(&txn, slot, Some(&self.0), certinfo, max_size)
|
write_certificate(&txn, slot, Some(&self.data), certinfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a certificate located at the given slot of the given YubiKey
|
/// Delete a certificate located at the given slot of the given YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<(), Error> {
|
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<(), Error> {
|
||||||
let max_size = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
write_certificate(&txn, slot, None, 0, max_size)
|
write_certificate(&txn, slot, None, CertInfo::Uncompressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize a local certificate struct from the given bytebuffer
|
/// Initialize a local certificate struct from the given bytebuffer
|
||||||
pub fn new(cert: impl Into<Buffer>) -> Result<Self, Error> {
|
pub fn from_bytes(cert: impl Into<Buffer>) -> Result<Self, Error> {
|
||||||
let cert = cert.into();
|
let cert = cert.into();
|
||||||
|
|
||||||
if cert.is_empty() {
|
if cert.is_empty() {
|
||||||
@@ -83,27 +471,63 @@ impl Certificate {
|
|||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Certificate(cert))
|
let parsed_cert = match parse_x509_der(&cert) {
|
||||||
|
Ok((_, cert)) => cert,
|
||||||
|
_ => return Err(Error::InvalidObject),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serial = Serial::try_from(parsed_cert.tbs_certificate.serial.to_bytes_be().as_slice())
|
||||||
|
.map_err(|_| Error::InvalidObject)?;
|
||||||
|
let issuer = parsed_cert.tbs_certificate.issuer.to_string();
|
||||||
|
let subject = parsed_cert.tbs_certificate.subject.to_string();
|
||||||
|
let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?;
|
||||||
|
|
||||||
|
Ok(Certificate {
|
||||||
|
serial,
|
||||||
|
issuer,
|
||||||
|
subject,
|
||||||
|
subject_pki,
|
||||||
|
data: cert,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the serial number of the certificate.
|
||||||
|
pub fn serial(&self) -> &Serial {
|
||||||
|
&self.serial
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Issuer field of the certificate.
|
||||||
|
pub fn issuer(&self) -> &str {
|
||||||
|
&self.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the SubjectName field of the certificate.
|
||||||
|
pub fn subject(&self) -> &str {
|
||||||
|
&self.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the SubjectPublicKeyInfo field of the certificate.
|
||||||
|
pub fn subject_pki(&self) -> &PublicKeyInfo {
|
||||||
|
&self.subject_pki
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the inner buffer
|
/// Extract the inner buffer
|
||||||
pub fn into_buffer(self) -> Buffer {
|
pub fn into_buffer(self) -> Buffer {
|
||||||
self.0
|
self.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<[u8]> for Certificate {
|
impl AsRef<[u8]> for Certificate {
|
||||||
fn as_ref(&self) -> &[u8] {
|
fn as_ref(&self) -> &[u8] {
|
||||||
self.0.as_ref()
|
self.data.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read certificate
|
/// Read certificate
|
||||||
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> {
|
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer, Error> {
|
||||||
let mut len: usize = 0;
|
let object_id = slot.object_id();
|
||||||
let object_id = key::slot_object(slot)?;
|
|
||||||
|
|
||||||
let mut buf = match txn.fetch_object(object_id) {
|
let buf = match txn.fetch_object(object_id) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// TODO(tarcieri): is this really ok?
|
// TODO(tarcieri): is this really ok?
|
||||||
@@ -111,27 +535,15 @@ pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Bu
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if buf.len() < CB_OBJ_TAG_MIN {
|
// TODO(str4d): Check the rest of the buffer (TAG_CERT_COMPRESS and TAG_CERT_LRC)
|
||||||
// TODO(tarcieri): is this really ok?
|
|
||||||
return Ok(Zeroizing::new(vec![]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf[0] == TAG_CERT {
|
if buf[0] == TAG_CERT {
|
||||||
let offset = 1 + get_length(&buf[1..], &mut len);
|
Tlv::parse_single(buf, TAG_CERT).or_else(|_| {
|
||||||
|
|
||||||
if len > buf.len() - offset {
|
|
||||||
// TODO(tarcieri): is this really ok?
|
// TODO(tarcieri): is this really ok?
|
||||||
return Ok(Zeroizing::new(vec![]));
|
Ok(Zeroizing::new(vec![]))
|
||||||
}
|
})
|
||||||
|
} else {
|
||||||
unsafe {
|
|
||||||
ptr::copy(buf.as_ptr().add(offset), buf.as_mut_ptr(), len);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.truncate(len);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write certificate
|
/// Write certificate
|
||||||
@@ -139,13 +551,9 @@ pub(crate) fn write_certificate(
|
|||||||
txn: &Transaction<'_>,
|
txn: &Transaction<'_>,
|
||||||
slot: SlotId,
|
slot: SlotId,
|
||||||
data: Option<&[u8]>,
|
data: Option<&[u8]>,
|
||||||
certinfo: u8,
|
certinfo: CertInfo,
|
||||||
max_size: usize,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut buf = [0u8; CB_OBJ_MAX];
|
let object_id = slot.object_id();
|
||||||
let mut offset = 0;
|
|
||||||
|
|
||||||
let object_id = key::slot_object(slot)?;
|
|
||||||
|
|
||||||
if data.is_none() {
|
if data.is_none() {
|
||||||
return txn.save_object(object_id, &[]);
|
return txn.save_object(object_id, &[]);
|
||||||
@@ -153,34 +561,115 @@ pub(crate) fn write_certificate(
|
|||||||
|
|
||||||
let data = data.unwrap();
|
let data = data.unwrap();
|
||||||
|
|
||||||
let mut req_len = 1 /* cert tag */ + 3 /* compression tag + data*/ + 2 /* lrc */;
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
req_len += set_length(&mut buf, data.len());
|
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
|
||||||
req_len += data.len();
|
|
||||||
|
|
||||||
if req_len < data.len() || req_len > max_size {
|
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf[offset] = TAG_CERT;
|
|
||||||
offset += 1;
|
|
||||||
offset += set_length(&mut buf[offset..], data.len());
|
|
||||||
|
|
||||||
buf[offset..(offset + data.len())].copy_from_slice(&data);
|
|
||||||
|
|
||||||
offset += data.len();
|
|
||||||
|
|
||||||
// write compression info and LRC trailer
|
// write compression info and LRC trailer
|
||||||
buf[offset] = TAG_CERT_COMPRESS;
|
offset += Tlv::write(&mut buf[offset..], TAG_CERT_COMPRESS, &[certinfo.into()])?;
|
||||||
buf[offset + 1] = 0x01;
|
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
|
||||||
buf[offset + 2] = if certinfo == YKPIV_CERTINFO_GZIP {
|
|
||||||
0x01
|
|
||||||
} else {
|
|
||||||
0x00
|
|
||||||
};
|
|
||||||
buf[offset + 3] = TAG_CERT_LRC;
|
|
||||||
buf[offset + 4] = 00;
|
|
||||||
|
|
||||||
offset += 5;
|
|
||||||
|
|
||||||
txn.save_object(object_id, &buf[..offset])
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod write_pki {
|
||||||
|
use cookie_factory::{SerializeFn, WriteContext};
|
||||||
|
use rsa::{BigUint, PublicKeyParts, RSAPublicKey};
|
||||||
|
use std::io::Write;
|
||||||
|
use x509::der::write::{der_integer, der_sequence};
|
||||||
|
|
||||||
|
/// Encodes a usize as an ASN.1 integer using DER.
|
||||||
|
fn der_integer_biguint<'a, W: Write + 'a>(num: &'a BigUint) -> impl SerializeFn<W> + 'a {
|
||||||
|
move |w: WriteContext<W>| der_integer(&num.to_bytes_be())(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<'a, W: Write + 'a>(
|
||||||
|
pubkey: &'a RSAPublicKey,
|
||||||
|
) -> impl SerializeFn<W> + 'a {
|
||||||
|
der_sequence((
|
||||||
|
der_integer_biguint(pubkey.n()),
|
||||||
|
der_integer_biguint(pubkey.e()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+90
-15
@@ -30,8 +30,34 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, yubikey::YubiKey};
|
use crate::{error::Error, yubikey::YubiKey};
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
|
use subtle_encoding::hex;
|
||||||
|
|
||||||
|
/// CHUID size
|
||||||
|
pub const CHUID_SIZE: usize = 59;
|
||||||
|
|
||||||
|
/// CARDID size
|
||||||
|
pub const CARDID_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// FASC-N component size
|
||||||
|
pub const FASCN_SIZE: usize = 25;
|
||||||
|
|
||||||
|
/// Expiration size
|
||||||
|
pub const EXPIRATION_SIZE: usize = 8;
|
||||||
|
|
||||||
|
/// FASC-N offset
|
||||||
|
const CHUID_FASCN_OFFS: usize = 2;
|
||||||
|
|
||||||
|
/// GUID offset
|
||||||
|
const CHUID_GUID_OFFS: usize = 29;
|
||||||
|
|
||||||
|
/// Expiration offset
|
||||||
|
const CHUID_EXPIRATION_OFFS: usize = 47;
|
||||||
|
|
||||||
|
/// CHUID Object ID
|
||||||
|
const OBJ_CHUID: u32 = 0x005f_c102;
|
||||||
|
|
||||||
/// Cardholder Unique Identifier (CHUID) Template
|
/// Cardholder Unique Identifier (CHUID) Template
|
||||||
///
|
///
|
||||||
@@ -55,38 +81,87 @@ const CHUID_TMPL: &[u8] = &[
|
|||||||
0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
|
0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Cardholder Unique Identifier (CHUID)
|
/// Cardholder Unique Identifier (CHUID) Card UUID/GUID value
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct CHUID([u8; YKPIV_CARDID_SIZE]);
|
pub struct Uuid(pub [u8; CARDID_SIZE]);
|
||||||
|
|
||||||
|
impl Uuid {
|
||||||
|
/// Generate a random Cardholder Unique Identifier (CHUID) UUID
|
||||||
|
pub fn generate() -> Result<Self, Error> {
|
||||||
|
let mut id = [0u8; CARDID_SIZE];
|
||||||
|
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
|
||||||
|
Ok(Self(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cardholder Unique Identifier (CHUID)
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct CHUID(pub [u8; CHUID_SIZE]);
|
||||||
|
|
||||||
impl CHUID {
|
impl CHUID {
|
||||||
/// Generate a random Cardholder Unique Identifier (CHUID)
|
/// Return FASC-N component of CHUID
|
||||||
pub fn generate() -> Result<Self, Error> {
|
pub fn fascn(&self) -> Result<[u8; FASCN_SIZE], Error> {
|
||||||
let mut id = [0u8; YKPIV_CARDID_SIZE];
|
let mut fascn = [0u8; FASCN_SIZE];
|
||||||
getrandom(&mut id).map_err(|_| Error::RandomnessError)?;
|
fascn.copy_from_slice(&self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + FASCN_SIZE)]);
|
||||||
Ok(CHUID(id))
|
Ok(fascn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return Card UUID/GUID component of CHUID
|
||||||
|
pub fn uuid(&self) -> Result<[u8; CARDID_SIZE], Error> {
|
||||||
|
let mut uuid = [0u8; CARDID_SIZE];
|
||||||
|
uuid.copy_from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + CARDID_SIZE)]);
|
||||||
|
Ok(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return expiration date component of CHUID
|
||||||
|
// TODO(tarcieri): parse expiration?
|
||||||
|
pub fn expiration(&self) -> Result<[u8; EXPIRATION_SIZE], Error> {
|
||||||
|
let mut expiration = [0u8; EXPIRATION_SIZE];
|
||||||
|
expiration.copy_from_slice(
|
||||||
|
&self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + EXPIRATION_SIZE)],
|
||||||
|
);
|
||||||
|
Ok(expiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Cardholder Unique Identifier (CHUID)
|
/// Get Cardholder Unique Identifier (CHUID)
|
||||||
pub fn get(yubikey: &mut YubiKey) -> Result<Self, Error> {
|
pub fn get(yubikey: &mut YubiKey) -> Result<CHUID, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let response = txn.fetch_object(YKPIV_OBJ_CHUID)?;
|
let response = txn.fetch_object(OBJ_CHUID)?;
|
||||||
|
|
||||||
if response.len() != CHUID_TMPL.len() {
|
if response.len() != CHUID_TMPL.len() {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cardid = [0u8; YKPIV_CARDID_SIZE];
|
let mut chuid = [0u8; CHUID_SIZE];
|
||||||
cardid.copy_from_slice(&response[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + YKPIV_CARDID_SIZE)]);
|
chuid.copy_from_slice(&response[0..CHUID_SIZE]);
|
||||||
Ok(CHUID(cardid))
|
let retval = CHUID { 0: chuid };
|
||||||
|
Ok(retval)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Cardholder Unique Identifier (CHUID)
|
/// Set Cardholder Unique Identifier (CHUID)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
|
||||||
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||||
let mut buf = CHUID_TMPL.to_vec();
|
let mut buf = CHUID_TMPL.to_vec();
|
||||||
buf[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + self.0.len())].copy_from_slice(&self.0);
|
buf[0..self.0.len()].copy_from_slice(&self.0);
|
||||||
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
txn.save_object(YKPIV_OBJ_CHUID, &buf)
|
txn.save_object(OBJ_CHUID, &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CHUID {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
String::from_utf8(hex::encode(&self.0[..])).unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for CHUID {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "CHUID({:?})", &self.0[..])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-7
@@ -30,12 +30,25 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, metadata, mgm::MgmType, yubikey::YubiKey};
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
metadata,
|
||||||
|
mgm::{MgmType, ADMIN_FLAGS_1_PROTECTED_MGM},
|
||||||
|
yubikey::{YubiKey, ADMIN_FLAGS_1_PUK_BLOCKED},
|
||||||
|
TAG_ADMIN, TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_ADMIN_TIMESTAMP, TAG_PROTECTED,
|
||||||
|
TAG_PROTECTED_FLAGS_1, TAG_PROTECTED_MGM,
|
||||||
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::convert::TryInto;
|
use std::{
|
||||||
|
convert::TryInto,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CB_ADMIN_TIMESTAMP: usize = 0x04;
|
||||||
|
const PROTECTED_FLAGS_1_PUK_NOBLOCK: u8 = 0x01;
|
||||||
|
|
||||||
/// Config
|
/// Config
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Protected data available
|
/// Protected data available
|
||||||
protected_data_available: bool,
|
protected_data_available: bool,
|
||||||
@@ -47,7 +60,7 @@ pub struct Config {
|
|||||||
puk_noblock_on_upgrade: bool,
|
puk_noblock_on_upgrade: bool,
|
||||||
|
|
||||||
/// PIN last changed
|
/// PIN last changed
|
||||||
pin_last_changed: u32,
|
pin_last_changed: Option<SystemTime>,
|
||||||
|
|
||||||
/// MGM type
|
/// MGM type
|
||||||
mgm_type: MgmType,
|
mgm_type: MgmType,
|
||||||
@@ -60,7 +73,7 @@ impl Config {
|
|||||||
protected_data_available: false,
|
protected_data_available: false,
|
||||||
puk_blocked: false,
|
puk_blocked: false,
|
||||||
puk_noblock_on_upgrade: false,
|
puk_noblock_on_upgrade: false,
|
||||||
pin_last_changed: 0,
|
pin_last_changed: None,
|
||||||
mgm_type: MgmType::Manual,
|
mgm_type: MgmType::Manual,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,8 +106,13 @@ impl Config {
|
|||||||
if item.len() != CB_ADMIN_TIMESTAMP {
|
if item.len() != CB_ADMIN_TIMESTAMP {
|
||||||
error!("pin timestamp in admin metadata is an invalid size");
|
error!("pin timestamp in admin metadata is an invalid size");
|
||||||
} else {
|
} else {
|
||||||
// TODO(tarcieri): double check this is little endian
|
// TODO(tarcieri): double-check endianness is correct
|
||||||
config.pin_last_changed = u32::from_le_bytes(item.try_into().unwrap());
|
let pin_last_changed = u32::from_le_bytes(item.try_into().unwrap());
|
||||||
|
|
||||||
|
if pin_last_changed != 0 {
|
||||||
|
config.pin_last_changed =
|
||||||
|
Some(UNIX_EPOCH + Duration::from_secs(pin_last_changed as u64));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-242
@@ -1,242 +0,0 @@
|
|||||||
//! Constant values
|
|
||||||
// TODO(tarcieri): refactor these into enums!
|
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
|
||||||
// <https://github.com/Yubico/yubico-piv-tool/>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-2016 Yubico AB
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Redistribution and use in source and binary forms, with or without
|
|
||||||
// modification, are permitted provided that the following conditions are
|
|
||||||
// met:
|
|
||||||
//
|
|
||||||
// * Redistributions of source code must retain the above copyright
|
|
||||||
// notice, this list of conditions and the following disclaimer.
|
|
||||||
//
|
|
||||||
// * Redistributions in binary form must reproduce the above
|
|
||||||
// copyright notice, this list of conditions and the following
|
|
||||||
// disclaimer in the documentation and/or other materials provided
|
|
||||||
// with the distribution.
|
|
||||||
//
|
|
||||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
// TODO(tarcieri): document these!
|
|
||||||
#![allow(missing_docs, non_upper_case_globals)]
|
|
||||||
|
|
||||||
pub const szLOG_SOURCE: &str = "yubikey-piv.rs";
|
|
||||||
|
|
||||||
pub const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
|
|
||||||
pub const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
|
|
||||||
|
|
||||||
pub const CB_ADMIN_TIMESTAMP: usize = 0x04;
|
|
||||||
pub const CB_ADMIN_SALT: usize = 16;
|
|
||||||
|
|
||||||
pub const CB_ATR_MAX: usize = 33;
|
|
||||||
|
|
||||||
pub const CB_BUF_MAX_NEO: usize = 2048;
|
|
||||||
pub const CB_BUF_MAX_YK4: usize = 3072;
|
|
||||||
pub const CB_BUF_MAX: usize = CB_BUF_MAX_YK4;
|
|
||||||
|
|
||||||
pub const CB_ECC_POINTP256: usize = 65;
|
|
||||||
pub const CB_ECC_POINTP384: usize = 97;
|
|
||||||
|
|
||||||
pub const CB_OBJ_MAX_YK4: usize = CB_BUF_MAX_YK4 - 9;
|
|
||||||
pub const CB_OBJ_MAX: usize = CB_OBJ_MAX_YK4;
|
|
||||||
pub const CB_OBJ_MAX_NEO: usize = CB_BUF_MAX_NEO - 9;
|
|
||||||
|
|
||||||
pub const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
|
||||||
pub const CB_OBJ_TAG_MAX: usize = (CB_OBJ_TAG_MIN + 2); // 1 byte tag + 3 bytes len
|
|
||||||
|
|
||||||
pub const CB_PAGE: usize = 4096;
|
|
||||||
pub const CB_PIN_MAX: usize = 8;
|
|
||||||
|
|
||||||
pub const CCC_ID_OFFS: usize = 9;
|
|
||||||
|
|
||||||
pub const CHUID_GUID_OFFS: usize = 29;
|
|
||||||
|
|
||||||
pub const CHREF_ACT_CHANGE_PIN: i32 = 0;
|
|
||||||
pub const CHREF_ACT_UNBLOCK_PIN: i32 = 1;
|
|
||||||
pub const CHREF_ACT_CHANGE_PUK: i32 = 2;
|
|
||||||
|
|
||||||
pub const CONTAINER_NAME_LEN: usize = 40;
|
|
||||||
pub const CONTAINER_REC_LEN: usize = (2 * CONTAINER_NAME_LEN) + 27; // 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20
|
|
||||||
|
|
||||||
pub const DES_TYPE_3DES: u8 = 1;
|
|
||||||
|
|
||||||
pub const DES_LEN_DES: usize = 8;
|
|
||||||
pub const DES_LEN_3DES: usize = DES_LEN_DES * 3;
|
|
||||||
|
|
||||||
// device types
|
|
||||||
|
|
||||||
pub const DEVTYPE_UNKNOWN: u32 = 0x0000_0000;
|
|
||||||
pub const DEVTYPE_NEO: u32 = 0x4E45_0000; //"NE"
|
|
||||||
pub const DEVTYPE_YK: u32 = 0x594B_0000; //"YK"
|
|
||||||
pub const DEVTYPE_NEOr3: u32 = (DEVTYPE_NEO | 0x0000_7233); //"r3"
|
|
||||||
pub const DEVTYPE_YK4: u32 = (DEVTYPE_YK | 0x0000_0034); // "4"
|
|
||||||
pub const DEVYTPE_YK5: u32 = (DEVTYPE_YK | 0x0000_0035); // "5"
|
|
||||||
|
|
||||||
pub const ITER_MGM_PBKDF2: usize = 10000;
|
|
||||||
|
|
||||||
pub const PROTECTED_FLAGS_1_PUK_NOBLOCK: u8 = 0x01;
|
|
||||||
|
|
||||||
// sw is status words, see NIST special publication 800-73-4, section 5.6
|
|
||||||
|
|
||||||
pub const SW_SUCCESS: i32 = 0x9000;
|
|
||||||
pub const SW_ERR_SECURITY_STATUS: i32 = 0x6982;
|
|
||||||
pub const SW_ERR_AUTH_BLOCKED: i32 = 0x6983;
|
|
||||||
pub const SW_ERR_INCORRECT_PARAM: i32 = 0x6a80;
|
|
||||||
|
|
||||||
// this is a custom sw for yubikey
|
|
||||||
|
|
||||||
pub const SW_ERR_INCORRECT_SLOT: i32 = 0x6b00;
|
|
||||||
pub const SW_ERR_NOT_SUPPORTED: i32 = 0x6d00;
|
|
||||||
|
|
||||||
pub const TAG_CERT: u8 = 0x70;
|
|
||||||
pub const TAG_CERT_COMPRESS: u8 = 0x71;
|
|
||||||
pub const TAG_CERT_LRC: u8 = 0xFE;
|
|
||||||
pub const TAG_ADMIN: u8 = 0x80;
|
|
||||||
pub const TAG_ADMIN_FLAGS_1: u8 = 0x81;
|
|
||||||
pub const TAG_ADMIN_SALT: u8 = 0x82;
|
|
||||||
pub const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
|
|
||||||
pub const TAG_PROTECTED: u8 = 0x88;
|
|
||||||
pub const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
|
||||||
pub const TAG_PROTECTED_MGM: u8 = 0x89;
|
|
||||||
pub const TAG_MSCMAP: u8 = 0x81;
|
|
||||||
pub const TAG_MSROOTS_END: u8 = 0x82;
|
|
||||||
pub const TAG_MSROOTS_MID: u8 = 0x83;
|
|
||||||
|
|
||||||
pub const TAG_RSA_MODULUS: u8 = 0x81;
|
|
||||||
pub const TAG_RSA_EXP: u8 = 0x82;
|
|
||||||
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_CARDID_SIZE: usize = 16;
|
|
||||||
|
|
||||||
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;
|
|
||||||
pub const YKPIV_OBJ_MSROOTS3: u32 = 0x005f_ff13;
|
|
||||||
pub const YKPIV_OBJ_MSROOTS4: u32 = 0x005f_ff14;
|
|
||||||
pub const YKPIV_OBJ_MSROOTS5: u32 = 0x005f_ff15;
|
|
||||||
|
|
||||||
pub const YKPIV_PINPOLICY_TAG: u8 = 0xaa;
|
|
||||||
pub const YKPIV_PINPOLICY_DEFAULT: u8 = 0;
|
|
||||||
pub const YKPIV_PINPOLICY_NEVER: u8 = 1;
|
|
||||||
pub const YKPIV_PINPOLICY_ONCE: u8 = 2;
|
|
||||||
pub const YKPIV_PINPOLICY_ALWAYS: u8 = 3;
|
|
||||||
|
|
||||||
pub const YKPIV_TOUCHPOLICY_TAG: u8 = 0xab;
|
|
||||||
pub const YKPIV_TOUCHPOLICY_DEFAULT: u8 = 0;
|
|
||||||
pub const YKPIV_TOUCHPOLICY_NEVER: u8 = 1;
|
|
||||||
pub const YKPIV_TOUCHPOLICY_ALWAYS: u8 = 2;
|
|
||||||
pub const YKPIV_TOUCHPOLICY_CACHED: u8 = 3;
|
|
||||||
+6
-1
@@ -68,7 +68,7 @@ pub enum Error {
|
|||||||
/// Wrong PIN
|
/// Wrong PIN
|
||||||
WrongPin {
|
WrongPin {
|
||||||
/// Number of tries remaining
|
/// Number of tries remaining
|
||||||
tries: u32,
|
tries: u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Invalid object
|
/// Invalid object
|
||||||
@@ -88,6 +88,9 @@ pub enum Error {
|
|||||||
|
|
||||||
/// Not supported
|
/// Not supported
|
||||||
NotSupported,
|
NotSupported,
|
||||||
|
|
||||||
|
/// Not found
|
||||||
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -113,6 +116,7 @@ impl Error {
|
|||||||
Error::ArgumentError => "YKPIV_ARGUMENT_ERROR",
|
Error::ArgumentError => "YKPIV_ARGUMENT_ERROR",
|
||||||
Error::RangeError => "YKPIV_RANGE_ERROR",
|
Error::RangeError => "YKPIV_RANGE_ERROR",
|
||||||
Error::NotSupported => "YKPIV_NOT_SUPPORTED",
|
Error::NotSupported => "YKPIV_NOT_SUPPORTED",
|
||||||
|
Error::NotFound => "<not found>",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +139,7 @@ impl Error {
|
|||||||
Error::ArgumentError => "argument error",
|
Error::ArgumentError => "argument error",
|
||||||
Error::RangeError => "range error",
|
Error::RangeError => "range error",
|
||||||
Error::NotSupported => "not supported",
|
Error::NotSupported => "not supported",
|
||||||
|
Error::NotFound => "not found",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+677
-170
@@ -38,68 +38,380 @@
|
|||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
apdu::{Ins, StatusWords},
|
||||||
certificate::{self, Certificate},
|
certificate::{self, Certificate},
|
||||||
consts::*,
|
|
||||||
error::Error,
|
error::Error,
|
||||||
response::StatusWords,
|
|
||||||
serialization::*,
|
serialization::*,
|
||||||
settings,
|
settings,
|
||||||
yubikey::YubiKey,
|
yubikey::YubiKey,
|
||||||
AlgorithmId, ObjectId,
|
ObjectId,
|
||||||
};
|
};
|
||||||
use log::{debug, error, warn};
|
use log::debug;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::CB_OBJ_MAX;
|
||||||
|
use crate::{
|
||||||
|
certificate::PublicKeyInfo,
|
||||||
|
policy::{PinPolicy, TouchPolicy},
|
||||||
|
Buffer,
|
||||||
|
};
|
||||||
|
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
|
||||||
|
use log::{error, warn};
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use num_bigint::traits::ModInverse;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use num_integer::Integer;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use num_traits::{FromPrimitive, One};
|
||||||
|
use rsa::{BigUint, RSAPublicKey};
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
const CB_ECC_POINTP256: usize = 65;
|
||||||
|
const CB_ECC_POINTP384: usize = 97;
|
||||||
|
|
||||||
|
const TAG_RSA_MODULUS: u8 = 0x81;
|
||||||
|
const TAG_RSA_EXP: u8 = 0x82;
|
||||||
|
const TAG_ECC_POINT: u8 = 0x86;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
const KEYDATA_LEN: usize = 1024;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
const KEYDATA_RSA_EXP: u64 = 65537;
|
||||||
|
|
||||||
/// Slot identifiers.
|
/// Slot identifiers.
|
||||||
/// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html>
|
/// <https://developers.yubico.com/PIV/Introduction/Certificate_slots.html>
|
||||||
// TODO(tarcieri): replace these with enums
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub type SlotId = u8;
|
pub enum SlotId {
|
||||||
|
/// This certificate and its associated private key is used to authenticate the card
|
||||||
|
/// and the cardholder. This slot is used for things like system login. The end user
|
||||||
|
/// PIN is required to perform any private key operations. Once the PIN has been
|
||||||
|
/// provided successfully, multiple private key operations may be performed without
|
||||||
|
/// additional cardholder consent.
|
||||||
|
Authentication,
|
||||||
|
|
||||||
/// Get the [`ObjectId`] that corresponds to a given [`SlotId`]
|
/// This certificate and its associated private key is used for digital signatures for
|
||||||
// TODO(tarcieri): factor this into a slot ID enum
|
/// the purpose of document signing, or signing files and executables. The end user
|
||||||
pub(crate) fn slot_object(slot: SlotId) -> Result<ObjectId, Error> {
|
/// PIN is required to perform any private key operations. The PIN must be submitted
|
||||||
let id = match slot {
|
/// every time immediately before a sign operation, to ensure cardholder participation
|
||||||
YKPIV_KEY_AUTHENTICATION => YKPIV_OBJ_AUTHENTICATION,
|
/// for every digital signature generated.
|
||||||
YKPIV_KEY_SIGNATURE => YKPIV_OBJ_SIGNATURE,
|
Signature,
|
||||||
YKPIV_KEY_KEYMGM => YKPIV_OBJ_KEY_MANAGEMENT,
|
|
||||||
YKPIV_KEY_CARDAUTH => YKPIV_OBJ_CARD_AUTH,
|
/// This certificate and its associated private key is used for encryption for the
|
||||||
YKPIV_KEY_ATTESTATION => YKPIV_OBJ_ATTESTATION,
|
/// purpose of confidentiality. This slot is used for things like encrypting e-mails
|
||||||
slot if slot >= YKPIV_KEY_RETIRED1 && (slot <= YKPIV_KEY_RETIRED20) => {
|
/// or files. The end user PIN is required to perform any private key operations. Once
|
||||||
YKPIV_OBJ_RETIRED1 + (slot - YKPIV_KEY_RETIRED1) as u32
|
/// the PIN has been provided successfully, multiple private key operations may be
|
||||||
|
/// performed without additional cardholder consent.
|
||||||
|
KeyManagement,
|
||||||
|
|
||||||
|
/// This certificate and its associated private key is used to support additional
|
||||||
|
/// physical access applications, such as providing physical access to buildings via
|
||||||
|
/// PIV-enabled door locks. The end user PIN is NOT required to perform private key
|
||||||
|
/// operations for this slot.
|
||||||
|
CardAuthentication,
|
||||||
|
|
||||||
|
/// These slots are only available on the YubiKey 4 & 5. They are meant for previously
|
||||||
|
/// used Key Management keys to be able to decrypt earlier encrypted documents or
|
||||||
|
/// emails. In the YubiKey 4 & 5 all 20 of them are fully available for use.
|
||||||
|
Retired(RetiredSlotId),
|
||||||
|
|
||||||
|
/// This slot is only available on YubiKey version 4.3 and newer. It is only used for
|
||||||
|
/// attestation of other keys generated on device with instruction `f9`. This slot is
|
||||||
|
/// not cleared on reset, but can be overwritten.
|
||||||
|
Attestation,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TryFrom<String> for SlotId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<SlotId, Error> {
|
||||||
|
match s.as_ref() {
|
||||||
|
"9a" => Ok(SlotId::Authentication),
|
||||||
|
"9c" => Ok(SlotId::Signature),
|
||||||
|
"9d" => Ok(SlotId::KeyManagement),
|
||||||
|
"9e" => Ok(SlotId::CardAuthentication),
|
||||||
|
"f9" => Ok(SlotId::Attestation),
|
||||||
|
_ => RetiredSlotId::try_from(s).map(SlotId::Retired),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TryFrom<String> for RetiredSlotId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
match value.as_ref() {
|
||||||
|
"82" => Ok(RetiredSlotId::R1),
|
||||||
|
"83" => Ok(RetiredSlotId::R2),
|
||||||
|
"84" => Ok(RetiredSlotId::R3),
|
||||||
|
"85" => Ok(RetiredSlotId::R4),
|
||||||
|
"86" => Ok(RetiredSlotId::R5),
|
||||||
|
"87" => Ok(RetiredSlotId::R6),
|
||||||
|
"88" => Ok(RetiredSlotId::R7),
|
||||||
|
"89" => Ok(RetiredSlotId::R8),
|
||||||
|
"8a" => Ok(RetiredSlotId::R9),
|
||||||
|
"8b" => Ok(RetiredSlotId::R10),
|
||||||
|
"8c" => Ok(RetiredSlotId::R11),
|
||||||
|
"8d" => Ok(RetiredSlotId::R12),
|
||||||
|
"8e" => Ok(RetiredSlotId::R13),
|
||||||
|
"8f" => Ok(RetiredSlotId::R14),
|
||||||
|
"90" => Ok(RetiredSlotId::R15),
|
||||||
|
"91" => Ok(RetiredSlotId::R16),
|
||||||
|
"92" => Ok(RetiredSlotId::R17),
|
||||||
|
"93" => Ok(RetiredSlotId::R18),
|
||||||
|
"94" => Ok(RetiredSlotId::R19),
|
||||||
|
"95" => Ok(RetiredSlotId::R20),
|
||||||
|
_ => Err(Error::InvalidObject),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RetiredSlotId> for u8 {
|
||||||
|
fn from(slot: RetiredSlotId) -> u8 {
|
||||||
|
match slot {
|
||||||
|
RetiredSlotId::R1 => 0x82,
|
||||||
|
RetiredSlotId::R2 => 0x83,
|
||||||
|
RetiredSlotId::R3 => 0x84,
|
||||||
|
RetiredSlotId::R4 => 0x85,
|
||||||
|
RetiredSlotId::R5 => 0x86,
|
||||||
|
RetiredSlotId::R6 => 0x87,
|
||||||
|
RetiredSlotId::R7 => 0x88,
|
||||||
|
RetiredSlotId::R8 => 0x89,
|
||||||
|
RetiredSlotId::R9 => 0x8a,
|
||||||
|
RetiredSlotId::R10 => 0x8b,
|
||||||
|
RetiredSlotId::R11 => 0x8c,
|
||||||
|
RetiredSlotId::R12 => 0x8d,
|
||||||
|
RetiredSlotId::R13 => 0x8e,
|
||||||
|
RetiredSlotId::R14 => 0x8f,
|
||||||
|
RetiredSlotId::R15 => 0x90,
|
||||||
|
RetiredSlotId::R16 => 0x91,
|
||||||
|
RetiredSlotId::R17 => 0x92,
|
||||||
|
RetiredSlotId::R18 => 0x93,
|
||||||
|
RetiredSlotId::R19 => 0x94,
|
||||||
|
RetiredSlotId::R20 => 0x95,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetiredSlotId {
|
||||||
|
/// Returns the [`ObjectId`] that corresponds to a given [`RetiredSlotId`].
|
||||||
|
pub(crate) fn object_id(self) -> ObjectId {
|
||||||
|
match self {
|
||||||
|
RetiredSlotId::R1 => 0x005f_c10d,
|
||||||
|
RetiredSlotId::R2 => 0x005f_c10e,
|
||||||
|
RetiredSlotId::R3 => 0x005f_c10f,
|
||||||
|
RetiredSlotId::R4 => 0x005f_c110,
|
||||||
|
RetiredSlotId::R5 => 0x005f_c111,
|
||||||
|
RetiredSlotId::R6 => 0x005f_c112,
|
||||||
|
RetiredSlotId::R7 => 0x005f_c113,
|
||||||
|
RetiredSlotId::R8 => 0x005f_c114,
|
||||||
|
RetiredSlotId::R9 => 0x005f_c115,
|
||||||
|
RetiredSlotId::R10 => 0x005f_c116,
|
||||||
|
RetiredSlotId::R11 => 0x005f_c117,
|
||||||
|
RetiredSlotId::R12 => 0x005f_c118,
|
||||||
|
RetiredSlotId::R13 => 0x005f_c119,
|
||||||
|
RetiredSlotId::R14 => 0x005f_c11a,
|
||||||
|
RetiredSlotId::R15 => 0x005f_c11b,
|
||||||
|
RetiredSlotId::R16 => 0x005f_c11c,
|
||||||
|
RetiredSlotId::R17 => 0x005f_c11d,
|
||||||
|
RetiredSlotId::R18 => 0x005f_c11e,
|
||||||
|
RetiredSlotId::R19 => 0x005f_c11f,
|
||||||
|
RetiredSlotId::R20 => 0x005f_c120,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Personal Identity Verification (PIV) key slots
|
/// Personal Identity Verification (PIV) key slots
|
||||||
pub const SLOTS: [u8; 24] = [
|
pub const SLOTS: [SlotId; 24] = [
|
||||||
YKPIV_KEY_AUTHENTICATION,
|
SlotId::Authentication,
|
||||||
YKPIV_KEY_SIGNATURE,
|
SlotId::Signature,
|
||||||
YKPIV_KEY_KEYMGM,
|
SlotId::KeyManagement,
|
||||||
YKPIV_KEY_RETIRED1,
|
SlotId::Retired(RetiredSlotId::R1),
|
||||||
YKPIV_KEY_RETIRED2,
|
SlotId::Retired(RetiredSlotId::R2),
|
||||||
YKPIV_KEY_RETIRED3,
|
SlotId::Retired(RetiredSlotId::R3),
|
||||||
YKPIV_KEY_RETIRED4,
|
SlotId::Retired(RetiredSlotId::R4),
|
||||||
YKPIV_KEY_RETIRED5,
|
SlotId::Retired(RetiredSlotId::R5),
|
||||||
YKPIV_KEY_RETIRED6,
|
SlotId::Retired(RetiredSlotId::R6),
|
||||||
YKPIV_KEY_RETIRED7,
|
SlotId::Retired(RetiredSlotId::R7),
|
||||||
YKPIV_KEY_RETIRED8,
|
SlotId::Retired(RetiredSlotId::R8),
|
||||||
YKPIV_KEY_RETIRED9,
|
SlotId::Retired(RetiredSlotId::R9),
|
||||||
YKPIV_KEY_RETIRED10,
|
SlotId::Retired(RetiredSlotId::R10),
|
||||||
YKPIV_KEY_RETIRED11,
|
SlotId::Retired(RetiredSlotId::R11),
|
||||||
YKPIV_KEY_RETIRED12,
|
SlotId::Retired(RetiredSlotId::R12),
|
||||||
YKPIV_KEY_RETIRED13,
|
SlotId::Retired(RetiredSlotId::R13),
|
||||||
YKPIV_KEY_RETIRED14,
|
SlotId::Retired(RetiredSlotId::R14),
|
||||||
YKPIV_KEY_RETIRED15,
|
SlotId::Retired(RetiredSlotId::R15),
|
||||||
YKPIV_KEY_RETIRED16,
|
SlotId::Retired(RetiredSlotId::R16),
|
||||||
YKPIV_KEY_RETIRED17,
|
SlotId::Retired(RetiredSlotId::R17),
|
||||||
YKPIV_KEY_RETIRED18,
|
SlotId::Retired(RetiredSlotId::R18),
|
||||||
YKPIV_KEY_RETIRED19,
|
SlotId::Retired(RetiredSlotId::R19),
|
||||||
YKPIV_KEY_RETIRED20,
|
SlotId::Retired(RetiredSlotId::R20),
|
||||||
YKPIV_KEY_CARDAUTH,
|
SlotId::CardAuthentication,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Algorithm identifiers
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum AlgorithmId {
|
||||||
|
/// 1024-bit RSA.
|
||||||
|
Rsa1024,
|
||||||
|
/// 2048-bit RSA.
|
||||||
|
Rsa2048,
|
||||||
|
/// ECDSA with the NIST P256 curve.
|
||||||
|
EccP256,
|
||||||
|
/// ECDSA with the NIST P384 curve.
|
||||||
|
EccP384,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for AlgorithmId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x06 => Ok(AlgorithmId::Rsa1024),
|
||||||
|
0x07 => Ok(AlgorithmId::Rsa2048),
|
||||||
|
0x11 => Ok(AlgorithmId::EccP256),
|
||||||
|
0x14 => Ok(AlgorithmId::EccP384),
|
||||||
|
_ => Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AlgorithmId> for u8 {
|
||||||
|
fn from(id: AlgorithmId) -> u8 {
|
||||||
|
match id {
|
||||||
|
AlgorithmId::Rsa1024 => 0x06,
|
||||||
|
AlgorithmId::Rsa2048 => 0x07,
|
||||||
|
AlgorithmId::EccP256 => 0x11,
|
||||||
|
AlgorithmId::EccP384 => 0x14,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlgorithmId {
|
||||||
|
/// Writes the `AlgorithmId` in the format the YubiKey expects during key generation.
|
||||||
|
pub(crate) fn write(self, buf: &mut [u8]) -> Result<usize, Error> {
|
||||||
|
Tlv::write(buf, 0x80, &[self.into()])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
fn get_elem_len(self) -> usize {
|
||||||
|
match self {
|
||||||
|
AlgorithmId::Rsa1024 => 64,
|
||||||
|
AlgorithmId::Rsa2048 => 128,
|
||||||
|
AlgorithmId::EccP256 => 32,
|
||||||
|
AlgorithmId::EccP384 => 48,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
fn get_param_tag(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => 0x01,
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// PIV cryptographic keys stored in a YubiKey
|
/// PIV cryptographic keys stored in a YubiKey
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
@@ -120,14 +432,16 @@ impl Key {
|
|||||||
let buf = match certificate::read_certificate(&txn, slot) {
|
let buf = match certificate::read_certificate(&txn, slot) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("error reading certificate in slot {}: {}", slot, e);
|
debug!("error reading certificate in slot {:?}: {}", slot, e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let cert = Certificate::new(buf)?;
|
if !buf.is_empty() {
|
||||||
|
let cert = Certificate::from_bytes(buf)?;
|
||||||
keys.push(Key { slot, cert });
|
keys.push(Key { slot, cert });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
@@ -143,69 +457,33 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keygen messages
|
|
||||||
// TODO(tarcieri): extract these into an I18N-handling type?
|
|
||||||
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
|
|
||||||
const SZ_ROCA_ALLOW_USER: &str =
|
|
||||||
"was permitted by an end-user configuration setting, but is not recommended.";
|
|
||||||
const SZ_ROCA_ALLOW_ADMIN: &str =
|
|
||||||
"was permitted by an administrator configuration setting, but is not recommended.";
|
|
||||||
const SZ_ROCA_BLOCK_USER: &str = "was blocked due to an end-user configuration setting.";
|
|
||||||
const SZ_ROCA_BLOCK_ADMIN: &str = "was blocked due to an administrator configuration setting.";
|
|
||||||
const SZ_ROCA_DEFAULT: &str = "was permitted by default, but is not recommended. The default behavior will change in a future Yubico release.";
|
|
||||||
|
|
||||||
/// Information about a generated key
|
|
||||||
// TODO(tarcieri): this could use some more work
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum GeneratedKey {
|
|
||||||
/// RSA keys
|
|
||||||
Rsa {
|
|
||||||
/// RSA algorithm
|
|
||||||
algorithm: AlgorithmId,
|
|
||||||
|
|
||||||
/// Modulus
|
|
||||||
modulus: Vec<u8>,
|
|
||||||
|
|
||||||
/// Exponent
|
|
||||||
exp: Vec<u8>,
|
|
||||||
},
|
|
||||||
/// ECC keys
|
|
||||||
Ecc {
|
|
||||||
/// ECC algorithm
|
|
||||||
algorithm: AlgorithmId,
|
|
||||||
|
|
||||||
/// Public curve point (i.e. public key)
|
|
||||||
point: Vec<u8>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GeneratedKey {
|
|
||||||
/// Get the algorithm
|
|
||||||
pub fn algorithm(&self) -> AlgorithmId {
|
|
||||||
*match self {
|
|
||||||
GeneratedKey::Rsa { algorithm, .. } => algorithm,
|
|
||||||
GeneratedKey::Ecc { algorithm, .. } => algorithm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate key
|
/// Generate key
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
pub fn generate(
|
pub fn generate(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
slot: SlotId,
|
slot: SlotId,
|
||||||
algorithm: AlgorithmId,
|
algorithm: AlgorithmId,
|
||||||
pin_policy: u8,
|
pin_policy: PinPolicy,
|
||||||
touch_policy: u8,
|
touch_policy: TouchPolicy,
|
||||||
) -> Result<GeneratedKey, Error> {
|
) -> Result<PublicKeyInfo, Error> {
|
||||||
let mut in_data = [0u8; 11];
|
// Keygen messages
|
||||||
let mut templ = [0, YKPIV_INS_GENERATE_ASYMMETRIC, 0, 0];
|
// TODO(tarcieri): extract these into an I18N-handling type?
|
||||||
|
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
|
||||||
|
const SZ_ROCA_ALLOW_USER: &str =
|
||||||
|
"was permitted by an end-user configuration setting, but is not recommended.";
|
||||||
|
const SZ_ROCA_ALLOW_ADMIN: &str =
|
||||||
|
"was permitted by an administrator configuration setting, but is not recommended.";
|
||||||
|
const SZ_ROCA_BLOCK_USER: &str = "was blocked due to an end-user configuration setting.";
|
||||||
|
const SZ_ROCA_BLOCK_ADMIN: &str = "was blocked due to an administrator configuration setting.";
|
||||||
|
const SZ_ROCA_DEFAULT: &str = "was permitted by default, but is not recommended. The default behavior will change in a future Yubico release.";
|
||||||
|
|
||||||
let setting_roca: settings::BoolValue;
|
let setting_roca: settings::BoolValue;
|
||||||
|
|
||||||
if yubikey.device_model() == DEVTYPE_YK4
|
match algorithm {
|
||||||
&& (algorithm == YKPIV_ALGO_RSA1024 || algorithm == YKPIV_ALGO_RSA2048)
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||||
&& yubikey.version.major == 4
|
if yubikey.version.major == 4
|
||||||
&& (yubikey.version.minor < 3 || yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
&& (yubikey.version.minor < 3
|
||||||
|
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
||||||
{
|
{
|
||||||
setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true);
|
setting_roca = settings::BoolValue::get(SZ_SETTING_ROCA, true);
|
||||||
|
|
||||||
@@ -239,43 +517,26 @@ pub fn generate(
|
|||||||
return Err(Error::NotSupported);
|
return Err(Error::NotSupported);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match algorithm {
|
|
||||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 | YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => (),
|
|
||||||
_ => {
|
|
||||||
error!("invalid algorithm specified");
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
templ[3] = slot;
|
let templ = [0, Ins::GenerateAsymmetric.code(), 0, slot.into()];
|
||||||
|
|
||||||
let mut offset = 5;
|
let mut in_data = [0u8; 11];
|
||||||
in_data[..offset].copy_from_slice(&[
|
let mut offset = Tlv::write_as(&mut in_data, 0xac, 3, |buf| {
|
||||||
0xac,
|
assert_eq!(algorithm.write(buf).expect("large enough"), 3);
|
||||||
3, // length sans this 2-byte header
|
})?;
|
||||||
YKPIV_ALGO_TAG,
|
|
||||||
1,
|
|
||||||
algorithm,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if in_data[4] == 0 {
|
let pin_len = pin_policy.write(&mut in_data[offset..])?;
|
||||||
error!("unexpected algorithm");
|
in_data[1] += pin_len as u8;
|
||||||
return Err(Error::AlgorithmError);
|
offset += pin_len;
|
||||||
}
|
|
||||||
|
|
||||||
if pin_policy != YKPIV_PINPOLICY_DEFAULT {
|
let touch_len = touch_policy.write(&mut in_data[offset..])?;
|
||||||
in_data[1] += 3;
|
in_data[1] += touch_len as u8;
|
||||||
in_data[offset..(offset + 3)].copy_from_slice(&[YKPIV_PINPOLICY_TAG, 1, pin_policy]);
|
offset += touch_len;
|
||||||
offset += 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
|
|
||||||
in_data[1] += 3;
|
|
||||||
in_data[offset..(offset + 3)].copy_from_slice(&[YKPIV_TOUCHPOLICY_TAG, 1, touch_policy]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = txn.transfer_data(&templ, &in_data[..offset], 1024)?;
|
let response = txn.transfer_data(&templ, &in_data[..offset], 1024)?;
|
||||||
|
|
||||||
@@ -288,12 +549,12 @@ pub fn generate(
|
|||||||
return Err(Error::KeyError);
|
return Err(Error::KeyError);
|
||||||
}
|
}
|
||||||
StatusWords::IncorrectParamError => {
|
StatusWords::IncorrectParamError => {
|
||||||
if pin_policy != YKPIV_PINPOLICY_DEFAULT {
|
match pin_policy {
|
||||||
error!("{} (pin policy not supported?)", err_msg);
|
PinPolicy::Default => match touch_policy {
|
||||||
} else if touch_policy != YKPIV_TOUCHPOLICY_DEFAULT {
|
TouchPolicy::Default => error!("{} (algorithm not supported?)", err_msg),
|
||||||
error!("{} (touch policy not supported?)", err_msg);
|
_ => error!("{} (touch policy not supported?)", err_msg),
|
||||||
} else {
|
},
|
||||||
error!("{} (algorithm not supported?)", err_msg);
|
_ => error!("{} (pin policy not supported?)", err_msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(Error::AlgorithmError);
|
return Err(Error::AlgorithmError);
|
||||||
@@ -303,73 +564,319 @@ pub fn generate(
|
|||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
error!("{} (error {:x})", err_msg, other.code());
|
error!("{} (error {:?})", err_msg, other);
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = response.into_buffer();
|
// TODO(str4d): Response is wrapped in an ASN.1 TLV:
|
||||||
|
//
|
||||||
|
// 0x7f 0x49 -> Application | Constructed | 0x49
|
||||||
match algorithm {
|
match algorithm {
|
||||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||||
let mut offset = 5;
|
// It appears that the inner application-specific value returned by the
|
||||||
let mut len = 0;
|
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
|
||||||
|
//
|
||||||
|
// - Use a full ASN.1 parser on the entire datastructure:
|
||||||
|
//
|
||||||
|
// RSA 1024:
|
||||||
|
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | tag | len:136 |0x81| len:128 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// RSA 2048:
|
||||||
|
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | tag | len:265 |0x81| len:256 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// - Skip the first 5 bytes and use crate::serialize::get_length during TLV
|
||||||
|
// parsing (which treats 128 as a single-byte definite length instead of an
|
||||||
|
// indefinite length):
|
||||||
|
//
|
||||||
|
// RSA 1024:
|
||||||
|
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | |0x81|len128| modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// RSA 2048:
|
||||||
|
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | |0x81| len:256 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// Because of the above, treat this for now as a 2-byte ASN.1 tag with a
|
||||||
|
// 3-byte length.
|
||||||
|
let data = &response.data()[5..];
|
||||||
|
|
||||||
if data[offset] != TAG_RSA_MODULUS {
|
let (data, modulus_tlv) = Tlv::parse(data)?;
|
||||||
|
if modulus_tlv.tag != TAG_RSA_MODULUS {
|
||||||
error!("Failed to parse public key structure (modulus)");
|
error!("Failed to parse public key structure (modulus)");
|
||||||
return Err(Error::ParseError);
|
return Err(Error::ParseError);
|
||||||
}
|
}
|
||||||
|
let modulus = modulus_tlv.value.to_vec();
|
||||||
|
|
||||||
offset += 1;
|
let (_, exp_tlv) = Tlv::parse(data)?;
|
||||||
offset += get_length(&data[offset..], &mut len);
|
if exp_tlv.tag != TAG_RSA_EXP {
|
||||||
let modulus = data[offset..(offset + len)].to_vec();
|
|
||||||
offset += len;
|
|
||||||
|
|
||||||
if data[offset] != TAG_RSA_EXP {
|
|
||||||
error!("failed to parse public key structure (public exponent)");
|
error!("failed to parse public key structure (public exponent)");
|
||||||
return Err(Error::ParseError);
|
return Err(Error::ParseError);
|
||||||
}
|
}
|
||||||
|
let exp = exp_tlv.value.to_vec();
|
||||||
|
|
||||||
offset += 1;
|
Ok(PublicKeyInfo::Rsa {
|
||||||
offset += get_length(&data[offset..], &mut len);
|
|
||||||
let exp = data[offset..(offset + len)].to_vec();
|
|
||||||
Ok(GeneratedKey::Rsa {
|
|
||||||
algorithm,
|
algorithm,
|
||||||
modulus,
|
pubkey: RSAPublicKey::new(
|
||||||
exp,
|
BigUint::from_bytes_be(&modulus),
|
||||||
|
BigUint::from_bytes_be(&exp),
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||||
let mut offset = 3;
|
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
|
||||||
|
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
|
||||||
|
let data = &response.data()[3..];
|
||||||
|
|
||||||
let len = if algorithm == YKPIV_ALGO_ECCP256 {
|
let len = if let AlgorithmId::EccP256 = algorithm {
|
||||||
CB_ECC_POINTP256
|
CB_ECC_POINTP256
|
||||||
} else {
|
} else {
|
||||||
CB_ECC_POINTP384
|
CB_ECC_POINTP384
|
||||||
};
|
};
|
||||||
|
|
||||||
if data[offset] != TAG_ECC_POINT {
|
let (_, tlv) = Tlv::parse(data)?;
|
||||||
|
|
||||||
|
if tlv.tag != TAG_ECC_POINT {
|
||||||
error!("failed to parse public key structure");
|
error!("failed to parse public key structure");
|
||||||
return Err(Error::ParseError);
|
return Err(Error::ParseError);
|
||||||
}
|
}
|
||||||
offset += 1;
|
|
||||||
|
|
||||||
// the curve point should always be determined by the curve
|
// the curve point should always be determined by the curve
|
||||||
let len_byte = data[offset];
|
if tlv.value.len() != len {
|
||||||
offset += 1;
|
|
||||||
|
|
||||||
if len_byte as usize != len {
|
|
||||||
error!("unexpected length");
|
error!("unexpected length");
|
||||||
return Err(Error::AlgorithmError);
|
return Err(Error::AlgorithmError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let point = data[offset..(offset + len)].to_vec();
|
let point = tlv.value.to_vec();
|
||||||
Ok(GeneratedKey::Ecc { algorithm, point })
|
|
||||||
|
if let AlgorithmId::EccP256 = algorithm {
|
||||||
|
EcPublicKey::from_bytes(point).map(PublicKeyInfo::EcP256)
|
||||||
|
} else {
|
||||||
|
EcPublicKey::from_bytes(point).map(PublicKeyInfo::EcP384)
|
||||||
}
|
}
|
||||||
_ => {
|
.map_err(|_| Error::InvalidObject)
|
||||||
error!("wrong algorithm");
|
|
||||||
Err(Error::AlgorithmError)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
fn write_key(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
params: Vec<&[u8]>,
|
||||||
|
pin_policy: PinPolicy,
|
||||||
|
touch_policy: TouchPolicy,
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut key_data = Buffer::new(vec![0u8; KEYDATA_LEN]);
|
||||||
|
let templ = [0, Ins::ImportKey.code(), algorithm.into(), slot.into()];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
let elem_len = algorithm.get_elem_len();
|
||||||
|
let param_tag = algorithm.get_param_tag();
|
||||||
|
|
||||||
|
for (i, param) in params.into_iter().enumerate() {
|
||||||
|
offset += Tlv::write_as(
|
||||||
|
&mut key_data[offset..],
|
||||||
|
param_tag + (i as u8),
|
||||||
|
elem_len,
|
||||||
|
|buf| {
|
||||||
|
let padding = elem_len - param.len();
|
||||||
|
for b in &mut buf[..padding] {
|
||||||
|
*b = 0;
|
||||||
|
}
|
||||||
|
buf[padding..].copy_from_slice(param);
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += pin_policy.write(&mut key_data[offset..])?;
|
||||||
|
offset += touch_policy.write(&mut key_data[offset..])?;
|
||||||
|
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
let status_words = txn
|
||||||
|
.transfer_data(&templ, &key_data[..offset], 256)?
|
||||||
|
.status_words();
|
||||||
|
|
||||||
|
match status_words {
|
||||||
|
StatusWords::Success => Ok(()),
|
||||||
|
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The key data that makes up an RSA key.
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub struct RsaKeyData {
|
||||||
|
/// The secret prime `p`.
|
||||||
|
p: Buffer,
|
||||||
|
/// The secret prime, `q`.
|
||||||
|
q: Buffer,
|
||||||
|
/// D mod (P-1)
|
||||||
|
dp: Buffer,
|
||||||
|
/// D mod (Q-1)
|
||||||
|
dq: Buffer,
|
||||||
|
/// Q^-1 mod P
|
||||||
|
qinv: Buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
impl RsaKeyData {
|
||||||
|
/// Generates a new RSA key data set from two randomly generated, secret, primes.
|
||||||
|
///
|
||||||
|
/// Panics if `secret_p` or `secret_q` are invalid primes.
|
||||||
|
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Self {
|
||||||
|
let p = BigUint::from_bytes_be(secret_p);
|
||||||
|
let q = BigUint::from_bytes_be(secret_q);
|
||||||
|
|
||||||
|
let totient = {
|
||||||
|
let p_t = &p - BigUint::one();
|
||||||
|
let q_t = &p - BigUint::one();
|
||||||
|
|
||||||
|
p_t.lcm(&q_t)
|
||||||
|
};
|
||||||
|
|
||||||
|
let exp = BigUint::from_u64(KEYDATA_RSA_EXP).unwrap();
|
||||||
|
|
||||||
|
let d = exp.mod_inverse(&totient).unwrap();
|
||||||
|
let d = d.to_biguint().unwrap();
|
||||||
|
|
||||||
|
// We calculate the optimization values ahead of time, instead of making the user
|
||||||
|
// do so.
|
||||||
|
|
||||||
|
let dp = &d % (&p - BigUint::one());
|
||||||
|
let dq = &d % (&q - BigUint::one());
|
||||||
|
|
||||||
|
let qinv = q.clone().mod_inverse(&p).unwrap();
|
||||||
|
let (_, qinv) = qinv.to_bytes_be();
|
||||||
|
|
||||||
|
RsaKeyData {
|
||||||
|
p: Zeroizing::new(p.to_bytes_be()),
|
||||||
|
q: Zeroizing::new(q.to_bytes_be()),
|
||||||
|
dp: Zeroizing::new(dp.to_bytes_be()),
|
||||||
|
dq: Zeroizing::new(dq.to_bytes_be()),
|
||||||
|
qinv: Zeroizing::new(qinv),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_len(&self) -> usize {
|
||||||
|
self.p.len() + self.q.len() + self.dp.len() + self.qinv.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imports a private RSA encryption or signing key into the YubiKey.
|
||||||
|
///
|
||||||
|
/// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048`.
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn import_rsa_key(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key_data: RsaKeyData,
|
||||||
|
touch_policy: TouchPolicy,
|
||||||
|
pin_policy: PinPolicy,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => (),
|
||||||
|
_ => return Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_data.total_len() > KEYDATA_LEN {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = vec![
|
||||||
|
key_data.p.as_slice(),
|
||||||
|
key_data.q.as_slice(),
|
||||||
|
key_data.dp.as_slice(),
|
||||||
|
key_data.dq.as_slice(),
|
||||||
|
key_data.qinv.as_slice(),
|
||||||
|
];
|
||||||
|
|
||||||
|
write_key(yubikey, slot, params, pin_policy, touch_policy, algorithm)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Imports a private ECC encryption or signing key into the YubiKey.
|
||||||
|
///
|
||||||
|
/// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`.
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn import_ecc_key(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key_data: &[u8],
|
||||||
|
touch_policy: TouchPolicy,
|
||||||
|
pin_policy: PinPolicy,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => (),
|
||||||
|
_ => return Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_data.len() > KEYDATA_LEN {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = vec![key_data];
|
||||||
|
|
||||||
|
write_key(yubikey, slot, params, pin_policy, touch_policy, algorithm)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an attestation certificate for a stored key.
|
||||||
|
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer, Error> {
|
||||||
|
let templ = [0, Ins::Attest.code(), key.into(), 0];
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
if response.status_words() == StatusWords::NotSupportedError {
|
||||||
|
return Err(Error::NotSupported);
|
||||||
|
} else {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.data()[0] != 0x30 {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Buffer::new(response.data().into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign data using a PIV key
|
||||||
|
pub fn sign_data(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
raw_in: &[u8],
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key: SlotId,
|
||||||
|
) -> Result<Buffer, Error> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
||||||
|
txn.authenticated_command(raw_in, algorithm, key, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt data using a PIV key
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn decrypt_data(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
input: &[u8],
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key: SlotId,
|
||||||
|
) -> Result<Buffer, Error> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
||||||
|
txn.authenticated_command(input, algorithm, key, true)
|
||||||
|
}
|
||||||
|
|||||||
+45
-25
@@ -14,15 +14,15 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Minimum Supported Rust Version
|
//! ## Minimum Supported Rust Version
|
||||||
//!
|
//!
|
||||||
//! Rust 1.39+
|
//! Rust 1.44+
|
||||||
//!
|
//!
|
||||||
//! ## Supported YubiKeys
|
//! ## Supported YubiKeys
|
||||||
//!
|
//!
|
||||||
//! - [YubiKey NEO] series
|
|
||||||
//! - [YubiKey 4] series
|
//! - [YubiKey 4] series
|
||||||
//! - [YubiKey 5] series
|
//! - [YubiKey 5] series
|
||||||
//!
|
//!
|
||||||
//! NOTE: Nano and USB-C variants of the above are also supported
|
//! NOTE: Nano and USB-C variants of the above are also supported.
|
||||||
|
//! Pre-YK4 [YubiKey NEO] series is **NOT** supported.
|
||||||
//!
|
//!
|
||||||
//! ## Supported Algorithms
|
//! ## Supported Algorithms
|
||||||
//!
|
//!
|
||||||
@@ -122,9 +122,10 @@
|
|||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
#![doc(
|
#![doc(
|
||||||
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
|
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/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.1.0"
|
||||||
)]
|
)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
#![warn(
|
#![warn(
|
||||||
missing_docs,
|
missing_docs,
|
||||||
rust_2018_idioms,
|
rust_2018_idioms,
|
||||||
@@ -135,44 +136,63 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
mod apdu;
|
mod apdu;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod cccid;
|
pub mod cccid;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod certificate;
|
pub mod certificate;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod chuid;
|
pub mod chuid;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod consts;
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod container;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod key;
|
pub mod key;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
mod metadata;
|
mod metadata;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod mgm;
|
pub mod mgm;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
|
pub mod mscmap;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub mod msroots;
|
pub mod msroots;
|
||||||
mod response;
|
pub mod policy;
|
||||||
#[cfg(feature = "untested")]
|
pub mod readers;
|
||||||
mod serialization;
|
mod serialization;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
pub mod yubikey;
|
pub mod yubikey;
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
pub use self::{
|
||||||
pub use self::{key::Key, mgm::MgmKey};
|
error::Error,
|
||||||
pub use yubikey::YubiKey;
|
key::Key,
|
||||||
|
mgm::MgmKey,
|
||||||
/// Algorithm identifiers
|
readers::Readers,
|
||||||
// TODO(tarcieri): make this an enum
|
yubikey::{Serial, YubiKey},
|
||||||
pub type AlgorithmId = u8;
|
};
|
||||||
|
|
||||||
/// Object identifiers
|
/// Object identifiers
|
||||||
pub type ObjectId = u32;
|
pub type ObjectId = u32;
|
||||||
|
|
||||||
/// Buffer type (self-zeroizing byte vector)
|
/// Buffer type (self-zeroizing byte vector)
|
||||||
pub(crate) type Buffer = zeroize::Zeroizing<Vec<u8>>;
|
pub(crate) type Buffer = zeroize::Zeroizing<Vec<u8>>;
|
||||||
|
|
||||||
|
/// YubiKey max buffer size
|
||||||
|
pub(crate) const CB_BUF_MAX: usize = 3072;
|
||||||
|
|
||||||
|
/// YubiKey max object size
|
||||||
|
pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9;
|
||||||
|
pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len
|
||||||
|
|
||||||
|
pub(crate) const TAG_ADMIN: u8 = 0x80;
|
||||||
|
pub(crate) const TAG_ADMIN_FLAGS_1: u8 = 0x81;
|
||||||
|
pub(crate) const TAG_ADMIN_SALT: u8 = 0x82;
|
||||||
|
pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
|
||||||
|
pub(crate) const TAG_PROTECTED: u8 = 0x88;
|
||||||
|
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
||||||
|
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
||||||
|
|
||||||
|
/// PIV Applet ID
|
||||||
|
pub(crate) const PIV_AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
|
||||||
|
|
||||||
|
/// MGMT Applet ID.
|
||||||
|
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub(crate) const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
|
||||||
|
|
||||||
|
/// YubiKey OTP Applet ID. Needed to query serial on YK4.
|
||||||
|
pub(crate) const YK_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
|
||||||
|
|||||||
+62
-141
@@ -30,45 +30,28 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, serialization::*, transaction::Transaction, Buffer};
|
use crate::{
|
||||||
use std::{ptr, slice};
|
error::Error, serialization::*, transaction::Transaction, Buffer, TAG_ADMIN, TAG_PROTECTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::{CB_OBJ_MAX, CB_OBJ_TAG_MAX};
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
pub const OBJ_ADMIN_DATA: u32 = 0x005f_ff00;
|
||||||
|
pub const OBJ_PRINTED: u32 = 0x005f_c109;
|
||||||
|
|
||||||
/// Get metadata item
|
/// Get metadata item
|
||||||
pub(crate) fn get_item(data: &[u8], tag: u8) -> Result<&[u8], Error> {
|
pub(crate) fn get_item(mut data: &[u8], tag: u8) -> Result<&[u8], Error> {
|
||||||
let mut p_temp: *const u8 = data.as_ptr();
|
while !data.is_empty() {
|
||||||
let mut cb_temp: usize = 0;
|
let (remaining, tlv) = Tlv::parse(data)?;
|
||||||
let mut tag_temp: u8;
|
data = remaining;
|
||||||
|
|
||||||
unsafe {
|
if tlv.tag == tag {
|
||||||
while p_temp < data.as_ptr().add(data.len()) {
|
|
||||||
tag_temp = *p_temp;
|
|
||||||
p_temp = p_temp.add(1);
|
|
||||||
|
|
||||||
let p_slice = slice::from_raw_parts(
|
|
||||||
p_temp,
|
|
||||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !has_valid_length(
|
|
||||||
p_slice,
|
|
||||||
data.as_ptr().add(data.len()) as usize - p_temp as usize,
|
|
||||||
) {
|
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
p_temp = p_temp.add(get_length(p_slice, &mut cb_temp));
|
|
||||||
|
|
||||||
if tag_temp == tag {
|
|
||||||
// found tag
|
// found tag
|
||||||
break;
|
return Ok(tlv.value);
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +59,7 @@ pub(crate) fn get_item(data: &[u8], tag: u8) -> Result<&[u8], Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set metadata item
|
/// Set metadata item
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub(crate) fn set_item(
|
pub(crate) fn set_item(
|
||||||
data: &mut [u8],
|
data: &mut [u8],
|
||||||
pcb_data: &mut usize,
|
pcb_data: &mut usize,
|
||||||
@@ -83,35 +67,25 @@ pub(crate) fn set_item(
|
|||||||
tag: u8,
|
tag: u8,
|
||||||
p_item: &[u8],
|
p_item: &[u8],
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut p_temp: *mut u8 = data.as_mut_ptr();
|
|
||||||
let mut cb_temp: usize = 0;
|
let mut cb_temp: usize = 0;
|
||||||
let mut tag_temp: u8 = 0;
|
let mut tag_temp: u8 = 0;
|
||||||
let mut cb_len: usize = 0;
|
let mut cb_len: usize = 0;
|
||||||
let cb_item = p_item.len();
|
let cb_item = p_item.len();
|
||||||
// Must be signed to have negative offsets
|
|
||||||
let cb_moved: isize;
|
|
||||||
let p_next: *mut u8;
|
|
||||||
|
|
||||||
while p_temp < data[*pcb_data..].as_mut_ptr() {
|
let mut offset = 0;
|
||||||
unsafe {
|
|
||||||
tag_temp = *p_temp;
|
|
||||||
p_temp = p_temp.add(1);
|
|
||||||
|
|
||||||
cb_len = get_length(
|
while offset < *pcb_data {
|
||||||
slice::from_raw_parts(
|
tag_temp = data[offset];
|
||||||
p_temp,
|
offset += 1;
|
||||||
data.as_mut_ptr() as usize + data.len() - p_temp as usize,
|
|
||||||
),
|
cb_len = get_length(&data[offset..], &mut cb_temp);
|
||||||
&mut cb_temp,
|
offset += cb_len;
|
||||||
);
|
|
||||||
p_temp = p_temp.add(cb_len);
|
|
||||||
|
|
||||||
if tag_temp == tag {
|
if tag_temp == tag {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
p_temp = p_temp.add(cb_temp);
|
offset += cb_temp;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag_temp != tag {
|
if tag_temp != tag {
|
||||||
@@ -120,75 +94,50 @@ pub(crate) fn set_item(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
// We did not find an existing tag, append
|
||||||
p_temp = data.as_mut_ptr().add(*pcb_data);
|
*pcb_data += Tlv::write(&mut data[*pcb_data..], tag, p_item)?;
|
||||||
cb_len = get_length_size(cb_item);
|
|
||||||
|
|
||||||
if (*pcb_data + cb_len + cb_item) > cb_data_max {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
*p_temp = tag;
|
|
||||||
p_temp = p_temp.add(1);
|
|
||||||
p_temp = p_temp.add(set_length(
|
|
||||||
slice::from_raw_parts_mut(
|
|
||||||
p_temp,
|
|
||||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
|
||||||
),
|
|
||||||
cb_item,
|
|
||||||
));
|
|
||||||
|
|
||||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
|
||||||
}
|
|
||||||
|
|
||||||
*pcb_data += 1 + cb_len + cb_item;
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Found tag
|
||||||
|
|
||||||
|
// Check length, if it matches, overwrite
|
||||||
if cb_temp == cb_item {
|
if cb_temp == cb_item {
|
||||||
unsafe {
|
data[offset..offset + cb_item].copy_from_slice(p_item);
|
||||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
p_next = unsafe { p_temp.add(cb_temp) };
|
// Length doesn't match, expand/shrink to fit
|
||||||
cb_moved = (cb_item as isize - cb_temp as isize)
|
let next_offset = offset + cb_temp;
|
||||||
|
// Must be signed to have negative offsets
|
||||||
|
let cb_moved: isize = (cb_item as isize - cb_temp as isize)
|
||||||
+ if cb_item != 0 {
|
+ if cb_item != 0 {
|
||||||
get_length_size(cb_item) as isize
|
get_length_size(cb_item) as isize
|
||||||
} else {
|
} else {
|
||||||
|
// For tag, if deleting
|
||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
|
// Accounts for different length encoding
|
||||||
- cb_len as isize;
|
- cb_len as isize;
|
||||||
|
|
||||||
if (*pcb_data + cb_moved as usize) > cb_data_max {
|
// If length would cause buffer overflow, return error
|
||||||
|
if (*pcb_data as isize + cb_moved) as usize > cb_data_max {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
// Move remaining data
|
||||||
ptr::copy(
|
data.copy_within(
|
||||||
p_next,
|
next_offset..*pcb_data,
|
||||||
p_next.offset(cb_moved),
|
(next_offset as isize + cb_moved) as usize,
|
||||||
*pcb_data - p_next as usize - data.as_ptr() as usize,
|
|
||||||
);
|
);
|
||||||
}
|
*pcb_data = (*pcb_data as isize + cb_moved) as usize;
|
||||||
|
|
||||||
*pcb_data += cb_moved as usize;
|
|
||||||
|
|
||||||
|
// Re-encode item and insert
|
||||||
if cb_item != 0 {
|
if cb_item != 0 {
|
||||||
unsafe {
|
offset -= cb_len;
|
||||||
p_temp = p_temp.offset(-(cb_len as isize));
|
offset += set_length(&mut data[offset..], cb_item)?;
|
||||||
p_temp = p_temp.add(set_length(
|
data[offset..offset + cb_item].copy_from_slice(p_item);
|
||||||
slice::from_raw_parts_mut(
|
|
||||||
p_temp,
|
|
||||||
data.as_ptr() as usize + data.len() - p_temp as usize,
|
|
||||||
),
|
|
||||||
cb_item,
|
|
||||||
));
|
|
||||||
ptr::copy(p_item.as_ptr(), p_temp, cb_item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -197,52 +146,25 @@ pub(crate) fn set_item(
|
|||||||
/// Read metadata
|
/// Read metadata
|
||||||
pub(crate) fn read(txn: &Transaction<'_>, tag: u8) -> Result<Buffer, Error> {
|
pub(crate) fn read(txn: &Transaction<'_>, tag: u8) -> Result<Buffer, Error> {
|
||||||
let obj_id = match tag {
|
let obj_id = match tag {
|
||||||
TAG_ADMIN => YKPIV_OBJ_ADMIN_DATA,
|
TAG_ADMIN => OBJ_ADMIN_DATA,
|
||||||
TAG_PROTECTED => YKPIV_OBJ_PRINTED,
|
TAG_PROTECTED => OBJ_PRINTED,
|
||||||
_ => return Err(Error::InvalidObject),
|
_ => return Err(Error::InvalidObject),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut data = txn.fetch_object(obj_id)?;
|
let data = txn.fetch_object(obj_id)?;
|
||||||
|
Tlv::parse_single(data, tag)
|
||||||
if data.len() < CB_OBJ_TAG_MIN {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if tag != data[0] {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pcb_data = 0;
|
|
||||||
let offset = 1 + get_length(&data[1..], &mut pcb_data);
|
|
||||||
|
|
||||||
if pcb_data > data.len() - offset {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
ptr::copy(data.as_ptr().add(offset), data.as_mut_ptr(), pcb_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.truncate(pcb_data);
|
|
||||||
Ok(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write metadata
|
/// Write metadata
|
||||||
pub(crate) fn write(
|
#[cfg(feature = "untested")]
|
||||||
txn: &Transaction<'_>,
|
pub(crate) fn write(txn: &Transaction<'_>, tag: u8, data: &[u8]) -> Result<(), Error> {
|
||||||
tag: u8,
|
if data.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
|
||||||
data: &[u8],
|
|
||||||
max_size: usize,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut buf = Zeroizing::new(vec![0u8; CB_OBJ_MAX]);
|
|
||||||
|
|
||||||
if data.len() > max_size - CB_OBJ_TAG_MAX {
|
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let obj_id = match tag {
|
let obj_id = match tag {
|
||||||
TAG_ADMIN => YKPIV_OBJ_ADMIN_DATA,
|
TAG_ADMIN => OBJ_ADMIN_DATA,
|
||||||
TAG_PROTECTED => YKPIV_OBJ_PRINTED,
|
TAG_PROTECTED => OBJ_PRINTED,
|
||||||
_ => return Err(Error::InvalidObject),
|
_ => return Err(Error::InvalidObject),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,15 +173,14 @@ pub(crate) fn write(
|
|||||||
return txn.save_object(obj_id, &[]);
|
return txn.save_object(obj_id, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
buf[0] = tag;
|
let mut buf = Zeroizing::new(vec![0u8; CB_OBJ_MAX]);
|
||||||
let mut offset = set_length(&mut buf[1..], data.len());
|
let len = Tlv::write(&mut buf, tag, data)?;
|
||||||
buf[offset..(offset + data.len())].copy_from_slice(data);
|
|
||||||
offset += data.len();
|
|
||||||
|
|
||||||
txn.save_object(obj_id, &buf[..offset])
|
txn.save_object(obj_id, &buf[..len])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the size of a length tag for the given length
|
/// Get the size of a length tag for the given length
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
fn get_length_size(length: usize) -> usize {
|
fn get_length_size(length: usize) -> usize {
|
||||||
if length < 0x80 {
|
if length < 0x80 {
|
||||||
1
|
1
|
||||||
|
|||||||
+39
-13
@@ -30,27 +30,50 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, metadata, yubikey::YubiKey};
|
use crate::error::Error;
|
||||||
use des::{
|
|
||||||
block_cipher_trait::{generic_array::GenericArray, BlockCipher},
|
|
||||||
TdesEde3,
|
|
||||||
};
|
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
use hmac::Hmac;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use pbkdf2::pbkdf2;
|
|
||||||
use sha1::Sha1;
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::{
|
||||||
|
metadata, yubikey::YubiKey, CB_BUF_MAX, CB_OBJ_MAX, TAG_ADMIN, TAG_ADMIN_FLAGS_1,
|
||||||
|
TAG_ADMIN_SALT, TAG_PROTECTED, TAG_PROTECTED_MGM,
|
||||||
|
};
|
||||||
|
use des::{
|
||||||
|
cipher::{generic_array::GenericArray, BlockCipher, NewBlockCipher},
|
||||||
|
TdesEde3,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use hmac::Hmac;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use pbkdf2::pbkdf2;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
pub(crate) const ADMIN_FLAGS_1_PROTECTED_MGM: u8 = 0x02;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
const CB_ADMIN_SALT: usize = 16;
|
||||||
|
|
||||||
/// Default MGM key configured on all YubiKeys
|
/// Default MGM key configured on all YubiKeys
|
||||||
const DEFAULT_MGM_KEY: [u8; DES_LEN_3DES] = [
|
const DEFAULT_MGM_KEY: [u8; DES_LEN_3DES] = [
|
||||||
1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8,
|
1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Size of a DES key
|
||||||
|
const DES_LEN_DES: usize = 8;
|
||||||
|
|
||||||
|
/// Size of a 3DES key
|
||||||
|
pub(crate) const DES_LEN_3DES: usize = DES_LEN_DES * 3;
|
||||||
|
|
||||||
|
/// Number of PBKDF2 iterations to use when deriving from a password
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
const ITER_MGM_PBKDF2: u32 = 10000;
|
||||||
|
|
||||||
/// Management Key (MGM) key types (manual/derived/protected)
|
/// Management Key (MGM) key types (manual/derived/protected)
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
pub enum MgmType {
|
pub enum MgmType {
|
||||||
/// Manual
|
/// Manual
|
||||||
Manual = 0,
|
Manual = 0,
|
||||||
@@ -107,6 +130,7 @@ impl MgmKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get derived management key (MGM)
|
/// Get derived management key (MGM)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn get_derived(yubikey: &mut YubiKey, pin: &[u8]) -> Result<Self, Error> {
|
pub fn get_derived(yubikey: &mut YubiKey, pin: &[u8]) -> Result<Self, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
@@ -131,6 +155,7 @@ impl MgmKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get protected management key (MGM)
|
/// Get protected management key (MGM)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn get_protected(yubikey: &mut YubiKey) -> Result<Self, Error> {
|
pub fn get_protected(yubikey: &mut YubiKey) -> Result<Self, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
@@ -158,16 +183,17 @@ impl MgmKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the management key (MGM)
|
/// Set the management key (MGM)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn set(&self, yubikey: &mut YubiKey, touch: Option<u8>) -> Result<(), Error> {
|
pub fn set(&self, yubikey: &mut YubiKey, touch: Option<u8>) -> Result<(), Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
txn.set_mgm_key(&self, touch)
|
txn.set_mgm_key(&self, touch)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set protected management key (MGM)
|
/// Set protected management key (MGM)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub fn set_protected(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
pub fn set_protected(&self, yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||||
let mut data = Zeroizing::new(vec![0u8; CB_BUF_MAX]);
|
let mut data = Zeroizing::new(vec![0u8; CB_BUF_MAX]);
|
||||||
|
|
||||||
let max_size = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
txn.set_mgm_key(self, None).map_err(|e| {
|
txn.set_mgm_key(self, None).map_err(|e| {
|
||||||
@@ -200,7 +226,7 @@ impl MgmKey {
|
|||||||
) {
|
) {
|
||||||
error!("could not set protected mgm item, err = {:?}", e);
|
error!("could not set protected mgm item, err = {:?}", e);
|
||||||
} else {
|
} else {
|
||||||
metadata::write(&txn, TAG_PROTECTED, &data, max_size).map_err(|e| {
|
metadata::write(&txn, TAG_PROTECTED, &data).map_err(|e| {
|
||||||
error!("could not write protected data, err = {:?}", e);
|
error!("could not write protected data, err = {:?}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
@@ -247,7 +273,7 @@ impl MgmKey {
|
|||||||
&flags_1,
|
&flags_1,
|
||||||
) {
|
) {
|
||||||
error!("could not set admin flags item, err = {}", e);
|
error!("could not set admin flags item, err = {}", e);
|
||||||
} else if let Err(e) = metadata::write(&txn, TAG_ADMIN, &data[..cb_data], max_size) {
|
} else if let Err(e) = metadata::write(&txn, TAG_ADMIN, &data[..cb_data]) {
|
||||||
error!("could not write admin data, err = {}", e);
|
error!("could not write admin data, err = {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +294,7 @@ impl MgmKey {
|
|||||||
pub(crate) fn decrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
|
pub(crate) fn decrypt(&self, input: &[u8; DES_LEN_DES]) -> [u8; DES_LEN_DES] {
|
||||||
let mut output = input.to_owned();
|
let mut output = input.to_owned();
|
||||||
TdesEde3::new(GenericArray::from_slice(&self.0))
|
TdesEde3::new(GenericArray::from_slice(&self.0))
|
||||||
.encrypt_block(GenericArray::from_mut_slice(&mut output));
|
.decrypt_block(GenericArray::from_mut_slice(&mut output));
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,23 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, key::SlotId, serialization::*, yubikey::YubiKey};
|
use crate::{error::Error, key::SlotId, serialization::*, yubikey::YubiKey, CB_OBJ_MAX};
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::{
|
use std::{
|
||||||
convert::{TryFrom, TryInto},
|
convert::{TryFrom, TryInto},
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Container name length
|
||||||
|
const CONTAINER_NAME_LEN: usize = 40;
|
||||||
|
|
||||||
|
/// Container record length: 27 = 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20
|
||||||
|
const CONTAINER_REC_LEN: usize = (2 * CONTAINER_NAME_LEN) + 27;
|
||||||
|
|
||||||
|
const OBJ_MSCMAP: u32 = 0x005f_ff10;
|
||||||
|
|
||||||
|
const TAG_MSCMAP: u8 = 0x81;
|
||||||
|
|
||||||
/// MS Container Map(?) Records
|
/// MS Container Map(?) Records
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
@@ -72,28 +82,23 @@ impl Container {
|
|||||||
/// Read MS Container Map records
|
/// Read MS Container Map records
|
||||||
pub fn read_mscmap(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
|
pub fn read_mscmap(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let response = txn.fetch_object(YKPIV_OBJ_MSCMAP)?;
|
let response = txn.fetch_object(OBJ_MSCMAP)?;
|
||||||
let mut containers = vec![];
|
let mut containers = vec![];
|
||||||
|
|
||||||
if response.len() < CB_OBJ_TAG_MIN {
|
let (_, tlv) = match Tlv::parse(&response) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => {
|
||||||
// TODO(tarcieri): is this really OK?
|
// TODO(tarcieri): is this really OK?
|
||||||
return Ok(containers);
|
return Ok(containers);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if response[0] != TAG_MSCMAP {
|
if tlv.tag != TAG_MSCMAP {
|
||||||
// TODO(tarcieri): yubico-piv-tool returned success here? should we?
|
// TODO(tarcieri): yubico-piv-tool returned success here? should we?
|
||||||
return Err(Error::InvalidObject);
|
return Err(Error::InvalidObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut len = 0;
|
for chunk in tlv.value.chunks_exact(CONTAINER_REC_LEN) {
|
||||||
let offset = 1 + get_length(&response[1..], &mut len);
|
|
||||||
|
|
||||||
if len > response.len() - offset {
|
|
||||||
// TODO(tarcieri): is this really OK?
|
|
||||||
return Ok(containers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for chunk in response[offset..(offset + len)].chunks_exact(CONTAINER_REC_LEN) {
|
|
||||||
containers.push(Container::new(chunk)?);
|
containers.push(Container::new(chunk)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,37 +107,23 @@ impl Container {
|
|||||||
|
|
||||||
/// Write MS Container Map records.
|
/// Write MS Container Map records.
|
||||||
pub fn write_mscmap(yubikey: &mut YubiKey, containers: &[Self]) -> Result<(), Error> {
|
pub fn write_mscmap(yubikey: &mut YubiKey, containers: &[Self]) -> Result<(), Error> {
|
||||||
let mut buf = [0u8; CB_OBJ_MAX];
|
|
||||||
let mut offset = 0;
|
|
||||||
let n_containers = containers.len();
|
let n_containers = containers.len();
|
||||||
let data_len = n_containers * CONTAINER_REC_LEN;
|
let data_len = n_containers * CONTAINER_REC_LEN;
|
||||||
|
|
||||||
let max_size = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
if n_containers == 0 {
|
if n_containers == 0 {
|
||||||
return txn.save_object(YKPIV_OBJ_MSCMAP, &[]);
|
return txn.save_object(OBJ_MSCMAP, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let req_len = 1 + set_length(&mut buf, data_len) + data_len;
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
|
let offset = Tlv::write_as(&mut buf, TAG_MSCMAP, data_len, |buf| {
|
||||||
if req_len > max_size {
|
for (i, chunk) in buf.chunks_exact_mut(CONTAINER_REC_LEN).enumerate() {
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf[offset] = TAG_MSCMAP;
|
|
||||||
offset += 1;
|
|
||||||
offset += set_length(&mut buf[offset..], data_len);
|
|
||||||
|
|
||||||
for (i, chunk) in buf[..data_len]
|
|
||||||
.chunks_exact_mut(CONTAINER_REC_LEN)
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
chunk.copy_from_slice(&containers[i].to_bytes());
|
chunk.copy_from_slice(&containers[i].to_bytes());
|
||||||
}
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
offset += data_len;
|
txn.save_object(OBJ_MSCMAP, &buf[..offset])
|
||||||
txn.save_object(YKPIV_OBJ_MSCMAP, &buf[..offset])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a container record from a byte slice
|
/// Parse a container record from a byte slice
|
||||||
@@ -158,7 +149,7 @@ impl Container {
|
|||||||
|
|
||||||
Ok(Container {
|
Ok(Container {
|
||||||
name,
|
name,
|
||||||
slot: bytes[name_bytes_len],
|
slot: bytes[name_bytes_len].try_into()?,
|
||||||
key_spec: bytes[name_bytes_len + 1],
|
key_spec: bytes[name_bytes_len + 1],
|
||||||
key_size_bits: u16::from_le_bytes(
|
key_size_bits: u16::from_le_bytes(
|
||||||
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
|
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
|
||||||
@@ -185,7 +176,7 @@ impl Container {
|
|||||||
bytes.extend_from_slice(&self.name[i].to_le_bytes());
|
bytes.extend_from_slice(&self.name[i].to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.push(self.slot);
|
bytes.push(self.slot.into());
|
||||||
bytes.push(self.key_spec);
|
bytes.push(self.key_spec);
|
||||||
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
|
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
|
||||||
bytes.push(self.flags);
|
bytes.push(self.flags);
|
||||||
@@ -204,7 +195,7 @@ impl Debug for Container {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"PivContainer {{ name: {:?}, slot: {}, key_spec: {}, key_size_bits: {}, \
|
"PivContainer {{ name: {:?}, slot: {:?}, key_spec: {}, key_size_bits: {}, \
|
||||||
flags: {}, pin_id: {}, associated_echd_container: {}, cert_fingerprint: {:?} }}",
|
flags: {}, pin_id: {}, associated_echd_container: {}, cert_fingerprint: {:?} }}",
|
||||||
&self.name[..],
|
&self.name[..],
|
||||||
self.slot,
|
self.slot,
|
||||||
+42
-64
@@ -37,9 +37,21 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, error::Error, serialization::*, yubikey::YubiKey};
|
use crate::{error::Error, serialization::*, yubikey::YubiKey};
|
||||||
|
use crate::{CB_OBJ_MAX, CB_OBJ_TAG_MAX};
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::{ptr, slice};
|
|
||||||
|
const OBJ_MSROOTS1: u32 = 0x005f_ff11;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const OBJ_MSROOTS2: u32 = 0x005f_ff12;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const OBJ_MSROOTS3: u32 = 0x005f_ff13;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const OBJ_MSROOTS4: u32 = 0x005f_ff14;
|
||||||
|
const OBJ_MSROOTS5: u32 = 0x005f_ff15;
|
||||||
|
|
||||||
|
const TAG_MSROOTS_END: u8 = 0x82;
|
||||||
|
const TAG_MSROOTS_MID: u8 = 0x83;
|
||||||
|
|
||||||
/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots
|
/// `msroots` file: PKCS#7-formatted certificate store for enterprise trust roots
|
||||||
pub struct MsRoots(Vec<u8>);
|
pub struct MsRoots(Vec<u8>);
|
||||||
@@ -51,72 +63,40 @@ impl MsRoots {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read `msroots` file from YubiKey
|
/// Read `msroots` file from YubiKey
|
||||||
pub fn read(yubikey: &mut YubiKey) -> Result<Vec<Self>, Error> {
|
pub fn read(yubikey: &mut YubiKey) -> Result<Option<Self>, Error> {
|
||||||
let mut len: usize = 0;
|
|
||||||
let mut ptr: *mut u8;
|
|
||||||
let mut tag: u8;
|
|
||||||
let mut offset: usize = 0;
|
|
||||||
|
|
||||||
let mut results = vec![];
|
|
||||||
let cb_data = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
// allocate first page
|
// allocate first page
|
||||||
let mut p_data = vec![0u8; cb_data];
|
let mut data = Vec::with_capacity(CB_OBJ_MAX);
|
||||||
|
|
||||||
for object_id in YKPIV_OBJ_MSROOTS1..YKPIV_OBJ_MSROOTS5 {
|
for object_id in OBJ_MSROOTS1..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();
|
let (_, tlv) = match Tlv::parse(&buf) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
if cb_buf < CB_OBJ_TAG_MIN {
|
if (TAG_MSROOTS_MID != tlv.tag || OBJ_MSROOTS5 == object_id)
|
||||||
return Ok(results);
|
&& (TAG_MSROOTS_END != tlv.tag)
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
tag = *ptr;
|
|
||||||
ptr = ptr.add(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TAG_MSROOTS_MID != tag || YKPIV_OBJ_MSROOTS5 == object_id)
|
|
||||||
&& (TAG_MSROOTS_END != tag)
|
|
||||||
{
|
{
|
||||||
// the current object doesn't contain a valid part of a msroots file
|
// the current object doesn't contain a valid part of a msroots file
|
||||||
|
|
||||||
// treat condition as object isn't found
|
// treat condition as object isn't found
|
||||||
return Ok(results);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
data.extend_from_slice(tlv.value);
|
||||||
ptr = ptr.add(get_length(
|
|
||||||
slice::from_raw_parts(ptr, buf.as_ptr() as usize + buf.len() - ptr as usize),
|
|
||||||
&mut len,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that decoded length represents object contents
|
if tlv.tag == TAG_MSROOTS_END {
|
||||||
if len > cb_buf - (ptr as isize - buf.as_mut_ptr() as isize) as usize {
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
ptr::copy(ptr, p_data.as_mut_ptr().add(offset), len);
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += len;
|
|
||||||
|
|
||||||
match MsRoots::new(&p_data[..offset]) {
|
|
||||||
Ok(msroots) => results.push(msroots),
|
|
||||||
Err(res) => error!("error parsing msroots: {:?}", res),
|
|
||||||
}
|
|
||||||
|
|
||||||
if tag == TAG_MSROOTS_END {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(results)
|
MsRoots::new(&data).map(Some).map_err(|e| {
|
||||||
|
error!("error parsing msroots: {:?}", e);
|
||||||
|
e
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `msroots` file to YubiKey
|
/// Write `msroots` file to YubiKey
|
||||||
@@ -128,15 +108,14 @@ impl MsRoots {
|
|||||||
let data = &self.0;
|
let data = &self.0;
|
||||||
let data_len = data.len();
|
let data_len = data.len();
|
||||||
let n_objs: usize;
|
let n_objs: usize;
|
||||||
let cb_obj_max = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
if data_len == 0 {
|
if data_len == 0 {
|
||||||
return txn.save_object(YKPIV_OBJ_MSROOTS1, &[]);
|
return txn.save_object(OBJ_MSROOTS1, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate number of objects required to store blob
|
// Calculate number of objects required to store blob
|
||||||
n_objs = (data_len / (cb_obj_max - CB_OBJ_TAG_MAX)) + 1;
|
n_objs = (data_len / (CB_OBJ_MAX - CB_OBJ_TAG_MAX)) + 1;
|
||||||
|
|
||||||
if n_objs > 5 {
|
if n_objs > 5 {
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
@@ -145,24 +124,23 @@ impl MsRoots {
|
|||||||
for i in 0..n_objs {
|
for i in 0..n_objs {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
data_chunk = if cb_obj_max - CB_OBJ_TAG_MAX < data_len - data_offset {
|
data_chunk = if CB_OBJ_MAX - CB_OBJ_TAG_MAX < data_len - data_offset {
|
||||||
cb_obj_max - CB_OBJ_TAG_MAX
|
CB_OBJ_MAX - CB_OBJ_TAG_MAX
|
||||||
} else {
|
} else {
|
||||||
data_len - data_offset
|
data_len - data_offset
|
||||||
};
|
};
|
||||||
|
|
||||||
buf[offset] = if i == n_objs - 1 {
|
offset += Tlv::write(
|
||||||
|
&mut buf,
|
||||||
|
if i == n_objs - 1 {
|
||||||
TAG_MSROOTS_END
|
TAG_MSROOTS_END
|
||||||
} else {
|
} else {
|
||||||
TAG_MSROOTS_MID
|
TAG_MSROOTS_MID
|
||||||
};
|
},
|
||||||
|
&data[data_offset..(data_offset + data_chunk)],
|
||||||
|
)?;
|
||||||
|
|
||||||
offset += 1;
|
txn.save_object(OBJ_MSROOTS1 + i as u32, &buf[..offset])?;
|
||||||
offset += set_length(&mut buf[offset..], data_chunk);
|
|
||||||
buf[offset..].copy_from_slice(&data[data_offset..(data_offset + data_chunk)]);
|
|
||||||
offset += data_chunk;
|
|
||||||
|
|
||||||
txn.save_object(YKPIV_OBJ_MSROOTS1 + i as u32, &buf[..offset])?;
|
|
||||||
|
|
||||||
data_offset += data_chunk;
|
data_offset += data_chunk;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//! Enums representing key policies.
|
||||||
|
|
||||||
|
use crate::{error::Error, serialization::Tlv};
|
||||||
|
|
||||||
|
/// Specifies how often the PIN needs to be entered for access to the credential in a
|
||||||
|
/// given slot. This policy must be set upon key generation or importation, and cannot be
|
||||||
|
/// changed later.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum PinPolicy {
|
||||||
|
/// Use the default PIN policy for the slot. See the slot's documentation for details.
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// The end user PIN is **NOT** required to perform private key operations.
|
||||||
|
Never,
|
||||||
|
|
||||||
|
/// The end user PIN is required to perform any private key operations. Once the
|
||||||
|
/// correct PIN has been provided, multiple private key operations may be performed
|
||||||
|
/// without additional cardholder consent.
|
||||||
|
Once,
|
||||||
|
|
||||||
|
/// The end user PIN is required to perform any private key operations. The PIN must
|
||||||
|
/// be submitted immediately before each operation to ensure cardholder participation.
|
||||||
|
Always,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PinPolicy> for u8 {
|
||||||
|
fn from(policy: PinPolicy) -> u8 {
|
||||||
|
match policy {
|
||||||
|
PinPolicy::Default => 0,
|
||||||
|
PinPolicy::Never => 1,
|
||||||
|
PinPolicy::Once => 2,
|
||||||
|
PinPolicy::Always => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PinPolicy {
|
||||||
|
/// Writes the `PinPolicy` in the format the YubiKey expects during key generation or
|
||||||
|
/// importation.
|
||||||
|
pub(crate) fn write(self, buf: &mut [u8]) -> Result<usize, Error> {
|
||||||
|
match self {
|
||||||
|
PinPolicy::Default => Ok(0),
|
||||||
|
_ => Tlv::write(buf, 0xaa, &[self.into()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specifies under what conditions a physical touch on the metal contact is required, in
|
||||||
|
/// addition to the [`PinPolicy`]. This policy must be set upon key generation or
|
||||||
|
/// importation, and cannot be changed later.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum TouchPolicy {
|
||||||
|
/// Use the default touch policy for the slot.
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// A physical touch is **NOT** required to perform private key operations.
|
||||||
|
Never,
|
||||||
|
|
||||||
|
/// A physical touch is required to perform any private key operations. The metal
|
||||||
|
/// contact must be touched during each operation to ensure cardholder participation.
|
||||||
|
Always,
|
||||||
|
|
||||||
|
/// A physical touch is required to perform any private key operations. Each touch
|
||||||
|
/// is cached for 15 seconds, during which time multiple private key operations may be
|
||||||
|
/// performed without additional cardholder interaction. After 15 seconds the cached
|
||||||
|
/// touch is cleared, and further operations require another physical touch.
|
||||||
|
Cached,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TouchPolicy> for u8 {
|
||||||
|
fn from(policy: TouchPolicy) -> u8 {
|
||||||
|
match policy {
|
||||||
|
TouchPolicy::Default => 0,
|
||||||
|
TouchPolicy::Never => 1,
|
||||||
|
TouchPolicy::Always => 2,
|
||||||
|
TouchPolicy::Cached => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TouchPolicy {
|
||||||
|
/// Writes the `TouchPolicy` in the format the YubiKey expects during key generation
|
||||||
|
/// or importation.
|
||||||
|
pub(crate) fn write(self, buf: &mut [u8]) -> Result<usize, Error> {
|
||||||
|
match self {
|
||||||
|
TouchPolicy::Default => Ok(0),
|
||||||
|
_ => Tlv::write(buf, 0xab, &[self.into()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+108
-7
@@ -30,30 +30,131 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{consts::*, ObjectId};
|
use crate::{error::Error, Buffer, ObjectId, CB_OBJ_TAG_MIN};
|
||||||
|
|
||||||
|
pub const OBJ_DISCOVERY: u32 = 0x7e;
|
||||||
|
|
||||||
// TODO(tarcieri): refactor these into better serializers/message builders
|
// TODO(tarcieri): refactor these into better serializers/message builders
|
||||||
|
|
||||||
|
/// A Type-Length-Value object that has been parsed from a buffer.
|
||||||
|
pub(crate) struct Tlv<'a> {
|
||||||
|
pub(crate) tag: u8,
|
||||||
|
pub(crate) value: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Tlv<'a> {
|
||||||
|
/// Parses a `Tlv` from a buffer, returning the remainder of the buffer.
|
||||||
|
pub(crate) fn parse(buffer: &'a [u8]) -> Result<(&'a [u8], Self), Error> {
|
||||||
|
if buffer.len() < CB_OBJ_TAG_MIN || !has_valid_length(&buffer[1..], buffer.len() - 1) {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = buffer[0];
|
||||||
|
|
||||||
|
let mut len = 0;
|
||||||
|
let offset = 1 + get_length(&buffer[1..], &mut len);
|
||||||
|
|
||||||
|
let (value, buffer) = buffer[offset..].split_at(len);
|
||||||
|
|
||||||
|
Ok((buffer, Tlv { tag, value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a [`Buffer`] containing a single `Tlv` with the given tag, and returns a
|
||||||
|
/// `Buffer` containing only the value part of the `Tlv`.
|
||||||
|
pub(crate) fn parse_single(mut buffer: Buffer, tag: u8) -> Result<Buffer, Error> {
|
||||||
|
if buffer.len() < CB_OBJ_TAG_MIN || !has_valid_length(&buffer[1..], buffer.len() - 1) {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag != buffer[0] {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut len = 0;
|
||||||
|
let offset = 1 + get_length(&buffer[1..], &mut len);
|
||||||
|
|
||||||
|
buffer.copy_within(offset..offset + len, 0);
|
||||||
|
buffer.truncate(len);
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a TLV to the given buffer.
|
||||||
|
pub(crate) fn write(buffer: &mut [u8], tag: u8, value: &[u8]) -> Result<usize, Error> {
|
||||||
|
if buffer.len() < CB_OBJ_TAG_MIN {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
buffer[0] = tag;
|
||||||
|
|
||||||
|
let offset = 1 + set_length(&mut buffer[1..], value.len())?;
|
||||||
|
|
||||||
|
if buffer.len() < offset + value.len() {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
buffer[offset..offset + value.len()].copy_from_slice(value);
|
||||||
|
|
||||||
|
Ok(offset + value.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a TLV to the given buffer.
|
||||||
|
///
|
||||||
|
/// `value` is guaranteed to be called with a mutable slice of length `length`.
|
||||||
|
pub(crate) fn write_as<Gen>(
|
||||||
|
buffer: &mut [u8],
|
||||||
|
tag: u8,
|
||||||
|
length: usize,
|
||||||
|
value: Gen,
|
||||||
|
) -> Result<usize, Error>
|
||||||
|
where
|
||||||
|
Gen: FnOnce(&mut [u8]),
|
||||||
|
{
|
||||||
|
if buffer.len() < CB_OBJ_TAG_MIN {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
buffer[0] = tag;
|
||||||
|
|
||||||
|
let offset = 1 + set_length(&mut buffer[1..], length)?;
|
||||||
|
|
||||||
|
if buffer.len() < offset + length {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
value(&mut buffer[offset..offset + length]);
|
||||||
|
|
||||||
|
Ok(offset + length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set length
|
/// Set length
|
||||||
pub(crate) fn set_length(buffer: &mut [u8], length: usize) -> usize {
|
pub(crate) fn set_length(buffer: &mut [u8], length: usize) -> Result<usize, Error> {
|
||||||
if length < 0x80 {
|
if length < 0x80 {
|
||||||
|
if buffer.is_empty() {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
} else {
|
||||||
buffer[0] = length as u8;
|
buffer[0] = length as u8;
|
||||||
1
|
Ok(1)
|
||||||
|
}
|
||||||
} else if length < 0x100 {
|
} else if length < 0x100 {
|
||||||
|
if buffer.len() < 2 {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
} else {
|
||||||
buffer[0] = 0x81;
|
buffer[0] = 0x81;
|
||||||
buffer[1] = length as u8;
|
buffer[1] = length as u8;
|
||||||
2
|
Ok(2)
|
||||||
|
}
|
||||||
|
} else if buffer.len() < 3 {
|
||||||
|
Err(Error::SizeError)
|
||||||
} else {
|
} else {
|
||||||
buffer[0] = 0x82;
|
buffer[0] = 0x82;
|
||||||
buffer[1] = ((length >> 8) & 0xff) as u8;
|
buffer[1] = ((length >> 8) & 0xff) as u8;
|
||||||
buffer[2] = (length & 0xff) as u8;
|
buffer[2] = (length & 0xff) as u8;
|
||||||
3
|
Ok(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse length tag, returning the size of the length tag itself as the
|
/// Parse length tag, returning the size of the length tag itself as the
|
||||||
/// returned value, and setting the len parameter to the parsed length.
|
/// returned value, and setting the len parameter to the parsed length.
|
||||||
pub(crate) fn get_length(buffer: &[u8], len: &mut usize) -> usize {
|
pub(crate) fn get_length(buffer: &[u8], len: &mut usize) -> usize {
|
||||||
|
// This is not valid ASN.1 (0x80 is the indefinite length marker).
|
||||||
|
// See comment in key::generate for more context.
|
||||||
if buffer[0] < 0x81 {
|
if buffer[0] < 0x81 {
|
||||||
*len = buffer[0] as usize;
|
*len = buffer[0] as usize;
|
||||||
1
|
1
|
||||||
@@ -83,9 +184,9 @@ pub(crate) fn has_valid_length(buffer: &[u8], len: usize) -> bool {
|
|||||||
pub(crate) fn set_object(object_id: ObjectId, mut buffer: &mut [u8]) -> &mut [u8] {
|
pub(crate) fn set_object(object_id: ObjectId, mut buffer: &mut [u8]) -> &mut [u8] {
|
||||||
buffer[0] = 0x5c;
|
buffer[0] = 0x5c;
|
||||||
|
|
||||||
if object_id == YKPIV_OBJ_DISCOVERY {
|
if object_id == OBJ_DISCOVERY {
|
||||||
buffer[1] = 1;
|
buffer[1] = 1;
|
||||||
buffer[2] = YKPIV_OBJ_DISCOVERY as u8;
|
buffer[2] = OBJ_DISCOVERY as u8;
|
||||||
buffer = &mut buffer[3..];
|
buffer = &mut buffer[3..];
|
||||||
} else if object_id > 0xffff && object_id <= 0x00ff_ffff {
|
} else if object_id > 0xffff && object_id <= 0x00ff_ffff {
|
||||||
buffer[1] = 3;
|
buffer[1] = 3;
|
||||||
|
|||||||
+126
-176
@@ -1,41 +1,43 @@
|
|||||||
//! YubiKey PC/SC transactions
|
//! YubiKey PC/SC transactions
|
||||||
|
|
||||||
use crate::{apdu::APDU, consts::*, error::Error, yubikey::*, Buffer};
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
use crate::{
|
use crate::{
|
||||||
mgm::MgmKey,
|
apdu::Response,
|
||||||
response::{Response, StatusWords},
|
apdu::{Ins, StatusWords, APDU},
|
||||||
|
error::Error,
|
||||||
|
key::{AlgorithmId, SlotId},
|
||||||
serialization::*,
|
serialization::*,
|
||||||
ObjectId,
|
yubikey::*,
|
||||||
|
Buffer, ObjectId, CB_BUF_MAX, CB_OBJ_MAX, PIV_AID, YK_AID,
|
||||||
};
|
};
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
use std::ptr;
|
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::mgm::{MgmKey, DES_LEN_3DES};
|
||||||
|
|
||||||
|
const CB_PIN_MAX: usize = 8;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub(crate) enum ChangeRefAction {
|
||||||
|
ChangePin,
|
||||||
|
ChangePuk,
|
||||||
|
UnblockPin,
|
||||||
|
}
|
||||||
|
|
||||||
/// Exclusive transaction with the YubiKey's PC/SC card.
|
/// Exclusive transaction with the YubiKey's PC/SC card.
|
||||||
pub(crate) struct Transaction<'tx> {
|
pub(crate) struct Transaction<'tx> {
|
||||||
inner: pcsc::Transaction<'tx>,
|
inner: pcsc::Transaction<'tx>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'tx> Transaction<'tx> {
|
impl<'tx> Transaction<'tx> {
|
||||||
/// Create a new transaction with the given card
|
/// Create a new transaction with the given card.
|
||||||
pub fn new(card: &'tx mut pcsc::Card) -> Result<Self, Error> {
|
pub fn new(card: &'tx mut pcsc::Card) -> Result<Self, Error> {
|
||||||
Ok(Transaction {
|
Ok(Transaction {
|
||||||
inner: card.transaction()?,
|
inner: card.transaction()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an attribute of the card or card reader.
|
|
||||||
pub fn get_attribute<'buf>(
|
|
||||||
&self,
|
|
||||||
attribute: pcsc::Attribute,
|
|
||||||
buffer: &'buf mut [u8],
|
|
||||||
) -> Result<&'buf [u8], Error> {
|
|
||||||
Ok(self.inner.get_attribute(attribute, buffer)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transmit a single serialized APDU to the card this transaction is open
|
/// Transmit a single serialized APDU to the card this transaction is open
|
||||||
/// with and receive a response.
|
/// with and receive a response.
|
||||||
///
|
///
|
||||||
@@ -43,10 +45,10 @@ impl<'tx> Transaction<'tx> {
|
|||||||
/// single APDU messages at a time. For larger messages that need to be
|
/// single APDU messages at a time. For larger messages that need to be
|
||||||
/// split into multiple APDUs, use the [`Transaction::transfer_data`]
|
/// split into multiple APDUs, use the [`Transaction::transfer_data`]
|
||||||
/// method instead.
|
/// method instead.
|
||||||
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Buffer, Error> {
|
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Vec<u8>, Error> {
|
||||||
trace!(">>> {:?}", send_buffer);
|
trace!(">>> {:?}", send_buffer);
|
||||||
|
|
||||||
let mut recv_buffer = Zeroizing::new(vec![0u8; recv_len]);
|
let mut recv_buffer = vec![0u8; recv_len];
|
||||||
|
|
||||||
let len = self
|
let len = self
|
||||||
.inner
|
.inner
|
||||||
@@ -54,17 +56,14 @@ impl<'tx> Transaction<'tx> {
|
|||||||
.len();
|
.len();
|
||||||
|
|
||||||
recv_buffer.truncate(len);
|
recv_buffer.truncate(len);
|
||||||
|
|
||||||
trace!("<<< {:?}", recv_buffer.as_slice());
|
|
||||||
|
|
||||||
Ok(recv_buffer)
|
Ok(recv_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select application.
|
/// Select application.
|
||||||
pub fn select_application(&self) -> Result<(), Error> {
|
pub fn select_application(&self) -> Result<(), Error> {
|
||||||
let response = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
let response = APDU::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(&AID)
|
.data(&PIV_AID)
|
||||||
.transmit(self, 0xFF)
|
.transmit(self, 0xFF)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("failed communicating with card: '{}'", e);
|
error!("failed communicating with card: '{}'", e);
|
||||||
@@ -82,35 +81,29 @@ impl<'tx> Transaction<'tx> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the version of the PIV application installed on the YubiKey
|
/// Get the version of the PIV application installed on the YubiKey.
|
||||||
pub fn get_version(&self) -> Result<Version, Error> {
|
pub fn get_version(&self) -> Result<Version, Error> {
|
||||||
// get version from device
|
// get version from device
|
||||||
let response = APDU::new(YKPIV_INS_GET_VERSION).transmit(self, 261)?;
|
let response = APDU::new(Ins::GetVersion).transmit(self, 261)?;
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.buffer().len() < 3 {
|
if response.data().len() < 3 {
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Version {
|
Ok(Version::new(response.data()[..3].try_into().unwrap()))
|
||||||
major: response.buffer()[0],
|
|
||||||
minor: response.buffer()[1],
|
|
||||||
patch: response.buffer()[2],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YubiKey device serial number
|
/// Get YubiKey device serial number.
|
||||||
pub fn get_serial(&self, version: Version) -> Result<Serial, Error> {
|
pub fn get_serial(&self, version: Version) -> Result<Serial, Error> {
|
||||||
let yk_applet = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
|
|
||||||
|
|
||||||
let response = if version.major < 5 {
|
let response = if version.major < 5 {
|
||||||
// get serial from neo/yk4 devices using the otp applet
|
// YK4 requires switching to the yk applet to retrieve the serial
|
||||||
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
let sw = APDU::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(&yk_applet)
|
.data(&YK_AID)
|
||||||
.transmit(self, 0xFF)?
|
.transmit(self, 0xFF)?
|
||||||
.status_words();
|
.status_words();
|
||||||
|
|
||||||
@@ -130,9 +123,9 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reselect the PIV applet
|
// reselect the PIV applet
|
||||||
let sw = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
let sw = APDU::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(&AID)
|
.data(&PIV_AID)
|
||||||
.transmit(self, 0xFF)?
|
.transmit(self, 0xFF)?
|
||||||
.status_words();
|
.status_words();
|
||||||
|
|
||||||
@@ -143,8 +136,8 @@ impl<'tx> Transaction<'tx> {
|
|||||||
|
|
||||||
resp
|
resp
|
||||||
} else {
|
} else {
|
||||||
// get serial from yk5 and later devices using the f8 command
|
// YK5 implements getting the serial as a PIV applet command (0xf8)
|
||||||
let resp = APDU::new(YKPIV_INS_GET_SERIAL).transmit(self, 0xFF)?;
|
let resp = APDU::new(Ins::GetSerial).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
if !resp.is_success() {
|
if !resp.is_success() {
|
||||||
error!(
|
error!(
|
||||||
@@ -157,74 +150,64 @@ impl<'tx> Transaction<'tx> {
|
|||||||
resp
|
resp
|
||||||
};
|
};
|
||||||
|
|
||||||
response.buffer()[..4]
|
response.data()[..4]
|
||||||
.try_into()
|
.try_into()
|
||||||
.map(|serial| Serial::from(u32::from_be_bytes(serial)))
|
.map(|serial| Serial::from(u32::from_be_bytes(serial)))
|
||||||
.map_err(|_| Error::SizeError)
|
.map_err(|_| Error::SizeError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify device PIN.
|
/// Verify device PIN.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> {
|
pub fn verify_pin(&self, pin: &[u8]) -> Result<(), Error> {
|
||||||
// TODO(tarcieri): allow unpadded (with `0xFF`) PIN shorter than CB_PIN_MAX?
|
if pin.len() > CB_PIN_MAX {
|
||||||
if pin.len() != CB_PIN_MAX {
|
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = APDU::new(YKPIV_INS_VERIFY)
|
let mut query = APDU::new(Ins::Verify);
|
||||||
.params(0x00, 0x80)
|
query.params(0x00, 0x80);
|
||||||
.data(pin)
|
|
||||||
.transmit(self, 261)?;
|
// Empty pin means we are querying the number of retries. We set no data in this
|
||||||
|
// case; if we instead sent [0xff; CB_PIN_MAX] it would count as an attempt and
|
||||||
|
// decrease the retry counter.
|
||||||
|
if !pin.is_empty() {
|
||||||
|
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
|
||||||
|
data[0..pin.len()].copy_from_slice(pin);
|
||||||
|
query.data(data.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = query.transmit(self, 261)?;
|
||||||
|
|
||||||
match response.status_words() {
|
match response.status_words() {
|
||||||
StatusWords::Success => Ok(()),
|
StatusWords::Success => Ok(()),
|
||||||
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
|
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
|
||||||
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }),
|
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
|
||||||
_ => Err(Error::GenericError),
|
_ => Err(Error::GenericError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the PIN
|
/// Change the PIN.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn change_pin(&self, action: i32, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
pub fn change_ref(
|
||||||
let mut templ = [0, YKPIV_INS_CHANGE_REFERENCE, 0, 0x80];
|
&self,
|
||||||
let mut indata = Zeroizing::new([0u8; 16]);
|
action: ChangeRefAction,
|
||||||
|
current_pin: &[u8],
|
||||||
|
new_pin: &[u8],
|
||||||
|
) -> Result<(), Error> {
|
||||||
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
|
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == CHREF_ACT_UNBLOCK_PIN {
|
const PIN: u8 = 0x80;
|
||||||
templ[1] = YKPIV_INS_RESET_RETRY;
|
const PUK: u8 = 0x81;
|
||||||
} else if action == CHREF_ACT_CHANGE_PUK {
|
|
||||||
templ[3] = 0x81;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
let templ = match action {
|
||||||
ptr::copy(current_pin.as_ptr(), indata.as_mut_ptr(), current_pin.len());
|
ChangeRefAction::ChangePin => [0, Ins::ChangeReference.code(), 0, PIN],
|
||||||
|
ChangeRefAction::ChangePuk => [0, Ins::ChangeReference.code(), 0, PUK],
|
||||||
|
ChangeRefAction::UnblockPin => [0, Ins::ResetRetry.code(), 0, PIN],
|
||||||
|
};
|
||||||
|
|
||||||
if current_pin.len() < CB_PIN_MAX {
|
let mut indata = Zeroizing::new([0xff; CB_PIN_MAX * 2]);
|
||||||
ptr::write_bytes(
|
indata[0..current_pin.len()].copy_from_slice(current_pin);
|
||||||
indata.as_mut_ptr().add(current_pin.len()),
|
indata[CB_PIN_MAX..CB_PIN_MAX + new_pin.len()].copy_from_slice(new_pin);
|
||||||
0xff,
|
|
||||||
CB_PIN_MAX - current_pin.len(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ptr::copy(
|
|
||||||
new_pin.as_ptr(),
|
|
||||||
indata.as_mut_ptr().offset(8),
|
|
||||||
new_pin.len(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if new_pin.len() < CB_PIN_MAX {
|
|
||||||
ptr::write_bytes(
|
|
||||||
indata.as_mut_ptr().offset(8).add(new_pin.len()),
|
|
||||||
0xff,
|
|
||||||
CB_PIN_MAX - new_pin.len(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let status_words = self
|
let status_words = self
|
||||||
.transfer_data(&templ, indata.as_ref(), 0xFF)?
|
.transfer_data(&templ, indata.as_ref(), 0xFF)?
|
||||||
@@ -233,7 +216,7 @@ impl<'tx> Transaction<'tx> {
|
|||||||
match status_words {
|
match status_words {
|
||||||
StatusWords::Success => Ok(()),
|
StatusWords::Success => Ok(()),
|
||||||
StatusWords::AuthBlockedError => Err(Error::PinLocked),
|
StatusWords::AuthBlockedError => Err(Error::PinLocked),
|
||||||
StatusWords::Other(sw) if sw >> 8 == 0x63 => Err(Error::WrongPin { tries: sw & 0xf }),
|
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
|
||||||
_ => {
|
_ => {
|
||||||
error!(
|
error!(
|
||||||
"failed changing pin, token response code: {:x}.",
|
"failed changing pin, token response code: {:x}.",
|
||||||
@@ -256,12 +239,12 @@ impl<'tx> Transaction<'tx> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut data = [0u8; DES_LEN_3DES + 3];
|
let mut data = [0u8; DES_LEN_3DES + 3];
|
||||||
data[0] = YKPIV_ALGO_3DES;
|
data[0] = ALGO_3DES;
|
||||||
data[1] = YKPIV_KEY_CARDMGM;
|
data[1] = KEY_CARDMGM;
|
||||||
data[2] = DES_LEN_3DES as u8;
|
data[2] = DES_LEN_3DES as u8;
|
||||||
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
|
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
|
||||||
|
|
||||||
let status_words = APDU::new(YKPIV_INS_SET_MGMKEY)
|
let status_words = APDU::new(Ins::SetMgmKey)
|
||||||
.params(0xff, p2)
|
.params(0xff, p2)
|
||||||
.data(&data)
|
.data(&data)
|
||||||
.transmit(self, 261)?
|
.transmit(self, 261)?
|
||||||
@@ -279,25 +262,21 @@ impl<'tx> Transaction<'tx> {
|
|||||||
/// This is the common backend for all public key encryption and signing
|
/// This is the common backend for all public key encryption and signing
|
||||||
/// operations.
|
/// operations.
|
||||||
// TODO(tarcieri): refactor this to be less gross/coupled.
|
// TODO(tarcieri): refactor this to be less gross/coupled.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn authenticated_command(
|
pub(crate) fn authenticated_command(
|
||||||
&self,
|
&self,
|
||||||
sign_in: &[u8],
|
sign_in: &[u8],
|
||||||
out: &mut [u8],
|
algorithm: AlgorithmId,
|
||||||
out_len: &mut usize,
|
key: SlotId,
|
||||||
algorithm: u8,
|
|
||||||
key: u8,
|
|
||||||
decipher: bool,
|
decipher: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Buffer, Error> {
|
||||||
let in_len = sign_in.len();
|
let in_len = sign_in.len();
|
||||||
let mut indata = [0u8; 1024];
|
let mut indata = [0u8; 1024];
|
||||||
let templ = [0, YKPIV_INS_AUTHENTICATE, algorithm, key];
|
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
|
||||||
let mut len: usize = 0;
|
|
||||||
|
|
||||||
match algorithm {
|
match algorithm {
|
||||||
YKPIV_ALGO_RSA1024 | YKPIV_ALGO_RSA2048 => {
|
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
||||||
let key_len = if algorithm == YKPIV_ALGO_RSA1024 {
|
let key_len = if let AlgorithmId::Rsa1024 = algorithm {
|
||||||
128
|
128
|
||||||
} else {
|
} else {
|
||||||
256
|
256
|
||||||
@@ -307,8 +286,8 @@ impl<'tx> Transaction<'tx> {
|
|||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
YKPIV_ALGO_ECCP256 | YKPIV_ALGO_ECCP384 => {
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||||
let key_len = if algorithm == YKPIV_ALGO_ECCP256 {
|
let key_len = if let AlgorithmId::EccP256 = algorithm {
|
||||||
32
|
32
|
||||||
} else {
|
} else {
|
||||||
48
|
48
|
||||||
@@ -319,7 +298,6 @@ impl<'tx> Transaction<'tx> {
|
|||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(Error::AlgorithmError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes = if in_len < 0x80 {
|
let bytes = if in_len < 0x80 {
|
||||||
@@ -330,21 +308,21 @@ impl<'tx> Transaction<'tx> {
|
|||||||
3
|
3
|
||||||
};
|
};
|
||||||
|
|
||||||
indata[0] = 0x7c;
|
let offset = Tlv::write_as(&mut indata, 0x7c, in_len + bytes + 3, |buf| {
|
||||||
let mut offset = 1 + set_length(&mut indata[1..], in_len + bytes + 3);
|
assert_eq!(Tlv::write(buf, 0x82, &[]).expect("large enough"), 2);
|
||||||
indata[offset] = 0x82;
|
assert_eq!(
|
||||||
indata[offset + 1] = 0x00;
|
Tlv::write(
|
||||||
indata[offset + 2] =
|
&mut buf[2..],
|
||||||
if (algorithm == YKPIV_ALGO_ECCP256 || algorithm == YKPIV_ALGO_ECCP384) && decipher {
|
match (algorithm, decipher) {
|
||||||
0x85
|
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
|
||||||
} else {
|
_ => 0x81,
|
||||||
0x81
|
},
|
||||||
};
|
sign_in
|
||||||
|
)
|
||||||
offset += 3;
|
.expect("large enough"),
|
||||||
offset += set_length(&mut indata[offset..], in_len);
|
1 + bytes + in_len
|
||||||
indata[offset..(offset + in_len)].copy_from_slice(sign_in);
|
);
|
||||||
offset += in_len;
|
})?;
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.transfer_data(&templ, &indata[..offset], 1024)
|
.transfer_data(&templ, &indata[..offset], 1024)
|
||||||
@@ -363,40 +341,29 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = response.buffer();
|
let (_, outer_tlv) = Tlv::parse(response.data())?;
|
||||||
|
|
||||||
// skip the first 7c tag
|
// skip the first 7c tag
|
||||||
if data[0] != 0x7c {
|
if outer_tlv.tag != 0x7c {
|
||||||
error!("failed parsing signature reply (0x7c byte)");
|
error!("failed parsing signature reply (0x7c byte)");
|
||||||
return Err(Error::ParseError);
|
return Err(Error::ParseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut offset = 1 + get_length(&data[1..], &mut len);
|
let (_, inner_tlv) = Tlv::parse(outer_tlv.value)?;
|
||||||
|
|
||||||
// skip the 82 tag
|
// skip the 82 tag
|
||||||
if data[offset] != 0x82 {
|
if inner_tlv.tag != 0x82 {
|
||||||
error!("failed parsing signature reply (0x82 byte)");
|
error!("failed parsing signature reply (0x82 byte)");
|
||||||
return Err(Error::ParseError);
|
return Err(Error::ParseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += 1;
|
Ok(Buffer::new(inner_tlv.value.into()))
|
||||||
offset += get_length(&data[offset..], &mut len);
|
|
||||||
|
|
||||||
if len > *out_len {
|
|
||||||
error!("wrong size on output buffer");
|
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
*out_len = len;
|
|
||||||
out[..len].copy_from_slice(&data[offset..(offset + len)]);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send/receive large amounts of data to/from the YubiKey, splitting long
|
/// Send/receive large amounts of data to/from the YubiKey, splitting long
|
||||||
/// messages into smaller APDU-sized messages (using the provided APDU
|
/// messages into smaller APDU-sized messages (using the provided APDU
|
||||||
/// template to construct them), and then sending those via
|
/// template to construct them), and then sending those via
|
||||||
/// [`Transaction::transmit`].
|
/// [`Transaction::transmit`].
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn transfer_data(
|
pub fn transfer_data(
|
||||||
&self,
|
&self,
|
||||||
templ: &[u8],
|
templ: &[u8],
|
||||||
@@ -404,8 +371,8 @@ impl<'tx> Transaction<'tx> {
|
|||||||
max_out: usize,
|
max_out: usize,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
let mut in_offset = 0;
|
let mut in_offset = 0;
|
||||||
let mut out_data = Zeroizing::new(vec![]);
|
let mut out_data = vec![];
|
||||||
let mut sw = 0;
|
let mut sw;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut this_size = 0xff;
|
let mut this_size = 0xff;
|
||||||
@@ -425,24 +392,24 @@ impl<'tx> Transaction<'tx> {
|
|||||||
.data(&in_data[in_offset..(in_offset + this_size)])
|
.data(&in_data[in_offset..(in_offset + this_size)])
|
||||||
.transmit(self, 261)?;
|
.transmit(self, 261)?;
|
||||||
|
|
||||||
if !response.is_success() && (response.status_words().code() >> 8 != 0x61) {
|
sw = response.status_words().code();
|
||||||
|
|
||||||
|
if !response.is_success() && (sw >> 8 != 0x61) {
|
||||||
// TODO(tarcieri): is this really OK?
|
// TODO(tarcieri): is this really OK?
|
||||||
return Ok(Response::new(sw.into(), out_data));
|
return Ok(Response::new(sw.into(), out_data));
|
||||||
}
|
}
|
||||||
|
|
||||||
sw = response.status_words().code();
|
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
|
||||||
|
|
||||||
if out_data.len() - response.buffer().len() - 2 > max_out {
|
|
||||||
error!(
|
error!(
|
||||||
"output buffer too small: wanted to write {}, max was {}",
|
"output buffer too small: wanted to write {}, max was {}",
|
||||||
out_data.len() - response.buffer().len() - 2,
|
out_data.len() - response.data().len(),
|
||||||
max_out
|
max_out
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]);
|
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
||||||
|
|
||||||
in_offset += this_size;
|
in_offset += this_size;
|
||||||
if in_offset >= in_data.len() {
|
if in_offset >= in_data.len() {
|
||||||
@@ -456,34 +423,33 @@ impl<'tx> Transaction<'tx> {
|
|||||||
sw & 0xff
|
sw & 0xff
|
||||||
);
|
);
|
||||||
|
|
||||||
let response = APDU::new(YKPIV_INS_GET_RESPONSE_APDU).transmit(self, 261)?;
|
let response = APDU::new(Ins::GetResponseApdu).transmit(self, 261)?;
|
||||||
sw = response.status_words().code();
|
sw = response.status_words().code();
|
||||||
|
|
||||||
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
|
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
|
||||||
return Ok(Response::new(sw.into(), Zeroizing::new(vec![])));
|
return Ok(Response::new(sw.into(), vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if out_data.len() + response.buffer().len() - 2 > max_out {
|
if out_data.len() + response.data().len() > max_out {
|
||||||
error!(
|
error!(
|
||||||
"output buffer too small: wanted to write {}, max was {}",
|
"output buffer too small: wanted to write {}, max was {}",
|
||||||
out_data.len() + response.buffer().len() - 2,
|
out_data.len() + response.data().len(),
|
||||||
max_out
|
max_out
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
out_data.extend_from_slice(&response.buffer()[..response.buffer().len() - 2]);
|
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Response::new(sw.into(), out_data))
|
Ok(Response::new(sw.into(), out_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch an object
|
/// Fetch an object.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
|
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer, Error> {
|
||||||
let mut indata = [0u8; 5];
|
let mut indata = [0u8; 5];
|
||||||
let templ = [0, YKPIV_INS_GET_DATA, 0x3f, 0xff];
|
let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
|
||||||
|
|
||||||
let mut inlen = indata.len();
|
let mut inlen = indata.len();
|
||||||
let indata_remaining = set_object(object_id, &mut indata);
|
let indata_remaining = set_object(object_id, &mut indata);
|
||||||
@@ -492,41 +458,31 @@ impl<'tx> Transaction<'tx> {
|
|||||||
let response = self.transfer_data(&templ, &indata[..inlen], CB_BUF_MAX)?;
|
let response = self.transfer_data(&templ, &indata[..inlen], CB_BUF_MAX)?;
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
|
if response.status_words() == StatusWords::NotFoundError {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
} else {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = response.into_buffer();
|
|
||||||
let mut outlen = 0;
|
|
||||||
|
|
||||||
if data.len() < 2 || !has_valid_length(&data[1..], data.len() - 1) {
|
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let offs = get_length(&data[1..], &mut outlen);
|
let (remaining, tlv) = Tlv::parse(response.data())?;
|
||||||
|
|
||||||
if offs == 0 {
|
if !remaining.is_empty() {
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if outlen + offs + 1 != data.len() {
|
|
||||||
error!(
|
error!(
|
||||||
"invalid length indicated in object: total len is {} but indicated length is {}",
|
"invalid length indicated in object: total len is {} but indicated length is {}",
|
||||||
data.len(),
|
tlv.value.len() + remaining.len(),
|
||||||
outlen
|
tlv.value.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Zeroizing::new(
|
Ok(Zeroizing::new(tlv.value.to_vec()))
|
||||||
data[(1 + offs)..(1 + offs + outlen)].to_vec(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save an object
|
/// Save an object.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
|
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<(), Error> {
|
||||||
let templ = [0, YKPIV_INS_PUT_DATA, 0x3f, 0xff];
|
let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
|
||||||
|
|
||||||
// TODO(tarcieri): replace with vector
|
// TODO(tarcieri): replace with vector
|
||||||
let mut data = [0u8; CB_BUF_MAX];
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
@@ -538,14 +494,8 @@ impl<'tx> Transaction<'tx> {
|
|||||||
let mut len = data.len();
|
let mut len = data.len();
|
||||||
let mut data_remaining = set_object(object_id, &mut data);
|
let mut data_remaining = set_object(object_id, &mut data);
|
||||||
|
|
||||||
data_remaining[0] = 0x53;
|
let offset = Tlv::write(data_remaining, 0x53, indata)?;
|
||||||
data_remaining = &mut data_remaining[1..];
|
|
||||||
|
|
||||||
let offset = set_length(data_remaining, indata.len());
|
|
||||||
data_remaining = &mut data_remaining[offset..];
|
data_remaining = &mut data_remaining[offset..];
|
||||||
data_remaining[..indata.len()].copy_from_slice(indata);
|
|
||||||
|
|
||||||
data_remaining = &mut data_remaining[indata.len()..];
|
|
||||||
len -= data_remaining.len();
|
len -= data_remaining.len();
|
||||||
|
|
||||||
let status_words = self
|
let status_words = self
|
||||||
|
|||||||
+181
-416
@@ -30,35 +30,48 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
#![allow(non_snake_case, non_upper_case_globals)]
|
use crate::{
|
||||||
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
|
apdu::{Ins, APDU},
|
||||||
|
cccid::CCC,
|
||||||
|
chuid::CHUID,
|
||||||
|
config::Config,
|
||||||
|
error::Error,
|
||||||
|
mgm::MgmKey,
|
||||||
|
readers::{Reader, Readers},
|
||||||
|
transaction::Transaction,
|
||||||
|
};
|
||||||
|
use log::{error, info};
|
||||||
|
use pcsc::Card;
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
fmt::{self, Display},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use crate::{
|
use crate::{
|
||||||
apdu::APDU, key::SlotId, metadata, mgm::MgmKey, response::StatusWords, serialization::*,
|
apdu::StatusWords, metadata, transaction::ChangeRefAction, Buffer, ObjectId, CB_BUF_MAX,
|
||||||
ObjectId,
|
CB_OBJ_MAX, MGMT_AID, TAG_ADMIN, TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP,
|
||||||
};
|
};
|
||||||
use crate::{consts::*, error::Error, transaction::Transaction, Buffer};
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
use log::{error, info, warn};
|
|
||||||
use pcsc::{Card, Context};
|
|
||||||
use std::fmt::{self, Display};
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use std::{
|
use secrecy::ExposeSecret;
|
||||||
convert::TryInto,
|
|
||||||
ptr, slice,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use zeroize::Zeroizing;
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// PIV Application ID
|
/// Flag for PUK blocked
|
||||||
pub const AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
|
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
|
||||||
|
|
||||||
/// MGMT Application ID.
|
/// 3DES authentication
|
||||||
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
|
pub(crate) const ALGO_3DES: u8 = 0x03;
|
||||||
pub const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
|
|
||||||
|
/// Card management key
|
||||||
|
pub(crate) const KEY_CARDMGM: u8 = 0x9b;
|
||||||
|
|
||||||
|
const TAG_DYN_AUTH: u8 = 0x7c;
|
||||||
|
|
||||||
|
/// Cached YubiKey PIN
|
||||||
|
pub type CachedPin = secrecy::SecretVec<u8>;
|
||||||
|
|
||||||
/// YubiKey Serial Number
|
/// YubiKey Serial Number
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
@@ -76,6 +89,14 @@ impl From<Serial> for u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for Serial {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Error> {
|
||||||
|
u32::from_str(s).map(Serial).map_err(|_| Error::ParseError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Serial {
|
impl Display for Serial {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
@@ -95,6 +116,23 @@ pub struct Version {
|
|||||||
pub patch: u8,
|
pub patch: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
/// Parse a version from bytes
|
||||||
|
pub fn new(bytes: [u8; 3]) -> Version {
|
||||||
|
Version {
|
||||||
|
major: bytes[0],
|
||||||
|
minor: bytes[1],
|
||||||
|
patch: bytes[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Version {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// YubiKey Device: this is the primary API for opening a session and
|
/// YubiKey Device: this is the primary API for opening a session and
|
||||||
/// performing various operations.
|
/// performing various operations.
|
||||||
///
|
///
|
||||||
@@ -104,103 +142,64 @@ pub struct Version {
|
|||||||
#[cfg_attr(not(feature = "untested"), allow(dead_code))]
|
#[cfg_attr(not(feature = "untested"), allow(dead_code))]
|
||||||
pub struct YubiKey {
|
pub struct YubiKey {
|
||||||
pub(crate) card: Card,
|
pub(crate) card: Card,
|
||||||
pub(crate) pin: Option<Buffer>,
|
pub(crate) name: String,
|
||||||
pub(crate) is_neo: bool,
|
pub(crate) pin: Option<CachedPin>,
|
||||||
pub(crate) version: Version,
|
pub(crate) version: Version,
|
||||||
pub(crate) serial: Serial,
|
pub(crate) serial: Serial,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl YubiKey {
|
impl YubiKey {
|
||||||
/// Open a connection to a YubiKey, optionally giving the name
|
/// Open a connection to a YubiKey.
|
||||||
/// (needed if e.g. there are multiple YubiKeys connected).
|
///
|
||||||
pub fn open(name: Option<&[u8]>) -> Result<YubiKey, Error> {
|
/// Returns an error if there is more than one YubiKey detected.
|
||||||
let context = Context::establish(pcsc::Scope::System)?;
|
///
|
||||||
let mut card = Self::connect(&context, name)?;
|
/// If you need to operate in environments with more than one YubiKey
|
||||||
|
/// attached to the same system, use [`YubiKey::open_by_serial`] or
|
||||||
|
///[`yubikey_piv::Readers`] to select from the available PC/SC readers.
|
||||||
|
pub fn open() -> Result<Self, Error> {
|
||||||
|
let mut readers = Readers::open().map_err(|e| match e {
|
||||||
|
Error::PcscError {
|
||||||
|
inner: Some(pcsc::Error::NoReadersAvailable),
|
||||||
|
} => Error::NotFound,
|
||||||
|
other => other,
|
||||||
|
})?;
|
||||||
|
let mut reader_iter = readers.iter()?;
|
||||||
|
|
||||||
let mut is_neo = false;
|
if let Some(reader) = reader_iter.next() {
|
||||||
let version: Version;
|
if reader_iter.next().is_some() {
|
||||||
let serial: Serial;
|
error!("multiple YubiKeys detected!");
|
||||||
|
return Err(Error::PcscError { inner: None });
|
||||||
{
|
|
||||||
let txn = Transaction::new(&mut card)?;
|
|
||||||
let mut atr_buf = [0; CB_ATR_MAX];
|
|
||||||
let atr = txn.get_attribute(pcsc::Attribute::AtrString, &mut atr_buf)?;
|
|
||||||
if atr == YKPIV_ATR_NEO_R3 {
|
|
||||||
is_neo = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.select_application()?;
|
return reader.open();
|
||||||
|
|
||||||
// now that the PIV application is selected, retrieve the version
|
|
||||||
// and serial number. Previously the NEO/YK4 required switching
|
|
||||||
// to the yk applet to retrieve the serial, YK5 implements this
|
|
||||||
// as a PIV applet command. Unfortunately, this change requires
|
|
||||||
// that we retrieve the version number first, so that get_serial
|
|
||||||
// can determine how to get the serial number, which for the NEO/Yk4
|
|
||||||
// will result in another selection of the PIV applet.
|
|
||||||
|
|
||||||
version = txn.get_version().map_err(|e| {
|
|
||||||
warn!("failed to retrieve version: '{}'", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
serial = txn.get_serial(version).map_err(|e| {
|
|
||||||
warn!("failed to retrieve serial number: '{}'", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey = YubiKey {
|
error!("no YubiKey detected!");
|
||||||
card,
|
Err(Error::NotFound)
|
||||||
pin: None,
|
}
|
||||||
is_neo,
|
|
||||||
version,
|
/// Open a YubiKey with a specific serial number.
|
||||||
serial,
|
pub fn open_by_serial(serial: Serial) -> Result<Self, Error> {
|
||||||
|
let mut readers = Readers::open().map_err(|e| match e {
|
||||||
|
Error::PcscError {
|
||||||
|
inner: Some(pcsc::Error::NoReadersAvailable),
|
||||||
|
} => Error::NotFound,
|
||||||
|
other => other,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for reader in readers.iter()? {
|
||||||
|
let yubikey = match reader.open() {
|
||||||
|
Ok(yk) => yk,
|
||||||
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(yubikey)
|
if serial == yubikey.serial() {
|
||||||
}
|
return 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());
|
error!("no YubiKey detected with serial: {}", serial);
|
||||||
|
Err(Error::NotFound)
|
||||||
match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::T1) {
|
|
||||||
Ok(card) => {
|
|
||||||
info!("connected to '{}' successfully", reader.to_string_lossy());
|
|
||||||
return Ok(card);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"skipping '{}' due to connection error: {}",
|
|
||||||
reader.to_string_lossy(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error!("error: no usable reader found");
|
|
||||||
Err(Error::PcscError { inner: None })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconnect to a YubiKey
|
/// Reconnect to a YubiKey
|
||||||
@@ -214,8 +213,10 @@ impl YubiKey {
|
|||||||
pcsc::Disposition::ResetCard,
|
pcsc::Disposition::ResetCard,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// TODO(tarcieri): zeroize pin!
|
let pin = self
|
||||||
let pin = self.pin.clone();
|
.pin
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| Buffer::new(p.expose_secret().clone()));
|
||||||
|
|
||||||
let txn = Transaction::new(&mut self.card)?;
|
let txn = Transaction::new(&mut self.card)?;
|
||||||
txn.select_application()?;
|
txn.select_application()?;
|
||||||
@@ -228,58 +229,64 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Begin a transaction.
|
/// Begin a transaction.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>, Error> {
|
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>, Error> {
|
||||||
// TODO(tarcieri): reconnect support
|
// TODO(tarcieri): reconnect support
|
||||||
Ok(Transaction::new(&mut self.card)?)
|
Ok(Transaction::new(&mut self.card)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the name of the associated PC/SC card reader
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the YubiKey's PIV application version.
|
/// Get the YubiKey's PIV application version.
|
||||||
///
|
///
|
||||||
/// This always uses the cached version queried when the key is initialized.
|
/// This always uses the cached version queried when the key is initialized.
|
||||||
pub fn version(&mut self) -> Version {
|
pub fn version(&self) -> Version {
|
||||||
self.version
|
self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YubiKey device serial number.
|
/// Get YubiKey device serial number.
|
||||||
///
|
///
|
||||||
/// This always uses the cached version queried when the key is initialized.
|
/// This always uses the cached version queried when the key is initialized.
|
||||||
pub fn serial(&mut self) -> Serial {
|
pub fn serial(&self) -> Serial {
|
||||||
self.serial
|
self.serial
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YubiKey device model
|
/// Get device configuration.
|
||||||
// TODO(tarcieri): use an emum for this
|
pub fn config(&mut self) -> Result<Config, Error> {
|
||||||
#[cfg(feature = "untested")]
|
Config::get(self)
|
||||||
pub fn device_model(&self) -> u32 {
|
|
||||||
if self.is_neo {
|
|
||||||
DEVTYPE_NEOr3
|
|
||||||
} else {
|
|
||||||
// TODO(tarcieri): YK5?
|
|
||||||
DEVTYPE_YK4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get CHUID
|
||||||
|
pub fn chuid(&mut self) -> Result<CHUID, Error> {
|
||||||
|
CHUID::get(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get CCCID
|
||||||
|
pub fn cccid(&mut self) -> Result<CCC, Error> {
|
||||||
|
CCC::get(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate to the card using the provided management key (MGM).
|
/// Authenticate to the card using the provided management key (MGM).
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<(), Error> {
|
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<(), Error> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
// get a challenge from the card
|
// get a challenge from the card
|
||||||
let challenge = APDU::new(YKPIV_INS_AUTHENTICATE)
|
let challenge = APDU::new(Ins::Authenticate)
|
||||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&[0x7c, 0x02, 0x80, 0x00])
|
.data(&[TAG_DYN_AUTH, 0x02, 0x80, 0x00])
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
if !challenge.is_success() || challenge.buffer().len() < 12 {
|
if !challenge.is_success() || challenge.data().len() < 12 {
|
||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a response to the cards challenge and a challenge of our own.
|
// send a response to the cards challenge and a challenge of our own.
|
||||||
let response = mgm_key.decrypt(challenge.buffer()[4..12].try_into().unwrap());
|
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
|
||||||
|
|
||||||
let mut data = [0u8; 22];
|
let mut data = [0u8; 22];
|
||||||
data[0] = 0x7c;
|
data[0] = TAG_DYN_AUTH;
|
||||||
data[1] = 20; // 2 + 8 + 2 +8
|
data[1] = 20; // 2 + 8 + 2 +8
|
||||||
data[2] = 0x80;
|
data[2] = 0x80;
|
||||||
data[3] = 8;
|
data[3] = 8;
|
||||||
@@ -295,8 +302,8 @@ impl YubiKey {
|
|||||||
let mut challenge = [0u8; 8];
|
let mut challenge = [0u8; 8];
|
||||||
challenge.copy_from_slice(&data[14..22]);
|
challenge.copy_from_slice(&data[14..22]);
|
||||||
|
|
||||||
let authentication = APDU::new(YKPIV_INS_AUTHENTICATE)
|
let authentication = APDU::new(Ins::Authenticate)
|
||||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&data)
|
.data(&data)
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
@@ -308,7 +315,7 @@ impl YubiKey {
|
|||||||
let response = mgm_key.encrypt(&challenge);
|
let response = mgm_key.encrypt(&challenge);
|
||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
if response.ct_eq(&authentication.buffer()[4..12]).unwrap_u8() != 1 {
|
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
|
||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +327,7 @@ impl YubiKey {
|
|||||||
pub fn deauthenticate(&mut self) -> Result<(), Error> {
|
pub fn deauthenticate(&mut self) -> Result<(), Error> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
let status_words = APDU::new(YKPIV_INS_SELECT_APPLICATION)
|
let status_words = APDU::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(MGMT_AID)
|
.data(MGMT_AID)
|
||||||
.transmit(&txn, 255)?
|
.transmit(&txn, 255)?
|
||||||
@@ -337,40 +344,7 @@ impl YubiKey {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign data using a PIV key
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn sign_data(
|
|
||||||
&mut self,
|
|
||||||
raw_in: &[u8],
|
|
||||||
sign_out: &mut [u8],
|
|
||||||
out_len: &mut usize,
|
|
||||||
algorithm: u8,
|
|
||||||
key: SlotId,
|
|
||||||
) -> Result<(), 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt data using a PIV key
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn decrypt_data(
|
|
||||||
&mut self,
|
|
||||||
input: &[u8],
|
|
||||||
out: &mut [u8],
|
|
||||||
out_len: &mut usize,
|
|
||||||
algorithm: u8,
|
|
||||||
key: SlotId,
|
|
||||||
) -> Result<(), 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify device PIN.
|
/// Verify device PIN.
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<(), Error> {
|
pub fn verify_pin(&mut self, pin: &[u8]) -> Result<(), Error> {
|
||||||
{
|
{
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
@@ -378,15 +352,14 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !pin.is_empty() {
|
if !pin.is_empty() {
|
||||||
self.pin = Some(Buffer::new(pin.into()))
|
self.pin = Some(CachedPin::new(pin.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of PIN retries
|
/// Get the number of PIN retries
|
||||||
#[cfg(feature = "untested")]
|
pub fn get_pin_retries(&mut self) -> Result<u8, Error> {
|
||||||
pub fn get_pin_retries(&mut self) -> Result<u32, Error> {
|
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
// Force a re-select to unverify, because once verified the spec dictates that
|
// Force a re-select to unverify, because once verified the spec dictates that
|
||||||
@@ -404,24 +377,15 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Set the number of PIN retries
|
/// Set the number of PIN retries
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn set_pin_retries(&mut self, pin_tries: usize, puk_tries: usize) -> Result<(), Error> {
|
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<(), Error> {
|
||||||
// Special case: if either retry count is 0, it's a successful no-op
|
// Special case: if either retry count is 0, it's a successful no-op
|
||||||
if pin_tries == 0 || puk_tries == 0 {
|
if pin_tries == 0 || puk_tries == 0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if pin_tries > 0xff || puk_tries > 0xff || pin_tries < 1 || puk_tries < 1 {
|
|
||||||
return Err(Error::RangeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
let templ = [
|
let templ = [0, Ins::SetPinRetries.code(), pin_tries, puk_tries];
|
||||||
0,
|
|
||||||
YKPIV_INS_SET_PIN_RETRIES,
|
|
||||||
pin_tries as u8,
|
|
||||||
puk_tries as u8,
|
|
||||||
];
|
|
||||||
|
|
||||||
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
||||||
|
|
||||||
@@ -440,11 +404,11 @@ impl YubiKey {
|
|||||||
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
||||||
{
|
{
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.change_pin(CHREF_ACT_CHANGE_PIN, current_pin, new_pin)?;
|
txn.change_ref(ChangeRefAction::ChangePin, current_pin, new_pin)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !new_pin.is_empty() {
|
if !new_pin.is_empty() {
|
||||||
self.pin = Some(Buffer::new(new_pin.into()));
|
self.pin = Some(CachedPin::new(new_pin.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -454,7 +418,6 @@ impl YubiKey {
|
|||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
|
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<(), Error> {
|
||||||
let mut data = [0u8; CB_BUF_MAX];
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
let max_size = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
let buffer = metadata::read(&txn, TAG_ADMIN)?;
|
let buffer = metadata::read(&txn, TAG_ADMIN)?;
|
||||||
@@ -480,7 +443,7 @@ impl YubiKey {
|
|||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
metadata::write(&txn, TAG_ADMIN, &data, max_size).map_err(|e| {
|
metadata::write(&txn, TAG_ADMIN, &data).map_err(|e| {
|
||||||
error!("could not write admin data, err = {}", e);
|
error!("could not write admin data, err = {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
@@ -498,7 +461,7 @@ impl YubiKey {
|
|||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
|
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<(), Error> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.change_pin(CHREF_ACT_CHANGE_PUK, current_puk, new_puk)
|
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block PUK: permanently prevent the PIN from becoming unblocked
|
/// Block PUK: permanently prevent the PIN from becoming unblocked
|
||||||
@@ -508,12 +471,11 @@ impl YubiKey {
|
|||||||
let mut tries_remaining: i32 = -1;
|
let mut tries_remaining: i32 = -1;
|
||||||
let mut flags = [0];
|
let mut flags = [0];
|
||||||
|
|
||||||
let max_size = yubikey.obj_size_max();
|
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
while tries_remaining != 0 {
|
while tries_remaining != 0 {
|
||||||
// 2 -> change puk
|
// 2 -> change puk
|
||||||
let res = txn.change_pin(CHREF_ACT_CHANGE_PUK, &puk, &puk);
|
let res = txn.change_ref(ChangeRefAction::ChangePuk, &puk, &puk);
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => puk[0] += 1,
|
Ok(()) => puk[0] += 1,
|
||||||
@@ -559,7 +521,7 @@ impl YubiKey {
|
|||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
if metadata::write(&txn, TAG_ADMIN, &data[..cb_data], max_size).is_err() {
|
if metadata::write(&txn, TAG_ADMIN, &data[..cb_data]).is_err() {
|
||||||
error!("could not write admin metadata");
|
error!("could not write admin metadata");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -574,7 +536,7 @@ impl YubiKey {
|
|||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), Error> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.change_pin(CHREF_ACT_UNBLOCK_PIN, puk, new_pin)
|
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch an object from the YubiKey
|
/// Fetch an object from the YubiKey
|
||||||
@@ -591,232 +553,13 @@ impl YubiKey {
|
|||||||
txn.save_object(object_id, indata)
|
txn.save_object(object_id, indata)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import a private encryption or signing key into the YubiKey
|
|
||||||
// TODO(tarcieri): refactor this into separate methods per key type
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn import_private_key(
|
|
||||||
&mut self,
|
|
||||||
key: SlotId,
|
|
||||||
algorithm: u8,
|
|
||||||
p: Option<&[u8]>,
|
|
||||||
q: Option<&[u8]>,
|
|
||||||
dp: Option<&[u8]>,
|
|
||||||
dq: Option<&[u8]>,
|
|
||||||
qinv: Option<&[u8]>,
|
|
||||||
ec_data: Option<&[u8]>,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if pin_policy != YKPIV_PINPOLICY_DEFAULT
|
|
||||||
&& (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)
|
|
||||||
{
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if algorithm == YKPIV_ALGO_ECCP256 {
|
|
||||||
elem_len = 32;
|
|
||||||
} else if algorithm == YKPIV_ALGO_ECCP384 {
|
|
||||||
elem_len = 48;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ec_data.is_null() {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
params[0] = ec_data;
|
|
||||||
lens[0] = ec_data_len;
|
|
||||||
param_tag = 0x6;
|
|
||||||
n_params = 1;
|
|
||||||
}
|
|
||||||
_ => return Err(Error::AlgorithmError),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..n_params {
|
|
||||||
unsafe {
|
|
||||||
*in_ptr = (param_tag + i as i32) as u8;
|
|
||||||
in_ptr = in_ptr.offset(1);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)?
|
|
||||||
.status_words();
|
|
||||||
|
|
||||||
match status_words {
|
|
||||||
StatusWords::Success => Ok(()),
|
|
||||||
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
|
|
||||||
_ => Err(Error::GenericError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate an attestation certificate for a stored key.
|
|
||||||
/// <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 txn = self.begin_transaction()?;
|
|
||||||
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
|
|
||||||
|
|
||||||
if !response.is_success() {
|
|
||||||
if response.status_words() == StatusWords::NotSupportedError {
|
|
||||||
return Err(Error::NotSupported);
|
|
||||||
} else {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.buffer()[0] != 0x30 {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(response.into_buffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get an auth challenge
|
/// Get an auth challenge
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
|
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8], Error> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
let response = APDU::new(YKPIV_INS_AUTHENTICATE)
|
let response = APDU::new(Ins::Authenticate)
|
||||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&[0x7c, 0x02, 0x81, 0x00])
|
.data(&[0x7c, 0x02, 0x81, 0x00])
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
@@ -824,7 +567,7 @@ impl YubiKey {
|
|||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(response.buffer()[4..12].try_into().unwrap())
|
Ok(response.data()[4..12].try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify an auth response
|
/// Verify an auth response
|
||||||
@@ -840,8 +583,8 @@ impl YubiKey {
|
|||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
// send the response to the card and a challenge of our own.
|
// send the response to the card and a challenge of our own.
|
||||||
let status_words = APDU::new(YKPIV_INS_AUTHENTICATE)
|
let status_words = APDU::new(Ins::Authenticate)
|
||||||
.params(YKPIV_ALGO_3DES, YKPIV_KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&data)
|
.data(&data)
|
||||||
.transmit(&txn, 261)?
|
.transmit(&txn, 261)?
|
||||||
.status_words();
|
.status_words();
|
||||||
@@ -860,7 +603,7 @@ impl YubiKey {
|
|||||||
/// The reset function is only available when both pins are blocked.
|
/// The reset function is only available when both pins are blocked.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
pub fn reset_device(&mut self) -> Result<(), Error> {
|
pub fn reset_device(&mut self) -> Result<(), Error> {
|
||||||
let templ = [0, YKPIV_INS_RESET, 0, 0];
|
let templ = [0, Ins::Reset.code(), 0, 0];
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
let status_words = txn.transfer_data(&templ, &[], 255)?.status_words();
|
||||||
|
|
||||||
@@ -870,14 +613,36 @@ impl YubiKey {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get max object size supported by this device
|
impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
|
||||||
#[cfg(feature = "untested")]
|
type Error = Error;
|
||||||
pub(crate) fn obj_size_max(&self) -> usize {
|
|
||||||
if self.is_neo {
|
fn try_from(reader: &'a Reader<'_>) -> Result<Self, Error> {
|
||||||
CB_OBJ_MAX_NEO
|
let mut card = reader.connect().map_err(|e| {
|
||||||
} else {
|
error!("error connecting to reader '{}': {}", reader.name(), e);
|
||||||
CB_OBJ_MAX
|
e
|
||||||
}
|
})?;
|
||||||
|
|
||||||
|
info!("connected to reader: {}", reader.name());
|
||||||
|
|
||||||
|
let (version, serial) = {
|
||||||
|
let txn = Transaction::new(&mut card)?;
|
||||||
|
txn.select_application()?;
|
||||||
|
|
||||||
|
let v = txn.get_version()?;
|
||||||
|
let s = txn.get_serial(v)?;
|
||||||
|
(v, s)
|
||||||
|
};
|
||||||
|
|
||||||
|
let yubikey = YubiKey {
|
||||||
|
card,
|
||||||
|
name: String::from(reader.name()),
|
||||||
|
pin: None,
|
||||||
|
version,
|
||||||
|
serial,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(yubikey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+194
-8
@@ -3,18 +3,204 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
||||||
|
|
||||||
use std::env;
|
use getrandom::getrandom;
|
||||||
use yubikey_piv::YubiKey;
|
use lazy_static::lazy_static;
|
||||||
|
use log::trace;
|
||||||
|
use rsa::{hash::Hash::SHA2_256, PaddingScheme, PublicKey};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::{env, sync::Mutex};
|
||||||
|
use yubikey_piv::{
|
||||||
|
certificate::{Certificate, PublicKeyInfo},
|
||||||
|
key::{self, AlgorithmId, Key, RetiredSlotId, SlotId},
|
||||||
|
policy::{PinPolicy, TouchPolicy},
|
||||||
|
Error, MgmKey, YubiKey,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
lazy_static! {
|
||||||
#[ignore]
|
/// Provide thread-safe access to a YubiKey
|
||||||
fn connect() {
|
static ref YUBIKEY: Mutex<YubiKey> = init_yubikey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-time test initialization and setup
|
||||||
|
fn init_yubikey() -> Mutex<YubiKey> {
|
||||||
// Only show logs if `RUST_LOG` is set
|
// Only show logs if `RUST_LOG` is set
|
||||||
if env::var("RUST_LOG").is_ok() {
|
if env::var("RUST_LOG").is_ok() {
|
||||||
env_logger::builder().format_timestamp(None).init();
|
env_logger::builder().format_timestamp(None).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut yubikey = YubiKey::open(None).unwrap();
|
let yubikey = YubiKey::open().unwrap();
|
||||||
dbg!(&yubikey.version());
|
trace!("serial: {}", yubikey.serial());
|
||||||
dbg!(&yubikey.serial());
|
trace!("version: {}", yubikey.version());
|
||||||
|
|
||||||
|
Mutex::new(yubikey)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// CCCID support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_get_cccid() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
|
||||||
|
match yubikey.cccid() {
|
||||||
|
Ok(cccid) => trace!("CCCID: {:?}", cccid),
|
||||||
|
Err(Error::NotFound) => trace!("CCCID not found"),
|
||||||
|
Err(err) => panic!("error getting CCCID: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// CHUID support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_get_chuid() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
|
||||||
|
match yubikey.chuid() {
|
||||||
|
Ok(chuid) => trace!("CHUID: {:?}", chuid),
|
||||||
|
Err(Error::NotFound) => trace!("CHUID not found"),
|
||||||
|
Err(err) => panic!("error getting CHUID: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Device config support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_get_config() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let config_result = yubikey.config();
|
||||||
|
assert!(config_result.is_ok());
|
||||||
|
trace!("config: {:?}", config_result.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Cryptographic key support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_list_keys() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let keys_result = Key::list(&mut yubikey);
|
||||||
|
assert!(keys_result.is_ok());
|
||||||
|
trace!("keys: {:?}", keys_result.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// PIN support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_verify_pin() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
assert!(yubikey.verify_pin(b"000000").is_err());
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Certificate support
|
||||||
|
//
|
||||||
|
|
||||||
|
fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
|
||||||
|
|
||||||
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
|
// Generate a new key in the selected slot.
|
||||||
|
let generated = key::generate(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
algorithm,
|
||||||
|
PinPolicy::Default,
|
||||||
|
TouchPolicy::Default,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut serial = [0u8; 20];
|
||||||
|
getrandom(&mut serial).unwrap();
|
||||||
|
|
||||||
|
// Generate a self-signed certificate for the new key.
|
||||||
|
let cert_result = Certificate::generate_self_signed(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
serial,
|
||||||
|
None,
|
||||||
|
"testSubject".to_owned(),
|
||||||
|
generated,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(cert_result.is_ok());
|
||||||
|
let cert = cert_result.unwrap();
|
||||||
|
trace!("cert: {:?}", cert);
|
||||||
|
cert
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_self_signed_rsa_cert() {
|
||||||
|
let cert = generate_self_signed_cert(AlgorithmId::Rsa1024);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that the certificate is signed correctly
|
||||||
|
//
|
||||||
|
|
||||||
|
let pubkey = match cert.subject_pki() {
|
||||||
|
PublicKeyInfo::Rsa { pubkey, .. } => pubkey,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = cert.as_ref();
|
||||||
|
let tbs_cert_len = u16::from_be_bytes(data[6..8].try_into().unwrap()) as usize;
|
||||||
|
let msg = &data[4..8 + tbs_cert_len];
|
||||||
|
let sig = &data[data.len() - 128..];
|
||||||
|
|
||||||
|
let hash = Sha256::digest(msg);
|
||||||
|
|
||||||
|
assert!(pubkey
|
||||||
|
.verify(
|
||||||
|
PaddingScheme::PKCS1v15Sign {
|
||||||
|
hash: Some(SHA2_256)
|
||||||
|
},
|
||||||
|
&hash,
|
||||||
|
sig
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_self_signed_ec_cert() {
|
||||||
|
let cert = generate_self_signed_cert(AlgorithmId::EccP256);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that the certificate is signed correctly
|
||||||
|
//
|
||||||
|
|
||||||
|
let pubkey = match cert.subject_pki() {
|
||||||
|
PublicKeyInfo::EcP256(pubkey) => pubkey,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = cert.as_ref();
|
||||||
|
let tbs_cert_len = data[6] as usize;
|
||||||
|
let sig_algo_len = data[7 + tbs_cert_len + 1] as usize;
|
||||||
|
let sig_start = 7 + tbs_cert_len + 2 + sig_algo_len + 3;
|
||||||
|
let msg = &data[4..7 + tbs_cert_len];
|
||||||
|
let sig = &data[sig_start..];
|
||||||
|
|
||||||
|
use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1};
|
||||||
|
let ring_pk = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, pubkey.as_bytes());
|
||||||
|
assert!(ring_pk.verify(msg, sig).is_ok());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user