Compare commits
482 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e244e16f0 | |||
| ba51f6ad16 | |||
| ca2615eef8 | |||
| 6c12c7b187 | |||
| 403632df76 | |||
| 95babac2d4 | |||
| 872ba35f54 | |||
| c96b50bcec | |||
| abcded88cf | |||
| ff6d5ee56e | |||
| a039431fc9 | |||
| ec78d6b2f7 | |||
| 49fc8796ab | |||
| efc587c88d | |||
| 9e75924908 | |||
| dcaf080ef2 | |||
| 0072b174b4 | |||
| 74968cbef0 | |||
| 80968606b2 | |||
| 1e1fe34734 | |||
| 7eb7a31a28 | |||
| 1fc807fdcb | |||
| b4be1bb216 | |||
| 7f2b423713 | |||
| f0dbf9425c | |||
| 0d8096f50d | |||
| 13bdf9a585 | |||
| 235eb6215e | |||
| 19e1cccfec | |||
| 1af3cbbf91 | |||
| 5955001e00 | |||
| d204051912 | |||
| 626ac3bffd | |||
| 32cd92af50 | |||
| 0a90dc3ca8 | |||
| 69b5404370 | |||
| 2db3ea55c4 | |||
| b07612eb4e | |||
| 01eb42bc60 | |||
| 82cb78aa95 | |||
| 1c9f71a989 | |||
| 853677b2d8 | |||
| 6189de288b | |||
| 1f0d42218e | |||
| 164faac609 | |||
| c0f3a2f841 | |||
| 3e31fe8663 | |||
| 385db11522 | |||
| c1dc4a4319 | |||
| 3d78874a3b | |||
| f04b8592ec | |||
| 0c57c06294 | |||
| 885528a3d6 | |||
| ee3702a65e | |||
| 45915e5e5a | |||
| 75ce24a3ea | |||
| 78313360a1 | |||
| d226209ea4 | |||
| de142256d0 | |||
| 485d49a6c8 | |||
| 9932d05428 | |||
| 363648bbc5 | |||
| 6a1e1603ef | |||
| 8cf18d2986 | |||
| 07281440c0 | |||
| cd76a55318 | |||
| 23bbf1b783 | |||
| cafb0b2c18 | |||
| 0c7441a81e | |||
| a50addc15b | |||
| 0809f300b7 | |||
| d55079f9a6 | |||
| 10241230b3 | |||
| 1e02f135f0 | |||
| 0c2633ab31 | |||
| f49c617a9d | |||
| 1d33ea1747 | |||
| 18eb4bf4f4 | |||
| 10941bfb5b | |||
| 002491193e | |||
| 2e5139b237 | |||
| d880faaefa | |||
| cc00a10c2f | |||
| 0a2e798894 | |||
| 5c4259023f | |||
| 57bb088c7d | |||
| ccf19a3668 | |||
| db13fce53b | |||
| 0071566097 | |||
| d8653bc6f0 | |||
| 603b102932 | |||
| 7470b1613a | |||
| 4310cc0f9a | |||
| 87ed7b2338 | |||
| 7866d8d53e | |||
| 744238fd77 | |||
| bbb186f95e | |||
| c89cc5acd0 | |||
| 2294c1cc3a | |||
| 65e201db0f | |||
| b571f81007 | |||
| 0a36a37ae3 | |||
| 3463d109b2 | |||
| 014b7ee6fd | |||
| 498de4c10d | |||
| 98b038c873 | |||
| fab9d25b0a | |||
| bb80551324 | |||
| 9e20ecfe55 | |||
| fac83c60fb | |||
| 914f9bee0d | |||
| 83de59983f | |||
| e21395c934 | |||
| 935fea0868 | |||
| dd4b1c60a4 | |||
| 74a50f0f0c | |||
| 86d482b38d | |||
| edf74871ba | |||
| b11d5c409b | |||
| 52107281df | |||
| bcef792f69 | |||
| 10a7ead932 | |||
| 54ce90d51d | |||
| 3905104b52 | |||
| 97e15abcee | |||
| da7e7af109 | |||
| 6e96087b93 | |||
| f3bb858a2f | |||
| ac72797d1f | |||
| fdd3b8730a | |||
| d51ec0a225 | |||
| d601c33ba3 | |||
| 8e52d75992 | |||
| 42ae5fb974 | |||
| 224d346f09 | |||
| 01e5bba33f | |||
| 48f42780df | |||
| 92f770805f | |||
| 563f6f9ccc | |||
| 5f418bbd1d | |||
| 47776ebf0b | |||
| 227518dd1b | |||
| e6cea2eca6 | |||
| e249e91297 | |||
| 1018127843 | |||
| 1765e11bc0 | |||
| 1228d16439 | |||
| de51b0cc46 | |||
| 1051eaf26d | |||
| a1d9c7afc5 | |||
| 2c06626c25 | |||
| a2a912fc3c | |||
| c9e2edc41f | |||
| 20bf9b0679 | |||
| 0d4d4f7f06 | |||
| d31872964d | |||
| 865353f4da | |||
| 1ad17bb025 | |||
| d33e80faea | |||
| e61682be43 | |||
| 43dfc06875 | |||
| e230390e7e | |||
| ef3df46ed2 | |||
| 18e3636161 | |||
| 9d1da84233 | |||
| 37088bba56 | |||
| 3580c45f71 | |||
| 79c289ac00 | |||
| cbca858488 | |||
| 8b896ab4de | |||
| 24b035008c | |||
| 90bc878b21 | |||
| 08185c5ec9 | |||
| 7da2c7ba6a | |||
| fecd786262 | |||
| 1a6d1d0a71 | |||
| f43539088b | |||
| 72f63131ac | |||
| b59856d09e | |||
| 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 | |||
| 77d9dd6e97 | |||
| aeb4e6c3fc | |||
| a23af7dc31 | |||
| 9083194c3b | |||
| cf8f3c88cf | |||
| eb399cbecc | |||
| fd77e9f844 | |||
| 4039560d97 | |||
| 63d7a21c9d | |||
| 79b1142f21 | |||
| 67ed32cbf9 | |||
| c54f66acb4 | |||
| 6e4819bad1 | |||
| a9d7996aa6 | |||
| 9367218c7d | |||
| e18828d048 | |||
| ebbf043bc9 | |||
| 96cd5d080b | |||
| bd485eb912 | |||
| 64bf135f6c | |||
| b5bee1aa2f | |||
| 7b1d98f695 | |||
| f372cfc2a7 | |||
| 9b6fb7a39c | |||
| d01d2dec84 | |||
| 7412c02892 | |||
| 6e24660a80 | |||
| ad21eaea81 | |||
| a71389a820 | |||
| 35cc1bbf72 | |||
| 86fde50c2d | |||
| 634740d751 | |||
| c5a486cb4b | |||
| 87c00a9b61 | |||
| c0bbf9aa06 | |||
| ffdb114ae5 | |||
| c3d5df1643 | |||
| f25eed1a86 | |||
| 683e463824 | |||
| ce55e08af8 | |||
| 88ec6bcb32 | |||
| b23ed1d48a | |||
| 6324f7a75d | |||
| 9252765940 | |||
| a43bddb531 | |||
| 71a334a9b8 | |||
| b750b9cbbb | |||
| 31ef465571 | |||
| 90bdda85cb | |||
| c394511c60 | |||
| 4e710da32c | |||
| 7add9bfa41 | |||
| 6c03ea89ec | |||
| 5733d0b0af | |||
| 943dd6f146 | |||
| 65ec5aad63 | |||
| 012d164e12 | |||
| 9bcd85bce0 |
@@ -0,0 +1,6 @@
|
|||||||
|
[advisories]
|
||||||
|
ignore = [
|
||||||
|
"RUSTSEC-2020-0071", # chrono
|
||||||
|
"RUSTSEC-2021-0145", # atty
|
||||||
|
"RUSTSEC-2023-0071", # rsa: Marvin Attack: potential key recovery
|
||||||
|
] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
versioning-strategy: lockfile-only
|
||||||
|
directory: "/"
|
||||||
|
allow:
|
||||||
|
- dependency-type: "all"
|
||||||
|
groups:
|
||||||
|
all-deps:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUSTFLAGS: "-Dwarnings"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- 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.85.0 # MSRV
|
||||||
|
deps: sudo apt-get install libpcsclite-dev
|
||||||
|
- platform: windows-latest
|
||||||
|
toolchain: 1.85.0 # MSRV
|
||||||
|
deps: true
|
||||||
|
- platform: macos-latest
|
||||||
|
toolchain: 1.85.0 # MSRV
|
||||||
|
deps: true
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- 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@v6
|
||||||
|
|
||||||
|
- 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@v6
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: 1.85.0 # MSRV
|
||||||
|
components: clippy
|
||||||
|
override: true
|
||||||
|
- run: sudo apt-get install libpcsclite-dev
|
||||||
|
- run: cargo clippy --all --all-features -- -D warnings
|
||||||
@@ -1,125 +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: Run cargo check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Test Suite
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform:
|
|
||||||
- ubuntu-latest
|
|
||||||
- macos-latest
|
|
||||||
# TODO: support Windows after eliminating C legacy
|
|
||||||
# - 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
|
|
||||||
|
|
||||||
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 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: main
|
||||||
|
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@v6
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v5
|
||||||
|
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
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
|
||||||
|
|||||||
+331
-2
@@ -4,5 +4,334 @@ 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.1 (2019-11-18)
|
## Unreleased
|
||||||
- It typechecks, ship it!
|
### Added
|
||||||
|
- `yubikey::certificate::SelfSigned`
|
||||||
|
- `yubikey::Error::CertificateBuilder`
|
||||||
|
- `yubikey::MgmAlgorithmId`
|
||||||
|
- `yubikey::mgm`:
|
||||||
|
- `MgmKey::generate_for`
|
||||||
|
- `MgmKey::get_default`
|
||||||
|
- `impl AsRef<[u8]> for MgmKey`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- MSRV is now 1.81.
|
||||||
|
- Migrated the public API to the following (pre-release) dependencies:
|
||||||
|
- `der 0.8.0-rc.1`
|
||||||
|
- `ecdsa 0.17.0-pre.9`
|
||||||
|
- `p256 0.14.0-pre.2`
|
||||||
|
- `p384 0.14.0-pre.2`
|
||||||
|
- `rsa 0.10.0-pre.3`
|
||||||
|
- `sha2 0.11.0-pre.4`
|
||||||
|
- `x509-cert 0.3.0-pre.0`
|
||||||
|
- `yubikey::mgm`:
|
||||||
|
- `MgmKey::generate` now takes a `rand::TryCryptoRng` argument.
|
||||||
|
- `MgmKey::generate` now requires the caller to specify the key algorithm via
|
||||||
|
an `MgmAlgorithmId` parameter.
|
||||||
|
- Use `MgmKey::generate_for` if you want to generate a key using the
|
||||||
|
preferred algorithm for a given Yubikey's firmware version.
|
||||||
|
- `MgmKey::from_bytes` now takes an `Option<MgmAlgorithmId>` argument, to
|
||||||
|
disambiguate algorithms with the same key length.
|
||||||
|
- `yubikey::piv`:
|
||||||
|
- `ManagementAlgorithmId` has been renamed to `SlotAlgorithmId`, and its
|
||||||
|
`ThreeDes` variant has been replaced by `SlotAlgorithmId::Management`
|
||||||
|
containing a `yubikey::MgmAlgorithmId`.
|
||||||
|
- Metadata command returns `Error:NotFound` instead of `Error::GenericError` when the object doesn't exist ([#558]).
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `yubikey::mgm`:
|
||||||
|
- `MgmKey::new` (use `MgmKey::from_bytes(_, Some(MgmAlgorithmId::ThreeDes))`
|
||||||
|
instead).
|
||||||
|
- `impl AsRef<[u8; DES_LEN_3DES]> for MgmKey` (use
|
||||||
|
`impl AsRef<[u8]> for MgmKey` instead).
|
||||||
|
- `impl Default for MgmKey` (use `MgmKey::get_default` instead).
|
||||||
|
- `impl TryFrom<&[u8]> for MgmKey` (use `MgmKey::from_bytes` instead).
|
||||||
|
|
||||||
|
## 0.8.0 (2023-08-15)
|
||||||
|
### Added
|
||||||
|
- `impl Debug for {Context, YubiKey}` ([#457])
|
||||||
|
- `YubiKey::disconnect` ([#462])
|
||||||
|
- `Error::AppletNotFound` ([#476])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `Reader::open` now returns `Error::AppletNotFound` instead of `Error::Generic`
|
||||||
|
if the PIV applet is not present on the device. This is returned by non-PIV
|
||||||
|
virtual smart cards like Windows Hello for Business, as well as some smart
|
||||||
|
card readers when no card is present.
|
||||||
|
- `Reader::open` now avoids resetting the card if an error occurs (equivalent to
|
||||||
|
calling `YubiKey::disconnect(pcsc::Disposition::LeaveCard)` if `Reader::open`
|
||||||
|
succeeds).
|
||||||
|
- Raise minimum `pcsc` version to 2.3.1 and remove workaround ([#478])
|
||||||
|
- Bump asymmetric crypto dependencies; MSRV 1.65 ([#490])
|
||||||
|
- `elliptic-curve` v0.13
|
||||||
|
- `k256` v0.13
|
||||||
|
- `p256` v0.13
|
||||||
|
- `p384` v0.13
|
||||||
|
- `pbkdf2` v0.12
|
||||||
|
- `rsa` v0.9 ([#502])
|
||||||
|
- `signature` v2
|
||||||
|
- Use `x509-cert` certificate builder ([#495])
|
||||||
|
- Make `RsaKeyData::new` fallible ([#517])
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `StatusWords::code` now returns the correct code (including embedded `tries`
|
||||||
|
count) for `StatusWords::VerifyFailError`. Previously the returned code lost
|
||||||
|
information and was not round-trip compatible with `StatusWords::from(u16)`.
|
||||||
|
- Parsing of serial numbers ([#466])
|
||||||
|
- Make `YubiKey::open()` more robust ([#504])
|
||||||
|
|
||||||
|
[#457]: https://github.com/iqlusioninc/yubikey.rs/pull/457
|
||||||
|
[#462]: https://github.com/iqlusioninc/yubikey.rs/pull/462
|
||||||
|
[#466]: https://github.com/iqlusioninc/yubikey.rs/pull/466
|
||||||
|
[#476]: https://github.com/iqlusioninc/yubikey.rs/pull/476
|
||||||
|
[#478]: https://github.com/iqlusioninc/yubikey.rs/pull/478
|
||||||
|
[#490]: https://github.com/iqlusioninc/yubikey.rs/pull/490
|
||||||
|
[#495]: https://github.com/iqlusioninc/yubikey.rs/pull/495
|
||||||
|
[#502]: https://github.com/iqlusioninc/yubikey.rs/pull/502
|
||||||
|
[#504]: https://github.com/iqlusioninc/yubikey.rs/pull/504
|
||||||
|
[#517]: https://github.com/iqlusioninc/yubikey.rs/pull/517
|
||||||
|
|
||||||
|
## 0.7.0 (2022-11-14)
|
||||||
|
### Added
|
||||||
|
- Display inner PC/SC errors ([#420])
|
||||||
|
- Support for metadata command ([#371])
|
||||||
|
- Better `certificate::Serial` inspection ([#437])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- MSRV 1.60.0 ([#423])
|
||||||
|
- Bump `rsa` to v0.7.1 ([#440])
|
||||||
|
- Switch from `lazy_static` to `once_cell` ([#442])
|
||||||
|
- Switch from `subtle-encoding` to `base16ct` ([#443])
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Use `chrono` v0.4.23 or newer ([#436])
|
||||||
|
- `Certificate::issuer` was returning the subject instead ([#437])
|
||||||
|
|
||||||
|
[#371]: https://github.com/iqlusioninc/yubikey.rs/pull/371
|
||||||
|
[#420]: https://github.com/iqlusioninc/yubikey.rs/pull/420
|
||||||
|
[#423]: https://github.com/iqlusioninc/yubikey.rs/pull/423
|
||||||
|
[#436]: https://github.com/iqlusioninc/yubikey.rs/pull/436
|
||||||
|
[#437]: https://github.com/iqlusioninc/yubikey.rs/pull/437
|
||||||
|
[#440]: https://github.com/iqlusioninc/yubikey.rs/pull/440
|
||||||
|
[#442]: https://github.com/iqlusioninc/yubikey.rs/pull/442
|
||||||
|
[#443]: https://github.com/iqlusioninc/yubikey.rs/pull/443
|
||||||
|
|
||||||
|
## 0.6.0 (2022-08-10)
|
||||||
|
### Changed
|
||||||
|
- 2021 edition upgrade ([#343])
|
||||||
|
- RustCrypto crate upgrades; MSRV 1.57 ([#378])
|
||||||
|
- `des` v0.8
|
||||||
|
- `elliptic-curve` v0.12
|
||||||
|
- `hmac` v0.12
|
||||||
|
- `num-bigint-dig` v0.8
|
||||||
|
- `pbkdf2` v0.11
|
||||||
|
- `p256` v0.11
|
||||||
|
- `p384` v0.11
|
||||||
|
- `rsa` v0.6
|
||||||
|
- `sha1` v0.10 (replacing `sha-1`)
|
||||||
|
- `sha2` v0.10
|
||||||
|
- Bump `uuid` to v1.0 ([#376])
|
||||||
|
- Bump `der-parser` to v8.0 ([#402])
|
||||||
|
- Bump `x509-parser` to v0.14 ([#402])
|
||||||
|
|
||||||
|
[#343]: https://github.com/iqlusioninc/yubikey.rs/pull/343
|
||||||
|
[#376]: https://github.com/iqlusioninc/yubikey.rs/pull/376
|
||||||
|
[#378]: https://github.com/iqlusioninc/yubikey.rs/pull/378
|
||||||
|
[#402]: https://github.com/iqlusioninc/yubikey.rs/pull/402
|
||||||
|
|
||||||
|
## 0.5.0 (2021-11-21)
|
||||||
|
### Changed
|
||||||
|
- Update `rsa` dependency to 0.5 ([#315])
|
||||||
|
- Update `pbkdf2` dependency to 0.9 ([#315])
|
||||||
|
- Update `x509-parser` dependency to 0.12 ([#315], [#322])
|
||||||
|
- Update `nom` to v7.0 ([#322])
|
||||||
|
|
||||||
|
[#315]: https://github.com/iqlusioninc/yubikey.rs/pull/315
|
||||||
|
[#322]: https://github.com/iqlusioninc/yubikey.rs/pull/322
|
||||||
|
|
||||||
|
## 0.4.2 (2021-07-13)
|
||||||
|
### Added
|
||||||
|
- Make `yubikey::Buffer` a pub type ([#290])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Have `YubiKey::block_puk` take `&mut self` as argument ([#289])
|
||||||
|
|
||||||
|
[#289]: https://github.com/iqlusioninc/yubikey.rs/pull/289
|
||||||
|
[#290]: https://github.com/iqlusioninc/yubikey.rs/pull/290
|
||||||
|
|
||||||
|
## 0.4.1 (2021-07-12)
|
||||||
|
### Changed
|
||||||
|
- Rename `SettingValue` to `Setting` ([#286])
|
||||||
|
- Rename `Ccc` to `CccId` ([#287])
|
||||||
|
|
||||||
|
[#286]: https://github.com/iqlusioninc/yubikey.rs/pull/286
|
||||||
|
[#287]: https://github.com/iqlusioninc/yubikey.rs/pull/287
|
||||||
|
|
||||||
|
## 0.4.0 (2021-07-12) [YANKED]
|
||||||
|
### Added
|
||||||
|
- `Result` alias ([#271])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Renamed crate from `yubikey-piv` => `yubikey` ([#267])
|
||||||
|
- Renamed the following:
|
||||||
|
- `APDU` => `Apdu` ([#269])
|
||||||
|
- `CCC` => `Ccc` ([#269])
|
||||||
|
- `CHUID` => `ChuId` ([#269])
|
||||||
|
- `Ccc::cccid` => `Ccc::card_id` ([#270])
|
||||||
|
- `key` => `piv` ([#277])
|
||||||
|
- `readers` => `reader` ([#278])
|
||||||
|
- `readers::Readers` => `reader::Context` ([#278])
|
||||||
|
- Bumped the following dependencies:
|
||||||
|
- `rsa` => v0.4 ([#246])
|
||||||
|
- `des` => v0.7 ([#251])
|
||||||
|
- `elliptic-curve` => v0.10 ([#268])
|
||||||
|
- `hmac` => v0.11 ([#251])
|
||||||
|
- `pbkdf2` => v0.8 ([#251])
|
||||||
|
- `p256` => v0.9 ([#268])
|
||||||
|
- `p384` => v0.8 ([#268])
|
||||||
|
- MSRV 1.51+ ([#268])
|
||||||
|
- Flatten API ([#274])
|
||||||
|
- Replace `getrandom` with `rand_core` ([#276])
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Potential local DoS in TLV parser ([#279])
|
||||||
|
|
||||||
|
[#246]: https://github.com/iqlusioninc/yubikey.rs/pull/246
|
||||||
|
[#251]: https://github.com/iqlusioninc/yubikey.rs/pull/251
|
||||||
|
[#267]: https://github.com/iqlusioninc/yubikey.rs/pull/267
|
||||||
|
[#268]: https://github.com/iqlusioninc/yubikey.rs/pull/268
|
||||||
|
[#269]: https://github.com/iqlusioninc/yubikey.rs/pull/269
|
||||||
|
[#270]: https://github.com/iqlusioninc/yubikey.rs/pull/270
|
||||||
|
[#271]: https://github.com/iqlusioninc/yubikey.rs/pull/271
|
||||||
|
[#274]: https://github.com/iqlusioninc/yubikey.rs/pull/274
|
||||||
|
[#276]: https://github.com/iqlusioninc/yubikey.rs/pull/276
|
||||||
|
[#277]: https://github.com/iqlusioninc/yubikey.rs/pull/277
|
||||||
|
[#278]: https://github.com/iqlusioninc/yubikey.rs/pull/278
|
||||||
|
[#279]: https://github.com/iqlusioninc/yubikey.rs/pull/279
|
||||||
|
|
||||||
|
## yubikey-piv 0.3.0 (2021-03-22)
|
||||||
|
### Added
|
||||||
|
- Typed structs for PIN-protected and admin metadata ([#223])
|
||||||
|
- `MgmKey::set_default`/`MgmKey::set_manual` methods ([#224])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Have `Transaction::set_mgm_key` take touch requirement as bool ([#224])
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `MgmKey::set` method ([#224])
|
||||||
|
|
||||||
|
[#223]: https://github.com/iqlusioninc/yubikey.rs/pull/223
|
||||||
|
[#224]: https://github.com/iqlusioninc/yubikey.rs/pull/224
|
||||||
|
|
||||||
|
## yubikey-piv 0.2.0 (2021-01-30)
|
||||||
|
### Changed
|
||||||
|
- Bump `der-parser` to v5.0 ([#194])
|
||||||
|
- Improve self-signed certificates ([#207])
|
||||||
|
- Bump `x509-parser` to v0.9 ([#208])
|
||||||
|
- Bump elliptic-curve to 0.8. Also requires bumping p256 and p384 ([#208])
|
||||||
|
- Bump MSRV to 1.46+ ([#208])
|
||||||
|
- Bump `pbkdf2` dependency to v0.7 ([#219])
|
||||||
|
|
||||||
|
[#194]: https://github.com/iqlusioninc/yubikey.rs/pull/194
|
||||||
|
[#207]: https://github.com/iqlusioninc/yubikey.rs/pull/207
|
||||||
|
[#208]: https://github.com/iqlusioninc/yubikey.rs/pull/208
|
||||||
|
[#219]: https://github.com/iqlusioninc/yubikey.rs/pull/219
|
||||||
|
|
||||||
|
## yubikey-piv 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.rs/pull/177
|
||||||
|
[#175]: https://github.com/iqlusioninc/yubikey.rs/pull/175
|
||||||
|
[#128]: https://github.com/iqlusioninc/yubikey.rs/pull/128
|
||||||
|
[#82]: https://github.com/iqlusioninc/yubikey.rs/pull/82
|
||||||
|
[#73]: https://github.com/iqlusioninc/yubikey.rs/pull/73
|
||||||
|
[#88]: https://github.com/iqlusioninc/yubikey.rs/pull/88
|
||||||
|
[#80]: https://github.com/iqlusioninc/yubikey.rs/pull/80
|
||||||
|
[#69]: https://github.com/iqlusioninc/yubikey.rs/pull/69
|
||||||
|
[#68]: https://github.com/iqlusioninc/yubikey.rs/pull/68
|
||||||
|
[#67]: https://github.com/iqlusioninc/yubikey.rs/pull/67
|
||||||
|
[#65]: https://github.com/iqlusioninc/yubikey.rs/pull/65
|
||||||
|
[#64]: https://github.com/iqlusioninc/yubikey.rs/pull/64
|
||||||
|
[#63]: https://github.com/iqlusioninc/yubikey.rs/pull/63
|
||||||
|
[#62]: https://github.com/iqlusioninc/yubikey.rs/pull/62
|
||||||
|
[#61]: https://github.com/iqlusioninc/yubikey.rs/pull/61
|
||||||
|
[#60]: https://github.com/iqlusioninc/yubikey.rs/pull/60
|
||||||
|
|
||||||
|
## yubikey-piv 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.rs/pull/51
|
||||||
|
[#45]: https://github.com/iqlusioninc/yubikey.rs/pull/45
|
||||||
|
[#44]: https://github.com/iqlusioninc/yubikey.rs/pull/44
|
||||||
|
[#43]: https://github.com/iqlusioninc/yubikey.rs/pull/43
|
||||||
|
[#42]: https://github.com/iqlusioninc/yubikey.rs/pull/42
|
||||||
|
[#39]: https://github.com/iqlusioninc/yubikey.rs/pull/39
|
||||||
|
[#37]: https://github.com/iqlusioninc/yubikey.rs/pull/37
|
||||||
|
[#36]: https://github.com/iqlusioninc/yubikey.rs/pull/36
|
||||||
|
[#34]: https://github.com/iqlusioninc/yubikey.rs/pull/34
|
||||||
|
[#33]: https://github.com/iqlusioninc/yubikey.rs/pull/33
|
||||||
|
[#32]: https://github.com/iqlusioninc/yubikey.rs/pull/32
|
||||||
|
|
||||||
|
## yubikey-piv 0.0.2 (2019-11-25)
|
||||||
|
### Added
|
||||||
|
- `untested` Cargo feature to mark untested functionality ([#30])
|
||||||
|
- Initial connect test and docs ([#19])
|
||||||
|
- Clean up APDU construction with builder API ([#15])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rewrite translated code to use the `pcsc` crate ([#17])
|
||||||
|
- Rename ErrorKind to Error ([#13])
|
||||||
|
- Use `des` crate for 3DES operations ([#10])
|
||||||
|
- Replace `PKCS5_PBKDF2_HMAC_SHA1` with `pbkdf2` et al crates ([#9])
|
||||||
|
- Replace `RAND_bytes` with `getrandom` crate ([#8])
|
||||||
|
- Use `log` crate for logging ([#7])
|
||||||
|
- Replace `ErrorKind::Ok` with `Result` ([#6])
|
||||||
|
|
||||||
|
[#30]: https://github.com/iqlusioninc/yubikey.rs/pull/30
|
||||||
|
[#19]: https://github.com/iqlusioninc/yubikey.rs/pull/19
|
||||||
|
[#17]: https://github.com/iqlusioninc/yubikey.rs/pull/17
|
||||||
|
[#15]: https://github.com/iqlusioninc/yubikey.rs/pull/15
|
||||||
|
[#13]: https://github.com/iqlusioninc/yubikey.rs/pull/13
|
||||||
|
[#10]: https://github.com/iqlusioninc/yubikey.rs/pull/10
|
||||||
|
[#9]: https://github.com/iqlusioninc/yubikey.rs/pull/9
|
||||||
|
[#8]: https://github.com/iqlusioninc/yubikey.rs/pull/8
|
||||||
|
[#7]: https://github.com/iqlusioninc/yubikey.rs/pull/7
|
||||||
|
[#6]: https://github.com/iqlusioninc/yubikey.rs/pull/6
|
||||||
|
|
||||||
|
## yubikey-piv 0.0.1 (2019-11-18)
|
||||||
|
- 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/
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2014-2019 Yubico AB, Tony Arcieri
|
Copyright (c) 2014-2021 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
|
||||||
|
|||||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
+60
-13
@@ -1,21 +1,68 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey-piv"
|
name = "yubikey"
|
||||||
version = "0.0.1" # Also update html_root_url in lib.rs when bumping this
|
version = "0.9.0-pre.0"
|
||||||
description = """
|
description = """
|
||||||
Pure Rust host-side driver for the YubiKey Personal Identity Verification (PIV)
|
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
|
||||||
CCID application providing general-purpose public-key signing and encryption
|
support for hardware-backed public-key decryption and digital signatures using
|
||||||
with hardware-backed private keys for RSA (2048/1024) and ECC (P-256/P-384)
|
the Personal Identity Verification (PIV) application. Supports RSA (1024/2048/3072/4096)
|
||||||
algorithms (e.g, PKCS#1v1.5, ECDSA)
|
or ECC (NIST P-256/P-384) algorithms e.g, PKCS#1v1.5, ECDSA
|
||||||
"""
|
"""
|
||||||
|
authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"]
|
||||||
authors = ["Tony Arcieri <bascule@gmail.com>", "Yubico AB"]
|
|
||||||
edition = "2018"
|
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
repository = "https://github.com/tarcieri/yubikey-piv.rs"
|
repository = "https://github.com/iqlusioninc/yubikey.rs"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["api-bindings", "cryptography", "hardware-support"]
|
categories = ["api-bindings", "authentication", "cryptography", "hardware-support"]
|
||||||
keywords = ["ccid", "ecdsa", "rsa", "piv", "yubikey"]
|
keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [".", "cli"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
sha2 = "0.11"
|
||||||
|
x509-cert = { version = "0.3.0-rc.4", features = ["builder", "hazmat"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2"
|
aes = { version = "0.9.0-rc.4", features = ["zeroize"] }
|
||||||
|
bitflags = "2.5.0"
|
||||||
|
cipher = { version = "0.5", features = ["getrandom", "rand_core"] }
|
||||||
|
curve25519-dalek = "5.0.0-pre.6"
|
||||||
|
der = "0.8"
|
||||||
|
des = "0.9.0-rc.3"
|
||||||
|
ecdsa = { version = "0.17.0-rc.16", features = ["digest", "pem"] }
|
||||||
|
ed25519-dalek = { version = "3.0.0-pre.6", features = ["alloc", "pkcs8"] }
|
||||||
|
elliptic-curve = "0.14.0-rc.29"
|
||||||
|
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
||||||
|
log = "0.4"
|
||||||
|
nom = "8"
|
||||||
|
p256 = "0.14.0-rc.8"
|
||||||
|
p384 = "0.14.0-rc.8"
|
||||||
|
pbkdf2 = { version = "0.13.0-rc.10", default-features = false, features = ["hmac"] }
|
||||||
|
pcsc = "2.3.1"
|
||||||
|
rand = "0.10"
|
||||||
|
rand_core = "0.10"
|
||||||
|
rsa = { version = "0.10.0-rc.17", features = ["sha2"] }
|
||||||
|
sha1 = { version = "0.11", features = ["oid"] }
|
||||||
|
sha2 = { workspace = true, features = ["oid"] }
|
||||||
|
signature = "3.0.0-rc.10"
|
||||||
|
subtle = "2"
|
||||||
|
uuid = { version = "1.2", features = ["v4"] }
|
||||||
|
x25519-dalek = "3.0.0-pre.6"
|
||||||
|
x509-cert.workspace = true
|
||||||
zeroize = "1"
|
zeroize = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
|
once_cell = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
untested = []
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "change-mode"
|
||||||
|
required-features = ["untested"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|||||||
@@ -1,76 +1,181 @@
|
|||||||
<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.rs/main/img/logo.png" width="150" height="110">
|
||||||
|
|
||||||
# yubikey-piv.rs
|
# yubikey.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]
|
|
||||||
![Rust Version][rustc-image]
|
|
||||||
![Maintenance Status: Experimental][maintenance-image]
|
|
||||||
[![Build Status][build-image]][build-link]
|
[![Build Status][build-image]][build-link]
|
||||||
[![Gitter Chat][gitter-image]][gitter-link]
|
[![Safety Dance][safety-image]][safety-link]
|
||||||
|
[![Dependency Status][deps-image]][deps-link]
|
||||||
|
[![2-Clause BSD Licensed][license-image]][license-link]
|
||||||
|
![MSRV][msrv-image]
|
||||||
|
|
||||||
Pure Rust host-side YubiKey [Personal Identity Verification (PIV)][1] driver
|
Pure Rust cross-platform host-side driver for [YubiKey] devices from [Yubico]
|
||||||
with general-purpose public-key encryption and signing support.
|
with support for public-key encryption and digital signatures using the
|
||||||
|
[Personal Identity Verification (PIV)][PIV] application.
|
||||||
|
|
||||||
|
Uses the Personal Computer/Smart Card ([PC/SC]) interface with cross-platform
|
||||||
|
access provided by the [`pcsc` crate].
|
||||||
|
|
||||||
[Documentation][docs-link]
|
[Documentation][docs-link]
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
YubiKeys are versatile devices and through their PIV support, you can use them
|
YubiKeys are versatile devices and through their PIV support, you can use them
|
||||||
to store a number of RSA (2048/1024) and ECC (NIST P-256/P-384) private keys
|
to store a number of RSA (1024/2048/3072/4096) and ECC (NIST P-256/P-384) private keys
|
||||||
with configurable access control policies. Both the signing (RSASSA/ECDSA) and
|
with configurable access control policies. Both the signing (RSASSA/ECDSA) and
|
||||||
encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type.
|
encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type.
|
||||||
|
|
||||||
If you've been wanting to use Rust to sign and/or encrypt stuff using a
|
See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
|
||||||
private key generated and stored on a Yubikey (with option PIN-based access),
|
on which devices support PIV and the available functionality.
|
||||||
|
|
||||||
|
If you've been wanting to use Rust to sign and/or encrypt data using a
|
||||||
|
private key generated and stored on a YubiKey (with option PIN-based access),
|
||||||
this is the crate you've been after!
|
this is the crate you've been after!
|
||||||
|
|
||||||
One small problem, it's not done yet... 😫
|
Note that while this project started as a fork of a [Yubico] project,
|
||||||
|
|
||||||
But it might be close?
|
|
||||||
|
|
||||||
## History
|
|
||||||
|
|
||||||
This library is a Rust translation of the [yubico-piv-tool][2] utility by
|
|
||||||
Yubico, which was originally written in C. It was mechanically translated
|
|
||||||
from C into Rust using [Corrode][3], and then subsequently heavily
|
|
||||||
refactored into safer, more idiomatic Rust§.
|
|
||||||
|
|
||||||
Note that while this project started as a fork of a [Yubico][4] project,
|
|
||||||
this fork is **NOT** an official Yubico project and is in no way supported or
|
this fork is **NOT** an official Yubico project and is in no way supported or
|
||||||
endorsed by Yubico.
|
endorsed by Yubico.
|
||||||
|
|
||||||
§ *NOTE*: This section is actually full of lies and notes aspirations/goals,
|
## Features
|
||||||
not history. That said, there's been a decent amount of work cleaning up the
|
|
||||||
mechanically translated code, and at ~5klocs it's not that much.
|
### Personal Identity Verification (PIV)
|
||||||
|
|
||||||
|
[PIV] is a [NIST] standard for both *signing* and *encryption*
|
||||||
|
using SmartCards and SmartCard-based hardware tokens like YubiKeys.
|
||||||
|
|
||||||
|
PIV-related functionality can be found in the [`piv`] module.
|
||||||
|
|
||||||
|
This library natively implements the protocol used to manage and
|
||||||
|
utilize PIV encryption and signing keys which can be generated, imported,
|
||||||
|
and stored on YubiKey devices.
|
||||||
|
|
||||||
|
See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
|
||||||
|
on which devices support PIV and the available functionality.
|
||||||
|
|
||||||
|
### Supported Algorithms
|
||||||
|
|
||||||
|
- **Authentication**: `3DES`
|
||||||
|
- **Encryption**:
|
||||||
|
- RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
|
||||||
|
- ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
||||||
|
- **Signatures**:
|
||||||
|
- RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
|
||||||
|
- ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
|
||||||
|
- RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
|
||||||
|
- `RSA3072` and `RSA4096` require a YubiKey with firmware 5.7 or newer.
|
||||||
|
|
||||||
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
|
Rust **1.60** or newer.
|
||||||
|
|
||||||
|
## Supported YubiKeys
|
||||||
|
|
||||||
|
- [YubiKey 4] series
|
||||||
|
- [YubiKey 5] series
|
||||||
|
|
||||||
|
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
|
||||||
|
|
||||||
|
## Supported Operating Systems
|
||||||
|
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
|
||||||
## Security Warning
|
## Security Warning
|
||||||
|
|
||||||
No security audits of this crate have ever been performed, and it has not been
|
No security audits of this crate have ever been performed. Presently it is in
|
||||||
thoroughly assessed to ensure its operation is constant-time on common CPU
|
an experimental stage and may still contain high-severity issues.
|
||||||
architectures.
|
|
||||||
|
|
||||||
USE AT YOUR OWN RISK!
|
USE AT YOUR OWN RISK!
|
||||||
|
|
||||||
## Requirements
|
## Status
|
||||||
|
|
||||||
- Rust 1.39+
|
Functionality which has been successfully tested is available by default.
|
||||||
|
|
||||||
|
Any functionality which is gated on the `untested` feature has not been
|
||||||
|
properly tested and is not known to function correctly.
|
||||||
|
|
||||||
|
Please see the [`untested` functionality tracking issue] for current status.
|
||||||
|
We would appreciate any help testing this functionality and removing the
|
||||||
|
`untested` gating as well as writing more automated tests.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To run the full test suite, you'll need a supported YubiKey device connected
|
||||||
|
which is in the default state (i.e. default PIN/PUK).
|
||||||
|
|
||||||
|
Tests which run live against a YubiKey device are marked as `#[ignore]` by
|
||||||
|
default in order to pass when running in a CI environment. To run these
|
||||||
|
tests locally, invoke the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo test -- --ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
This crate makes extensive use of the `log` facade to provide detailed
|
||||||
|
information about what is happening. If you'd like to print this logging
|
||||||
|
information while running the tests, set the `RUST_LOG` environment variable
|
||||||
|
to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
RUST_LOG=info cargo test -- --ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
To trace every message sent to/from the card i.e. the raw
|
||||||
|
Application Protocol Data Unit (APDU) messages, use the `trace` log level:
|
||||||
|
|
||||||
|
```text
|
||||||
|
running 1 test
|
||||||
|
[INFO yubikey::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
||||||
|
[INFO yubikey::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
||||||
|
[TRACE yubikey::apdu] >>> Apdu { cla: 0, ins: SelectApplication, p1: 4, p2: 0, data: [160, 0, 0, 3, 8] }
|
||||||
|
[TRACE yubikey::transaction] >>> [0, 164, 4, 0, 5, 160, 0, 0, 3, 8]
|
||||||
|
[TRACE yubikey::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::apdu] >>> Apdu { cla: 0, ins: GetVersion, p1: 0, p2: 0, data: [] }
|
||||||
|
[TRACE yubikey::transaction] >>> [0, 253, 0, 0, 0]
|
||||||
|
[TRACE yubikey::apdu] <<< Response { status_words: Success, data: [5, 1, 2] }
|
||||||
|
[TRACE yubikey::apdu] >>> Apdu { cla: 0, ins: GetSerial, p1: 0, p2: 0, data: [] }
|
||||||
|
[TRACE yubikey::transaction] >>> [0, 248, 0, 0, 0]
|
||||||
|
[TRACE yubikey::apdu] <<< Response { status_words: Success, data: [0, 115, 0, 178] }
|
||||||
|
test connect ... ok
|
||||||
|
```
|
||||||
|
|
||||||
|
APDU messages labeled `>>>` are being sent to the YubiKey's internal SmartCard,
|
||||||
|
and ones labeled `<<<` are the responses.
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
This library is a Rust translation of the [yubico-piv-tool] utility by
|
||||||
|
Yubico, which was originally written in C. It was mechanically translated
|
||||||
|
from C into Rust using [Corrode], and then subsequently heavily
|
||||||
|
refactored into safer, more idiomatic Rust.
|
||||||
|
|
||||||
|
For more information on [yubico-piv-tool] and background information on how
|
||||||
|
the YubiKey implementation of PIV works in general, see the
|
||||||
|
[Yubico PIV Tool Command Line Guide][piv-tool-guide].
|
||||||
|
|
||||||
|
## ⚠️ Security Warning
|
||||||
|
|
||||||
|
No security audits of this crate have ever been performed.
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
We abide by the [Contributor Covenant][5] and ask that you do as well.
|
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
||||||
|
|
||||||
For more information, please see [CODE_OF_CONDUCT.md][6].
|
For more information, please see [CODE_OF_CONDUCT.md][cc-md].
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
**yubikey-piv.rs** is a fork of and originally a mechanical translation from
|
**yubikey.rs** is a fork of and originally a mechanical translation from
|
||||||
Yubico's [`yubico-piv-tool`][2], a C library/CLI program. The original library
|
Yubico's [yubico-piv-tool], a C library/CLI program. The original library
|
||||||
was licensed under a [2-Clause BSD License][5], 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-2025 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
|
||||||
@@ -101,29 +206,53 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
Unless you explicitly state otherwise, any contribution intentionally
|
Unless you explicitly state otherwise, any contribution intentionally
|
||||||
submitted for inclusion in the work by you shall be licensed under the
|
submitted for inclusion in the work by you shall be licensed under the
|
||||||
[2-Clause BSD License][5] as shown above, without any additional terms
|
[2-Clause BSD License][BSDL] as shown above, without any additional terms
|
||||||
or conditions.
|
or conditions.
|
||||||
|
|
||||||
[//]: # (badges)
|
[//]: # (badges)
|
||||||
|
|
||||||
[crate-image]: https://img.shields.io/crates/v/yubikey-piv.svg
|
[crate-image]: https://img.shields.io/crates/v/yubikey?logo=rust
|
||||||
[crate-link]: https://crates.io/crates/yubikey-piv
|
[crate-link]: https://crates.io/crates/yubikey
|
||||||
[docs-image]: https://docs.rs/yubikey-piv/badge.svg
|
[docs-image]: https://docs.rs/yubikey/badge.svg
|
||||||
[docs-link]: https://docs.rs/yubikey-piv/
|
[docs-link]: https://docs.rs/yubikey/
|
||||||
[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.rs/blob/main/COPYING
|
||||||
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
|
[msrv-image]: https://img.shields.io/badge/rustc-1.85+-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/tarcieri/yubihsm-piv.rs.svg
|
[build-image]: https://github.com/iqlusioninc/yubikey.rs/actions/workflows/ci.yml/badge.svg
|
||||||
[gitter-link]: https://gitter.im/tarcieri/community
|
[build-link]: https://github.com/iqlusioninc/yubikey.rs/actions
|
||||||
|
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
|
||||||
|
[deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs
|
||||||
|
|
||||||
[//]: # (general links)
|
[//]: # (general links)
|
||||||
|
|
||||||
[1]: https://piv.idmanagement.gov/
|
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
|
||||||
[2]: https://github.com/Yubico/yubico-piv-tool/
|
[Yubico]: https://www.yubico.com/
|
||||||
[3]: https://github.com/jameysharp/corrode
|
[PIV]: https://piv.idmanagement.gov/
|
||||||
[4]: https://www.yubico.com/
|
[NIST]: https://www.nist.gov/
|
||||||
[5]: https://contributor-covenant.org/
|
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC
|
||||||
[6]: https://github.com/tarcieri/yubikey-piv.rs/blob/develop/CODE_OF_CONDUCT.md
|
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
|
||||||
[7]: https://opensource.org/licenses/BSD-2-Clause
|
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
||||||
|
[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/
|
||||||
|
[piv-tool-guide]: https://docs.yubico.com/software/yubikey/tools/pivtool/Introduction.html
|
||||||
|
[Corrode]: https://github.com/jameysharp/corrode
|
||||||
|
[cc-web]: https://contributor-covenant.org/
|
||||||
|
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
|
||||||
|
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
||||||
|
[`untested` functionality tracking issue]: https://github.com/iqlusioninc/yubikey.rs/issues/280
|
||||||
|
|
||||||
|
[//]: # (github issues)
|
||||||
|
|
||||||
|
[#18]: https://github.com/iqlusioninc/yubikey.rs/issues/18
|
||||||
|
[#20]: https://github.com/iqlusioninc/yubikey.rs/issues/20
|
||||||
|
[#21]: https://github.com/iqlusioninc/yubikey.rs/issues/21
|
||||||
|
[#22]: https://github.com/iqlusioninc/yubikey.rs/issues/22
|
||||||
|
[#23]: https://github.com/iqlusioninc/yubikey.rs/issues/23
|
||||||
|
[#24]: https://github.com/iqlusioninc/yubikey.rs/issues/24
|
||||||
|
[#25]: https://github.com/iqlusioninc/yubikey.rs/issues/25
|
||||||
|
[#26]: https://github.com/iqlusioninc/yubikey.rs/issues/26
|
||||||
|
[#27]: https://github.com/iqlusioninc/yubikey.rs/issues/27
|
||||||
|
[#28]: https://github.com/iqlusioninc/yubikey.rs/issues/28
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 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).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### Changed
|
||||||
|
- MSRV is now 1.81.
|
||||||
|
|
||||||
|
## 0.7.0 (2022-11-14)
|
||||||
|
### Changed
|
||||||
|
- Bump `clap` to v4.0 ([#438])
|
||||||
|
- Bump `x509-parser` to v0.14 ([#441])
|
||||||
|
- Switch from `lazy_static` to `once_cell` ([#442])
|
||||||
|
- Switch from `subtle-encoding` to `base16ct` ([#443])
|
||||||
|
- Bump `yubikey` dependency to v0.7 ([#444])
|
||||||
|
|
||||||
|
[#438]: https://github.com/iqlusioninc/yubikey.rs/pull/438
|
||||||
|
[#441]: https://github.com/iqlusioninc/yubikey.rs/pull/441
|
||||||
|
[#442]: https://github.com/iqlusioninc/yubikey.rs/pull/442
|
||||||
|
[#443]: https://github.com/iqlusioninc/yubikey.rs/pull/443
|
||||||
|
[#444]: https://github.com/iqlusioninc/yubikey.rs/pull/444
|
||||||
|
|
||||||
|
## 0.6.0 (2022-08-10)
|
||||||
|
### Changed
|
||||||
|
- 2021 edition upgrade; MSRV 1.57 ([#343])
|
||||||
|
- Migrate from `gumdrop` to `clap` v3 ([#379])
|
||||||
|
- Bump `yubikey` dependency to v0.6 ([#403])
|
||||||
|
|
||||||
|
[#343]: https://github.com/iqlusioninc/yubikey.rs/pull/343
|
||||||
|
[#379]: https://github.com/iqlusioninc/yubikey.rs/pull/379
|
||||||
|
[#403]: https://github.com/iqlusioninc/yubikey.rs/pull/403
|
||||||
|
|
||||||
|
## 0.5.0 (2021-11-21)
|
||||||
|
### Changed
|
||||||
|
- Bump `yubikey` dependency to v0.5 ([#327])
|
||||||
|
|
||||||
|
[#327]: https://github.com/iqlusioninc/yubikey.rs/pull/327
|
||||||
|
|
||||||
|
## 0.4.0 (2021-07-12)
|
||||||
|
### Changed
|
||||||
|
- Switch to renamed `yubikey` crate ([#283])
|
||||||
|
- Bump MSRV to 1.51+ ([#283])
|
||||||
|
|
||||||
|
[#283]: https://github.com/iqlusioninc/yubikey.rs/pull/283
|
||||||
|
|
||||||
|
## 0.3.0 (2021-03-22)
|
||||||
|
### Changed
|
||||||
|
- Bump `yubikey-piv` dependency to v0.3 ([#240])
|
||||||
|
|
||||||
|
[#240]: https://github.com/iqlusioninc/yubikey.rs/pull/240
|
||||||
|
|
||||||
|
## 0.2.0 (2021-01-30)
|
||||||
|
### Changed
|
||||||
|
- Bump MSRV to 1.46+ ([#208])
|
||||||
|
- Bump `yubikey-piv` dependency to v0.2 ([#220])
|
||||||
|
|
||||||
|
[#208]: https://github.com/iqlusioninc/yubikey.rs/pull/208
|
||||||
|
[#220]: https://github.com/iqlusioninc/yubikey.rs/pull/220
|
||||||
|
|
||||||
|
## 0.1.0 (2020-10-19)
|
||||||
|
### Added
|
||||||
|
- `status` command ([#72], [#74])
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bump `yubikey-piv` to v0.1 ([#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.rs/pull/182
|
||||||
|
[#181]: https://github.com/iqlusioninc/yubikey.rs/pull/181
|
||||||
|
[#180]: https://github.com/iqlusioninc/yubikey.rs/pull/180
|
||||||
|
[#74]: https://github.com/iqlusioninc/yubikey.rs/pull/74
|
||||||
|
[#72]: https://github.com/iqlusioninc/yubikey.rs/pull/72
|
||||||
|
[#71]: https://github.com/iqlusioninc/yubikey.rs/pull/71
|
||||||
|
|
||||||
|
## 0.0.1 (2019-12-02)
|
||||||
|
- Initial release
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "yubikey-cli"
|
||||||
|
version = "0.9.0-pre"
|
||||||
|
description = """
|
||||||
|
Command-line interface for performing encryption and signing using RSA/ECC keys
|
||||||
|
stored on YubiKey devices.
|
||||||
|
"""
|
||||||
|
authors = ["Tony Arcieri <tony@iqlusion.io>"]
|
||||||
|
license = "BSD-2-Clause"
|
||||||
|
repository = "https://github.com/iqlusioninc/yubikey.rs"
|
||||||
|
readme = "README.md"
|
||||||
|
categories = ["command-line-utilities", "cryptography", "hardware-support"]
|
||||||
|
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
env_logger = "0.11"
|
||||||
|
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
||||||
|
log = "0.4"
|
||||||
|
once_cell = "1"
|
||||||
|
sha2.workspace = true
|
||||||
|
termcolor = "1"
|
||||||
|
x509-cert.workspace = true
|
||||||
|
yubikey = { version = "=0.9.0-pre.0", path = ".." }
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
<img src="https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/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.60** or newer.
|
||||||
|
|
||||||
|
## Supported YubiKeys
|
||||||
|
|
||||||
|
- [YubiKey 4] series
|
||||||
|
- [YubiKey 5] series
|
||||||
|
|
||||||
|
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
|
||||||
|
|
||||||
|
## Security Warning
|
||||||
|
|
||||||
|
No security audits of this crate have ever been performed. Presently it is in
|
||||||
|
an experimental stage and may still contain high-severity issues.
|
||||||
|
|
||||||
|
USE AT YOUR OWN RISK!
|
||||||
|
|
||||||
|
## 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-2023 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.85+-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.rs/workflows/CI/badge.svg?branch=main&event=push
|
||||||
|
[build-link]: https://github.com/iqlusioninc/yubikey.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 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/main/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 clap::Parser;
|
||||||
|
use yubikey_cli::commands::YubiKeyCli;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
YubiKeyCli::parse().run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//! Commands of the CLI application
|
||||||
|
|
||||||
|
pub mod readers;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
|
use self::{readers::ReadersCmd, status::StatusCmd};
|
||||||
|
use crate::terminal;
|
||||||
|
use clap::Parser;
|
||||||
|
use std::{env, process::exit};
|
||||||
|
use termcolor::ColorChoice;
|
||||||
|
use yubikey::{Serial, YubiKey};
|
||||||
|
|
||||||
|
/// The `yubikey` CLI utility
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct YubiKeyCli {
|
||||||
|
/// Serial number of the YubiKey to connect to
|
||||||
|
#[clap(short = 's', long = "serial")]
|
||||||
|
pub serial: Option<Serial>,
|
||||||
|
|
||||||
|
/// Subcommand to execute.
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl YubiKeyCli {
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command.run(self.yubikey_init())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, Parser)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// `version` subcommand
|
||||||
|
#[clap(about = "display version information")]
|
||||||
|
Version(VersionOpts),
|
||||||
|
|
||||||
|
/// `readers` subcommand
|
||||||
|
#[clap(about = "list detected readers")]
|
||||||
|
Readers(ReadersCmd),
|
||||||
|
|
||||||
|
/// `status` subcommand
|
||||||
|
#[clap(about = "show yubikey status")]
|
||||||
|
Status(StatusCmd),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commands {
|
||||||
|
/// Run the given command
|
||||||
|
pub fn run(&self, yubikey: YubiKey) {
|
||||||
|
match self {
|
||||||
|
Commands::Version(version) => version.run(),
|
||||||
|
Commands::Readers(list) => list.run(),
|
||||||
|
Commands::Status(status) => status.run(yubikey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version options
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
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 clap::Parser;
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
process::exit,
|
||||||
|
};
|
||||||
|
use termcolor::{ColorSpec, StandardStreamLock, WriteColor};
|
||||||
|
use yubikey::{Context, Serial};
|
||||||
|
|
||||||
|
/// The `readers` subcommand
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct ReadersCmd {}
|
||||||
|
|
||||||
|
impl ReadersCmd {
|
||||||
|
/// Run the `readers` subcommand
|
||||||
|
pub fn run(&self) {
|
||||||
|
let mut readers = Context::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,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
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,60 @@
|
|||||||
|
//! Print device status
|
||||||
|
|
||||||
|
use crate::terminal::{print_cert_info, STDOUT};
|
||||||
|
use clap::Parser;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use termcolor::{ColorSpec, StandardStreamLock, WriteColor};
|
||||||
|
use yubikey::{piv::*, YubiKey};
|
||||||
|
|
||||||
|
// String to use for `None`
|
||||||
|
const NONE_STR: &str = "<none>";
|
||||||
|
|
||||||
|
/// The `status` subcommand
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
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,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
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,13 @@
|
|||||||
|
//! `yubikey` command-line utility.
|
||||||
|
//!
|
||||||
|
//! The goal of this tool is to provide functionality similar to `yubico-piv-tool`
|
||||||
|
//! but implemented in pure Rust.
|
||||||
|
//!
|
||||||
|
//! It also serves as a demonstration/example of how to use the `yubikey` crate.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
pub mod terminal;
|
||||||
|
pub mod commands;
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
//! Status messages
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
str,
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
|
||||||
|
use x509_cert::der::Encode;
|
||||||
|
use yubikey::{certificate::Certificate, piv::*, YubiKey};
|
||||||
|
|
||||||
|
/// 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)+));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color configuration
|
||||||
|
static COLOR_CHOICE: Lazy<Mutex<Option<ColorChoice>>> = Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
/// Standard output
|
||||||
|
pub static STDOUT: Lazy<StandardStream> = Lazy::new(|| StandardStream::stdout(get_color_choice()));
|
||||||
|
|
||||||
|
/// Standard error
|
||||||
|
pub static STDERR: Lazy<StandardStream> = Lazy::new(|| 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>) -> io::Result<()> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<'_>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let cert = match Certificate::read(yubikey, slot) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error reading certificate in slot {:?}: {}", slot, e);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cert = &cert.cert;
|
||||||
|
|
||||||
|
let fingerprint = Sha256::digest(cert.to_der().unwrap());
|
||||||
|
let slot_id: u8 = slot.into();
|
||||||
|
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Algorithm",
|
||||||
|
cert.tbs_certificate()
|
||||||
|
.subject_public_key_info()
|
||||||
|
.algorithm
|
||||||
|
.oid,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
print_cert_attr(stream, "Subject", cert.tbs_certificate().subject())?;
|
||||||
|
print_cert_attr(stream, "Issuer", cert.tbs_certificate().issuer())?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Fingerprint",
|
||||||
|
hex::upper::encode_string(&fingerprint),
|
||||||
|
)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Not Before",
|
||||||
|
cert.tbs_certificate().validity().not_before,
|
||||||
|
)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Not After",
|
||||||
|
cert.tbs_certificate().validity().not_after,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a status attribute
|
||||||
|
fn print_cert_attr(
|
||||||
|
stream: &mut StandardStreamLock<'_>,
|
||||||
|
name: &str,
|
||||||
|
value: impl ToString,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
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,10 @@
|
|||||||
|
#![cfg(feature = "untested")]
|
||||||
|
|
||||||
|
use yubikey::{mgm, YubiKey};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let yubikey = YubiKey::open().unwrap();
|
||||||
|
|
||||||
|
let mut mgmt = mgm::Manager::new(yubikey).unwrap();
|
||||||
|
mgmt.enable_yubihsm().unwrap();
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+542
-42
@@ -1,66 +1,566 @@
|
|||||||
//! Application Protocol Data Unit (APDU)
|
//! Application Protocol Data Unit (APDU)
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{transaction::Transaction, Buffer, Result};
|
||||||
|
use log::trace;
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
|
/// Maximum amount of command data that can be included in an APDU
|
||||||
|
const APDU_DATA_MAX: usize = 0xFF;
|
||||||
|
|
||||||
/// Application Protocol Data Unit (APDU).
|
/// Application Protocol Data Unit (APDU).
|
||||||
///
|
///
|
||||||
/// These messages are packets used to communicate with the YubiKey using the
|
/// These messages are packets used to communicate with the YubiKey.
|
||||||
/// Chip Card Interface Device (CCID) protocol.
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
#[derive(Clone)]
|
pub(crate) struct Apdu {
|
||||||
pub struct APDU {
|
/// Instruction class: indicates the type of command (e.g. inter-industry or proprietary)
|
||||||
/// Instruction class - indicates the type of command, e.g. interindustry or proprietary
|
cla: u8,
|
||||||
pub cla: u8,
|
|
||||||
|
|
||||||
/// Instruction code - indicates the specific command, e.g. "write data"
|
/// Instruction code: indicates the specific command (e.g. "write data")
|
||||||
pub 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)
|
||||||
pub p1: u8,
|
p1: u8,
|
||||||
|
|
||||||
/// Instruction parameter 2 for the command
|
/// Instruction parameter 2 for the command
|
||||||
pub p2: u8,
|
p2: u8,
|
||||||
|
|
||||||
/// Length of command - encodes the number of bytes of command data to follow
|
/// Command data to be sent (`lc` is calculated as `data.len()`)
|
||||||
pub lc: u8,
|
data: Vec<u8>,
|
||||||
|
|
||||||
/// Command data
|
|
||||||
pub data: [u8; 255],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl APDU {
|
impl Apdu {
|
||||||
/// Get a const pointer to this APDU
|
/// Create a new APDU with the given instruction code
|
||||||
// TODO(tarcieri): eliminate pointers and use all safe references
|
pub fn new(ins: impl Into<Ins>) -> Self {
|
||||||
pub(crate) fn as_ptr(&self) -> *const APDU {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mut pointer to this APDU
|
|
||||||
// TODO(tarcieri): eliminate pointers and use all safe references
|
|
||||||
pub(crate) fn as_mut_ptr(&mut self) -> *mut APDU {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for APDU {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
cla: 0,
|
cla: 0,
|
||||||
ins: 0,
|
ins: ins.into(),
|
||||||
p1: 0,
|
p1: 0,
|
||||||
p2: 0,
|
p2: 0,
|
||||||
lc: 0,
|
data: vec![],
|
||||||
data: [0u8; 255],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set this APDU's class
|
||||||
|
pub fn cla(&mut self, value: u8) -> &mut Self {
|
||||||
|
self.cla = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this APDU's first parameter only
|
||||||
|
pub fn p1(&mut self, value: u8) -> &mut Self {
|
||||||
|
self.p1 = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set this APDU's second parameter only
|
||||||
|
pub(crate) fn p2(&mut self, value: u8) -> &mut Self {
|
||||||
|
self.p2 = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set both parameters for this APDU
|
||||||
|
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
|
||||||
|
self.p1 = p1;
|
||||||
|
self.p2 = p2;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the command data for this APDU.
|
||||||
|
///
|
||||||
|
/// Panics if the byte slice is more than 255 bytes!
|
||||||
|
pub fn data(&mut self, bytes: impl AsRef<[u8]>) -> &mut Self {
|
||||||
|
assert!(self.data.is_empty(), "APDU command already set!");
|
||||||
|
|
||||||
|
let bytes = bytes.as_ref();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
bytes.len() <= APDU_DATA_MAX,
|
||||||
|
"APDU command data too long: {} (max: {})",
|
||||||
|
bytes.len(),
|
||||||
|
APDU_DATA_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
self.data.extend_from_slice(bytes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transmit this APDU using the given card transaction.
|
||||||
|
///
|
||||||
|
/// Handles ISO 7816-4 `SW1=61` (bytes remaining) responses by issuing
|
||||||
|
/// [`Ins::GetResponseApdu`] commands until all response data is collected.
|
||||||
|
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response> {
|
||||||
|
trace!(">>> {:?}", self);
|
||||||
|
let mut response = Response::from(txn.transmit(&self.to_bytes(), recv_len)?);
|
||||||
|
trace!("<<< {:?}", &response);
|
||||||
|
|
||||||
|
if let StatusWords::BytesRemaining { .. } = response.status_words() {
|
||||||
|
let mut data = response.data().to_vec();
|
||||||
|
let mut sw = response.status_words();
|
||||||
|
|
||||||
|
while let StatusWords::BytesRemaining { .. } = sw {
|
||||||
|
let next = Response::from(
|
||||||
|
txn.transmit(&Apdu::new(Ins::GetResponseApdu).to_bytes(), recv_len)?,
|
||||||
|
);
|
||||||
|
trace!("<<< {:?}", &next);
|
||||||
|
data.extend_from_slice(next.data());
|
||||||
|
sw = next.status_words();
|
||||||
|
}
|
||||||
|
|
||||||
|
response = Response::new(sw, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize this APDU as a self-zeroizing byte buffer
|
||||||
|
pub fn to_bytes(&self) -> Buffer {
|
||||||
|
let mut bytes = Vec::with_capacity(5 + self.data.len());
|
||||||
|
bytes.push(self.cla);
|
||||||
|
bytes.push(self.ins.code());
|
||||||
|
bytes.push(self.p1);
|
||||||
|
bytes.push(self.p2);
|
||||||
|
bytes.push(self.data.len() as u8);
|
||||||
|
bytes.extend_from_slice(self.data.as_ref());
|
||||||
|
Zeroizing::new(bytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Zeroize for APDU {
|
impl Drop for Apdu {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.lc.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,
|
||||||
|
|
||||||
|
/// Get slot metadata
|
||||||
|
GetMetadata,
|
||||||
|
|
||||||
|
/// Management // Read Config
|
||||||
|
ReadConfig,
|
||||||
|
|
||||||
|
/// Management // Write Config
|
||||||
|
WriteConfig,
|
||||||
|
|
||||||
|
/// Management // DeviceReset
|
||||||
|
DeviceReset,
|
||||||
|
|
||||||
|
/// 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::GetMetadata => 0xf7,
|
||||||
|
|
||||||
|
// Management
|
||||||
|
Ins::ReadConfig => 0x1d,
|
||||||
|
Ins::WriteConfig => 0x1c,
|
||||||
|
Ins::DeviceReset => 0x1f,
|
||||||
|
|
||||||
|
Ins::Other(code) => code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for Ins {
|
||||||
|
fn from(code: u8) -> Self {
|
||||||
|
match code {
|
||||||
|
// Management
|
||||||
|
0x1d => Ins::ReadConfig,
|
||||||
|
0x1c => Ins::WriteConfig,
|
||||||
|
0x1f => Ins::DeviceReset,
|
||||||
|
|
||||||
|
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,
|
||||||
|
0xf7 => Ins::GetMetadata,
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// The requested data was too large for the response, and there is data remaining.
|
||||||
|
BytesRemaining {
|
||||||
|
/// The number of bytes remaining, as indicated in the response.
|
||||||
|
len: u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Referenced data or reference data not found
|
||||||
|
ReferenceDataNotFoundError,
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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::BytesRemaining { len } => 0x6100 | len as u16,
|
||||||
|
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::ReferenceDataNotFoundError => 0x6a88,
|
||||||
|
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,
|
||||||
|
sw if sw & 0xff00 == 0x6100 => Self::BytesRemaining {
|
||||||
|
len: (sw & 0x00ff) as u8,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
0x6a88 => StatusWords::ReferenceDataNotFoundError,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::StatusWords;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_words_round_trip() {
|
||||||
|
let round_trip = |sw: StatusWords| {
|
||||||
|
assert_eq!(StatusWords::from(sw.code()), sw);
|
||||||
|
};
|
||||||
|
|
||||||
|
round_trip(StatusWords::None);
|
||||||
|
round_trip(StatusWords::BytesRemaining { len: 1 });
|
||||||
|
round_trip(StatusWords::BytesRemaining { len: 10 });
|
||||||
|
round_trip(StatusWords::BytesRemaining { len: 0xFF });
|
||||||
|
round_trip(StatusWords::Success);
|
||||||
|
round_trip(StatusWords::NoInputDataError);
|
||||||
|
round_trip(StatusWords::VerifyFailError { tries: 0x0F });
|
||||||
|
round_trip(StatusWords::VerifyFailError { tries: 3 });
|
||||||
|
round_trip(StatusWords::VerifyFailError { tries: 2 });
|
||||||
|
round_trip(StatusWords::VerifyFailError { tries: 1 });
|
||||||
|
round_trip(StatusWords::VerifyFailError { tries: 0 });
|
||||||
|
round_trip(StatusWords::WrongLengthError);
|
||||||
|
round_trip(StatusWords::SecurityStatusError);
|
||||||
|
round_trip(StatusWords::AuthBlockedError);
|
||||||
|
round_trip(StatusWords::DataInvalidError);
|
||||||
|
round_trip(StatusWords::ConditionsNotSatisfiedError);
|
||||||
|
round_trip(StatusWords::CommandNotAllowedError);
|
||||||
|
round_trip(StatusWords::IncorrectParamError);
|
||||||
|
round_trip(StatusWords::NotFoundError);
|
||||||
|
round_trip(StatusWords::NoSpaceError);
|
||||||
|
round_trip(StatusWords::IncorrectSlotError);
|
||||||
|
round_trip(StatusWords::NotSupportedError);
|
||||||
|
round_trip(StatusWords::CommandAbortedError);
|
||||||
|
round_trip(StatusWords::Other(0x1337));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
//! Cardholder Capability Container (CCC) ID Support
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{Result, YubiKey};
|
||||||
|
use cipher::common::Generate;
|
||||||
|
use rand_core::CryptoRng;
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
|
|
||||||
|
/// CCCID offset
|
||||||
|
const CCC_ID_OFFS: usize = 9;
|
||||||
|
|
||||||
|
/// CCC Object ID
|
||||||
|
const OBJ_CAPABILITY: u32 = 0x005f_c107;
|
||||||
|
|
||||||
|
/// Cardholder Capability Container (CCC) Template
|
||||||
|
///
|
||||||
|
/// f0: Card Identifier
|
||||||
|
///
|
||||||
|
/// - 0xa000000116 == GSC-IS RID
|
||||||
|
/// - 0xff == Manufacturer ID (dummy)
|
||||||
|
/// - 0x02 == Card type (javaCard)
|
||||||
|
/// - next 14 bytes: card ID
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const CCC_TMPL: &[u8] = &[
|
||||||
|
0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4,
|
||||||
|
0x01, 0x00, 0xf5, 0x01, 0x10, 0xf6, 0x00, 0xf7, 0x00, 0xfa, 0x00, 0xfb, 0x00, 0xfc, 0x00, 0xfd,
|
||||||
|
0x00, 0xfe, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Cardholder Capability Container (CCC) Identifier Card ID.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub struct CardId(pub [u8; Self::BYTE_SIZE]);
|
||||||
|
|
||||||
|
impl CardId {
|
||||||
|
/// CCCID size in bytes
|
||||||
|
pub const BYTE_SIZE: usize = 14;
|
||||||
|
|
||||||
|
/// Generate a random CCC Card ID
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
Self(Generate::generate())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a random CCC Card ID from an [`Rng`]
|
||||||
|
pub fn generate_from_rng<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
|
||||||
|
Self(Generate::generate_from_rng(rng))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cardholder Capability Container (CCC) Identifier.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct CccId(pub [u8; Self::BYTE_SIZE]);
|
||||||
|
|
||||||
|
impl CccId {
|
||||||
|
/// CCC size in bytes
|
||||||
|
pub const BYTE_SIZE: usize = 51;
|
||||||
|
|
||||||
|
/// Return CardId component of CCC
|
||||||
|
pub fn card_id(&self) -> Result<CardId> {
|
||||||
|
let mut cccid = [0u8; CardId::BYTE_SIZE];
|
||||||
|
cccid.copy_from_slice(&self.0[CCC_ID_OFFS..(CCC_ID_OFFS + CardId::BYTE_SIZE)]);
|
||||||
|
Ok(CardId(cccid))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Cardholder Capability Container (CCC) ID
|
||||||
|
pub fn get(yubikey: &mut YubiKey) -> Result<Self> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let response = txn.fetch_object(OBJ_CAPABILITY)?;
|
||||||
|
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Cardholder Capability Container (CCC) ID
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
||||||
|
let mut buf = CCC_TMPL.to_vec();
|
||||||
|
buf[0..self.0.len()].copy_from_slice(&self.0);
|
||||||
|
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
txn.save_object(OBJ_CAPABILITY, &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for CccId {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CccId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&hex::upper::encode_string(self.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,548 @@
|
|||||||
|
//! X.509 certificate support.
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consts::CB_OBJ_MAX,
|
||||||
|
error::{Error, Result},
|
||||||
|
piv::SlotId,
|
||||||
|
serialization::*,
|
||||||
|
transaction::Transaction,
|
||||||
|
yubikey::YubiKey,
|
||||||
|
Buffer,
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
use x509_cert::{
|
||||||
|
builder::{profile::BuilderProfile, Builder, CertificateBuilder},
|
||||||
|
der::{referenced::OwnedToRef, Decode, Encode},
|
||||||
|
name::Name,
|
||||||
|
serial_number::SerialNumber,
|
||||||
|
spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
|
||||||
|
time::Validity,
|
||||||
|
};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
const TAG_CERT: u8 = 0x70;
|
||||||
|
const TAG_CERT_COMPRESS: u8 = 0x71;
|
||||||
|
const TAG_CERT_LRC: u8 = 0xFE;
|
||||||
|
|
||||||
|
/// Information about how a [`Certificate`] is stored within a YubiKey.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, 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> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Certificates
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Certificate {
|
||||||
|
/// Inner certificate
|
||||||
|
pub cert: x509_cert::Certificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Certificate {
|
||||||
|
/// Creates a new self-signed certificate for the given key. Writes the resulting
|
||||||
|
/// certificate to the slot before returning it.
|
||||||
|
///
|
||||||
|
/// `extensions` is a required argument; users who do not have any extensions
|
||||||
|
/// should set the `extensions` argument to `|_| Ok(())`.
|
||||||
|
pub fn generate_self_signed<F, KT: yubikey_signer::KeyType>(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
key: SlotId,
|
||||||
|
serial: SerialNumber,
|
||||||
|
validity: Validity,
|
||||||
|
subject: Name,
|
||||||
|
subject_pki: SubjectPublicKeyInfoOwned,
|
||||||
|
extensions: F,
|
||||||
|
) -> Result<Self>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut CertificateBuilder<SelfSigned>) -> der::Result<()>,
|
||||||
|
{
|
||||||
|
let signer =
|
||||||
|
yubikey_signer::Signer::<'_, KT>::new(yubikey, key, subject_pki.owned_to_ref())?;
|
||||||
|
let mut builder =
|
||||||
|
CertificateBuilder::new(SelfSigned { subject }, serial, validity, subject_pki)
|
||||||
|
.map_err(|_| Error::KeyError)?;
|
||||||
|
|
||||||
|
// Add custom extensions
|
||||||
|
extensions(&mut builder)?;
|
||||||
|
|
||||||
|
let cert = builder.build(&signer).map_err(|_| Error::KeyError)?;
|
||||||
|
let cert = Self { cert };
|
||||||
|
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
||||||
|
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a certificate from the given slot in the YubiKey
|
||||||
|
pub fn read(yubikey: &mut YubiKey, slot: SlotId) -> Result<Self> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let buf = read_certificate(&txn, slot)?;
|
||||||
|
|
||||||
|
if buf.is_empty() {
|
||||||
|
return Err(Error::InvalidObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::from_bytes(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write this certificate into the YubiKey in the given slot
|
||||||
|
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let data = self.cert.to_der().map_err(|_| Error::InvalidObject)?;
|
||||||
|
write_certificate(&txn, slot, Some(&data), certinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a certificate located at the given slot of the given YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
write_certificate(&txn, slot, None, CertInfo::Uncompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a local certificate struct from the given bytebuffer
|
||||||
|
pub fn from_bytes(cert: impl Into<Buffer>) -> Result<Self> {
|
||||||
|
let cert = cert.into();
|
||||||
|
|
||||||
|
if cert.is_empty() {
|
||||||
|
error!("certificate cannot be empty");
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
x509_cert::Certificate::from_der(&cert)
|
||||||
|
.map(|cert| Self { cert })
|
||||||
|
.map_err(|_| Error::InvalidObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Issuer field of the certificate.
|
||||||
|
pub fn issuer(&self) -> String {
|
||||||
|
self.cert.tbs_certificate().issuer().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the SubjectName field of the certificate.
|
||||||
|
pub fn subject(&self) -> String {
|
||||||
|
self.cert.tbs_certificate().subject().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the SubjectPublicKeyInfo field of the certificate.
|
||||||
|
pub fn subject_pki(&self) -> SubjectPublicKeyInfoRef<'_> {
|
||||||
|
self.cert
|
||||||
|
.tbs_certificate()
|
||||||
|
.subject_public_key_info()
|
||||||
|
.owned_to_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`BuilderProfile`] for self-signed certificates.
|
||||||
|
///
|
||||||
|
/// This profile has no default extensions.
|
||||||
|
pub struct SelfSigned {
|
||||||
|
subject: Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuilderProfile for SelfSigned {
|
||||||
|
fn get_issuer(&self, subject: &Name) -> Name {
|
||||||
|
// RFC 5280 Section 3.2:
|
||||||
|
//
|
||||||
|
// > Self-issued certificates are CA certificates in which the issuer and subject
|
||||||
|
// > are the same entity. [..] Self-signed certificates are self-issued
|
||||||
|
// > certificates where the digital signature may be verified by the public key
|
||||||
|
// > bound into the certificate.
|
||||||
|
subject.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_subject(&self) -> Name {
|
||||||
|
self.subject.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_extensions(
|
||||||
|
&self,
|
||||||
|
_spk: SubjectPublicKeyInfoRef<'_>,
|
||||||
|
_issuer_spk: SubjectPublicKeyInfoRef<'_>,
|
||||||
|
_tbs: &x509_cert::TbsCertificate,
|
||||||
|
) -> x509_cert::builder::Result<Vec<x509_cert::ext::Extension>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read certificate
|
||||||
|
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer> {
|
||||||
|
let object_id = slot.object_id();
|
||||||
|
|
||||||
|
let buf = match txn.fetch_object(object_id) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => {
|
||||||
|
// TODO(tarcieri): is this really ok?
|
||||||
|
return Ok(Zeroizing::new(vec![]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(str4d): Check the rest of the buffer (TAG_CERT_COMPRESS and TAG_CERT_LRC)
|
||||||
|
if buf[0] == TAG_CERT {
|
||||||
|
Tlv::parse_single(buf, TAG_CERT).or_else(|_| {
|
||||||
|
// TODO(tarcieri): is this really ok?
|
||||||
|
Ok(Zeroizing::new(vec![]))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write certificate
|
||||||
|
pub(crate) fn write_certificate(
|
||||||
|
txn: &Transaction<'_>,
|
||||||
|
slot: SlotId,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
certinfo: CertInfo,
|
||||||
|
) -> Result<()> {
|
||||||
|
let object_id = slot.object_id();
|
||||||
|
|
||||||
|
if let Some(data) = data {
|
||||||
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
|
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
|
||||||
|
|
||||||
|
// write compression info and LRC trailer
|
||||||
|
offset += Tlv::write(&mut buf[offset..], TAG_CERT_COMPRESS, &[certinfo.into()])?;
|
||||||
|
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
|
||||||
|
|
||||||
|
txn.save_object(object_id, &buf[..offset])
|
||||||
|
} else {
|
||||||
|
txn.save_object(object_id, &[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod yubikey_signer {
|
||||||
|
//! Signer implementation for yubikey
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::{Error, Result},
|
||||||
|
piv::AlgorithmId,
|
||||||
|
piv::{sign_data, SlotId},
|
||||||
|
YubiKey,
|
||||||
|
};
|
||||||
|
use der::{
|
||||||
|
asn1::{Any, OctetString},
|
||||||
|
oid::db::rfc5912,
|
||||||
|
Encode, Sequence,
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256, Sha384, Sha512};
|
||||||
|
use signature::Keypair;
|
||||||
|
use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
|
||||||
|
use x509_cert::spki::{
|
||||||
|
self, AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier, EncodePublicKey,
|
||||||
|
SignatureBitStringEncoding, SubjectPublicKeyInfoRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
type SigResult<T> = core::result::Result<T, signature::Error>;
|
||||||
|
|
||||||
|
/// Key to be used to sign certificates
|
||||||
|
pub trait KeyType {
|
||||||
|
/// Error returned when working with signature
|
||||||
|
type Error: Into<signature::Error> + fmt::Debug;
|
||||||
|
/// The signature type returned by the signer
|
||||||
|
type Signature: SignatureBitStringEncoding
|
||||||
|
+ for<'s> TryFrom<&'s [u8], Error = Self::Error>
|
||||||
|
+ fmt::Debug;
|
||||||
|
/// The public key used to verify signature
|
||||||
|
type VerifyingKey: EncodePublicKey
|
||||||
|
+ DynSignatureAlgorithmIdentifier
|
||||||
|
+ Clone
|
||||||
|
+ From<Self::PublicKey>;
|
||||||
|
/// Public key type used to load the SPKI formatted key
|
||||||
|
type PublicKey: for<'a> TryFrom<SubjectPublicKeyInfoRef<'a>, Error = spki::Error>;
|
||||||
|
|
||||||
|
/// The algorithm used when talking with the yubikey
|
||||||
|
const ALGORITHM: AlgorithmId;
|
||||||
|
|
||||||
|
/// Prepare buffer before submitting it for signature
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>>;
|
||||||
|
|
||||||
|
/// Prepare a prehashed message before submitting it for signature
|
||||||
|
fn prepare_prehash(hashed: &[u8]) -> SigResult<Vec<u8>>;
|
||||||
|
|
||||||
|
/// Read back the signature from the device
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyType for ed25519_dalek::SigningKey {
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
|
||||||
|
type Error = ed25519_dalek::SignatureError;
|
||||||
|
type Signature = ed25519_dalek::Signature;
|
||||||
|
type VerifyingKey = ed25519_dalek::VerifyingKey;
|
||||||
|
type PublicKey = ed25519_dalek::VerifyingKey;
|
||||||
|
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(Sha512::digest(input).to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
|
Self::Signature::try_from(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyType for p256::NistP256 {
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::EccP256;
|
||||||
|
type Error = ecdsa::Error;
|
||||||
|
type Signature = p256::ecdsa::DerSignature;
|
||||||
|
type VerifyingKey = p256::ecdsa::VerifyingKey;
|
||||||
|
type PublicKey = p256::ecdsa::VerifyingKey;
|
||||||
|
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(Sha256::digest(input).to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_prehash(hashed: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(hashed.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
|
Self::Signature::from_bytes(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyType for p384::NistP384 {
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::EccP384;
|
||||||
|
type Error = ecdsa::Error;
|
||||||
|
type Signature = p384::ecdsa::DerSignature;
|
||||||
|
type VerifyingKey = p384::ecdsa::VerifyingKey;
|
||||||
|
type PublicKey = p384::ecdsa::VerifyingKey;
|
||||||
|
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(Sha384::digest(input).to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_prehash(hashed: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(hashed.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
|
Self::Signature::from_bytes(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait used to handle subtypes of RSA keys
|
||||||
|
pub trait RsaLength {
|
||||||
|
/// The length of the RSA key in bits
|
||||||
|
const BIT_LENGTH: usize;
|
||||||
|
/// The algorithm to use when talking with the Yubikey.
|
||||||
|
const ALGORITHM: AlgorithmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSA 1024 bits key
|
||||||
|
pub struct Rsa1024;
|
||||||
|
|
||||||
|
impl RsaLength for Rsa1024 {
|
||||||
|
const BIT_LENGTH: usize = 1024;
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSA 2048 bits key
|
||||||
|
pub struct Rsa2048;
|
||||||
|
|
||||||
|
impl RsaLength for Rsa2048 {
|
||||||
|
const BIT_LENGTH: usize = 2048;
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa2048;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSA 3072 bits key
|
||||||
|
pub struct Rsa3072;
|
||||||
|
|
||||||
|
impl RsaLength for Rsa3072 {
|
||||||
|
const BIT_LENGTH: usize = 3072;
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa3072;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSA 4096 bits key
|
||||||
|
pub struct Rsa4096;
|
||||||
|
|
||||||
|
impl RsaLength for Rsa4096 {
|
||||||
|
const BIT_LENGTH: usize = 4096;
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RSA keys used to sign certificates
|
||||||
|
pub struct YubiRsa<N: RsaLength> {
|
||||||
|
_len: PhantomData<N>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: RsaLength> KeyType for YubiRsa<N> {
|
||||||
|
type Error = signature::Error;
|
||||||
|
type Signature = rsa::pkcs1v15::Signature;
|
||||||
|
type VerifyingKey = rsa::pkcs1v15::VerifyingKey<Sha256>;
|
||||||
|
type PublicKey = rsa::RsaPublicKey;
|
||||||
|
const ALGORITHM: AlgorithmId = N::ALGORITHM;
|
||||||
|
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
let hashed = Sha256::digest(input).to_vec();
|
||||||
|
Self::prepare_prehash(&hashed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_prehash(hashed: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
OctetString::new(hashed)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
.and_then(Self::emsa_pkcs1_1_5)
|
||||||
|
.map_err(signature::Error::from_source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
|
Self::Signature::try_from(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: RsaLength> YubiRsa<N> {
|
||||||
|
/// https://www.rfc-editor.org/rfc/rfc8017#section-9.2
|
||||||
|
fn emsa_pkcs1_1_5(digest: OctetString) -> Result<Vec<u8>> {
|
||||||
|
/// https://www.rfc-editor.org/rfc/rfc8017#appendix-A.2.4
|
||||||
|
#[derive(Debug, Sequence)]
|
||||||
|
struct DigestInfo {
|
||||||
|
digest_algorithm: AlgorithmIdentifierOwned,
|
||||||
|
digest: OctetString,
|
||||||
|
}
|
||||||
|
|
||||||
|
let em_len = N::BIT_LENGTH / 8;
|
||||||
|
|
||||||
|
let null = Any::null();
|
||||||
|
|
||||||
|
let t = DigestInfo {
|
||||||
|
digest_algorithm: AlgorithmIdentifierOwned {
|
||||||
|
oid: rfc5912::ID_SHA_256,
|
||||||
|
parameters: Some(null),
|
||||||
|
},
|
||||||
|
digest,
|
||||||
|
};
|
||||||
|
let t = t.to_der()?;
|
||||||
|
|
||||||
|
let ps = vec![0xff; em_len - t.len() - 3];
|
||||||
|
assert!(ps.len() >= 8, "spec violation");
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(em_len);
|
||||||
|
out.write(&[0x00, 0x01]).map_err(|_| Error::MemoryError)?;
|
||||||
|
out.write(&ps).map_err(|_| Error::MemoryError)?;
|
||||||
|
out.write(&[0x00]).map_err(|_| Error::MemoryError)?;
|
||||||
|
out.write(&t).map_err(|_| Error::MemoryError)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entrypoint to sign data with the yubikey.
|
||||||
|
pub struct Signer<'y, KT: KeyType> {
|
||||||
|
yubikey: RefCell<&'y mut YubiKey>,
|
||||||
|
key: SlotId,
|
||||||
|
public_key: KT::VerifyingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'y, KT: KeyType> Signer<'y, KT> {
|
||||||
|
/// Create new Signer
|
||||||
|
pub fn new(
|
||||||
|
yubikey: &'y mut YubiKey,
|
||||||
|
key: SlotId,
|
||||||
|
subject_pki: SubjectPublicKeyInfoRef<'_>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let public_key = KT::PublicKey::try_from(subject_pki).map_err(|_| Error::ParseError)?;
|
||||||
|
let public_key = public_key.into();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
yubikey: RefCell::new(yubikey),
|
||||||
|
key,
|
||||||
|
public_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<KT: KeyType> Keypair for Signer<'_, KT> {
|
||||||
|
type VerifyingKey = KT::VerifyingKey;
|
||||||
|
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
|
||||||
|
self.public_key.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'_, KT> {
|
||||||
|
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
|
||||||
|
self.verifying_key().signature_algorithm_identifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<KT: KeyType> signature::Signer<KT::Signature> for Signer<'_, KT> {
|
||||||
|
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
|
||||||
|
let data = KT::prepare(msg)?;
|
||||||
|
|
||||||
|
let out = sign_data(
|
||||||
|
&mut self.yubikey.borrow_mut(),
|
||||||
|
&data,
|
||||||
|
KT::ALGORITHM,
|
||||||
|
self.key,
|
||||||
|
)
|
||||||
|
.map_err(signature::Error::from_source)?;
|
||||||
|
let out = KT::read_signature(&out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<KT: KeyType> signature::hazmat::PrehashSigner<KT::Signature> for Signer<'_, KT> {
|
||||||
|
fn sign_prehash(&self, hashed: &[u8]) -> SigResult<KT::Signature> {
|
||||||
|
let data = KT::prepare_prehash(hashed)?;
|
||||||
|
|
||||||
|
let out = sign_data(
|
||||||
|
&mut self.yubikey.borrow_mut(),
|
||||||
|
&data,
|
||||||
|
KT::ALGORITHM,
|
||||||
|
self.key,
|
||||||
|
)
|
||||||
|
.map_err(signature::Error::from_source)?;
|
||||||
|
let out = KT::read_signature(&out)?;
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
//! Cardholder Unique Identifier (CHUID) Support
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{Result, YubiKey};
|
||||||
|
use std::fmt::{self, Debug, Display};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
///
|
||||||
|
/// Format defined in SP-800-73-4, Appendix A, Table 9
|
||||||
|
///
|
||||||
|
/// FASC-N containing S9999F9999F999999F0F1F0000000000300001E encoded in
|
||||||
|
/// 4-bit BCD with 1 bit parity. run through the tools/fasc.pl script to get
|
||||||
|
/// bytes. This CHUID has an expiry of 2030-01-01.
|
||||||
|
///
|
||||||
|
/// Defined fields:
|
||||||
|
///
|
||||||
|
/// - 0x30: FASC-N (hard-coded)
|
||||||
|
/// - 0x34: Card UUID / GUID (settable)
|
||||||
|
/// - 0x35: Exp. Date (hard-coded)
|
||||||
|
/// - 0x3e: Signature (hard-coded, empty)
|
||||||
|
/// - 0xfe: Error Detection Code (hard-coded)
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const CHUID_TMPL: &[u8] = &[
|
||||||
|
0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58,
|
||||||
|
0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x08, 0x32,
|
||||||
|
0x30, 0x33, 0x30, 0x30, 0x31, 0x30, 0x31, 0x3e, 0x00, 0xfe, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Cardholder Unique Identifier (CHUID).
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct ChuId(pub [u8; Self::BYTE_SIZE]);
|
||||||
|
|
||||||
|
impl ChuId {
|
||||||
|
/// CHUID size in bytes
|
||||||
|
pub const BYTE_SIZE: usize = 59;
|
||||||
|
|
||||||
|
/// FASC-N component size
|
||||||
|
pub const FASCN_SIZE: usize = 25;
|
||||||
|
|
||||||
|
/// Expiration size
|
||||||
|
pub const EXPIRATION_SIZE: usize = 8;
|
||||||
|
|
||||||
|
/// Return FASC-N component of CHUID
|
||||||
|
pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] {
|
||||||
|
self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)]
|
||||||
|
.try_into()
|
||||||
|
.expect("should be FASCN_SIZE")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return Card UUID/GUID component of CHUID
|
||||||
|
pub fn uuid(&self) -> Uuid {
|
||||||
|
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)])
|
||||||
|
.expect("should be UUID-sized")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return expiration date component of CHUID
|
||||||
|
// TODO(tarcieri): parse expiration?
|
||||||
|
pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] {
|
||||||
|
self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)]
|
||||||
|
.try_into()
|
||||||
|
.expect("should be EXPIRATION_SIZE")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Cardholder Unique Identifier (CHUID)
|
||||||
|
pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let response = txn.fetch_object(OBJ_CHUID)?;
|
||||||
|
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Cardholder Unique Identifier (CHUID)
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
||||||
|
let mut buf = CHUID_TMPL.to_vec();
|
||||||
|
buf[..Self::BYTE_SIZE].copy_from_slice(&self.0);
|
||||||
|
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
txn.save_object(OBJ_CHUID, &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for ChuId {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ChuId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&hex::upper::encode_string(self.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
//! YubiKey Configuration Values
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consts::{
|
||||||
|
TAG_ADMIN_FLAGS_1, TAG_ADMIN_SALT, TAG_ADMIN_TIMESTAMP, TAG_PROTECTED_FLAGS_1,
|
||||||
|
TAG_PROTECTED_MGM,
|
||||||
|
},
|
||||||
|
metadata::{AdminData, ProtectedData},
|
||||||
|
mgm::{MgmType, ADMIN_FLAGS_1_PROTECTED_MGM},
|
||||||
|
yubikey::{YubiKey, ADMIN_FLAGS_1_PUK_BLOCKED},
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const CB_ADMIN_TIMESTAMP: usize = 0x04;
|
||||||
|
const PROTECTED_FLAGS_1_PUK_NOBLOCK: u8 = 0x01;
|
||||||
|
|
||||||
|
/// YubiKey configuration.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
/// Protected data available
|
||||||
|
pub protected_data_available: bool,
|
||||||
|
|
||||||
|
/// PUK blocked
|
||||||
|
pub puk_blocked: bool,
|
||||||
|
|
||||||
|
/// No block on upgrade
|
||||||
|
pub puk_noblock_on_upgrade: bool,
|
||||||
|
|
||||||
|
/// PIN last changed
|
||||||
|
pub pin_last_changed: Option<SystemTime>,
|
||||||
|
|
||||||
|
/// MGM type
|
||||||
|
pub mgm_type: MgmType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Config {
|
||||||
|
Config {
|
||||||
|
protected_data_available: false,
|
||||||
|
puk_blocked: false,
|
||||||
|
puk_noblock_on_upgrade: false,
|
||||||
|
pin_last_changed: None,
|
||||||
|
mgm_type: MgmType::Manual,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Get YubiKey config.
|
||||||
|
pub(crate) fn get(yubikey: &mut YubiKey) -> Result<Config> {
|
||||||
|
let mut config = Self::default();
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
if let Ok(admin_data) = AdminData::read(&txn) {
|
||||||
|
if let Ok(item) = admin_data.get_item(TAG_ADMIN_FLAGS_1) {
|
||||||
|
if item.is_empty() {
|
||||||
|
error!("empty response for admin flags metadata item! ignoring");
|
||||||
|
} else {
|
||||||
|
if item[0] & ADMIN_FLAGS_1_PUK_BLOCKED != 0 {
|
||||||
|
config.puk_blocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if item[0] & ADMIN_FLAGS_1_PROTECTED_MGM != 0 {
|
||||||
|
config.mgm_type = MgmType::Protected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if admin_data.get_item(TAG_ADMIN_SALT).is_ok() {
|
||||||
|
if config.mgm_type != MgmType::Manual {
|
||||||
|
error!("conflicting types of MGM key administration configured");
|
||||||
|
} else {
|
||||||
|
config.mgm_type = MgmType::Derived;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(item) = admin_data.get_item(TAG_ADMIN_TIMESTAMP) {
|
||||||
|
if item.len() != CB_ADMIN_TIMESTAMP {
|
||||||
|
error!("pin timestamp in admin metadata is an invalid size");
|
||||||
|
} else {
|
||||||
|
// TODO(tarcieri): double-check endianness is correct
|
||||||
|
let pin_last_changed = u32::from_le_bytes([item[0], item[1], item[2], item[3]]);
|
||||||
|
|
||||||
|
if pin_last_changed != 0 {
|
||||||
|
config.pin_last_changed =
|
||||||
|
Some(UNIX_EPOCH + Duration::from_secs(pin_last_changed as u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(protected_data) = ProtectedData::read(&txn) {
|
||||||
|
config.protected_data_available = true;
|
||||||
|
|
||||||
|
if let Ok(item) = protected_data.get_item(TAG_PROTECTED_FLAGS_1) {
|
||||||
|
if item.is_empty() {
|
||||||
|
error!("empty response for protected flags metadata item! ignoring");
|
||||||
|
} else if item[0] & PROTECTED_FLAGS_1_PUK_NOBLOCK != 0 {
|
||||||
|
config.puk_noblock_on_upgrade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if protected_data.get_item(TAG_PROTECTED_MGM).is_ok() {
|
||||||
|
if config.mgm_type != MgmType::Protected {
|
||||||
|
error!("conflicting MGM key types: protected MGM exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always favor protected MGM
|
||||||
|
config.mgm_type = MgmType::Protected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-211
@@ -1,218 +1,37 @@
|
|||||||
//! Constant values
|
//! Miscellaneous constant values
|
||||||
// TODO(tarcieri): refactor these into enums!
|
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
#![allow(dead_code)]
|
||||||
// <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!
|
/// YubiKey max buffer size
|
||||||
#![allow(missing_docs, non_upper_case_globals)]
|
pub(crate) const CB_BUF_MAX: usize = 3072;
|
||||||
|
|
||||||
pub const szLOG_SOURCE: &str = "yubikey-piv.rs";
|
/// YubiKey max object size
|
||||||
|
pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9;
|
||||||
|
|
||||||
pub const CB_OBJ_MAX: usize = 3063;
|
pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
||||||
|
pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len
|
||||||
|
|
||||||
pub const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
// Admin tags
|
||||||
pub const CB_OBJ_TAG_MAX: usize = (CB_OBJ_TAG_MIN + 2); // 1 byte tag + 3 bytes len
|
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 const CB_PIN_MAX: usize = 8;
|
// Protected tags
|
||||||
pub const CB_ECC_POINTP256: usize = 65;
|
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
||||||
pub const CB_ECC_POINTP384: usize = 97;
|
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
||||||
|
|
||||||
pub const CHUID_GUID_OFFS: usize = 29;
|
// Management
|
||||||
|
pub(crate) const TAG_USB_SUPPORTED: u8 = 0x01;
|
||||||
pub const CHREF_ACT_CHANGE_PIN: i32 = 0;
|
pub(crate) const TAG_SERIAL: u8 = 0x02;
|
||||||
pub const CHREF_ACT_UNBLOCK_PIN: i32 = 1;
|
pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
|
||||||
pub const CHREF_ACT_CHANGE_PUK: i32 = 2;
|
pub(crate) const TAG_FORM_FACTOR: u8 = 0x04;
|
||||||
|
pub(crate) const TAG_VERSION: u8 = 0x05;
|
||||||
pub const DES_TYPE_3DES: u8 = 1;
|
pub(crate) const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
|
||||||
|
pub(crate) const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
|
||||||
pub const DES_LEN_DES: usize = 8;
|
pub(crate) const TAG_DEVICE_FLAGS: u8 = 0x08;
|
||||||
pub const DES_LEN_3DES: usize = DES_LEN_DES * 3;
|
pub(crate) const TAG_APP_VERSIONS: u8 = 0x09;
|
||||||
|
pub(crate) const TAG_CONFIG_LOCK: u8 = 0x0A;
|
||||||
// device types
|
pub(crate) const TAG_UNLOCK: u8 = 0x0B;
|
||||||
|
pub(crate) const TAG_REBOOT: u8 = 0x0C;
|
||||||
pub const DEVTYPE_UNKNOWN: u32 = 0x0000_0000;
|
pub(crate) const TAG_NFC_SUPPORTED: u8 = 0x0D;
|
||||||
pub const DEVTYPE_NEO: u32 = 0x4E45_0000; //"NE"
|
pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
|
||||||
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"
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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_OBJ_MAX_SIZE: usize = 3072;
|
|
||||||
|
|
||||||
pub const YKPIV_PINPOLICY_TAG: u8 = 0xaa;
|
|
||||||
pub const YKPIV_PINPOLICY_DEFAULT: u8 = 0;
|
|
||||||
pub const YKPIV_PINPOLICY_NEVER: u8 = 1;
|
|
||||||
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;
|
|
||||||
|
|||||||
+143
-94
@@ -30,131 +30,180 @@
|
|||||||
// (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 std::fmt;
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
/// Kinds of errors
|
/// Result type with [`Error`].
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// Kinds of errors.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ErrorKind {
|
#[non_exhaustive]
|
||||||
/// OK
|
pub enum Error {
|
||||||
// TODO(tarcieri): replace this with proper result types
|
/// Algorithm error
|
||||||
Ok,
|
AlgorithmError,
|
||||||
|
|
||||||
/// Memory error
|
|
||||||
MemoryError,
|
|
||||||
|
|
||||||
/// PCSC error
|
|
||||||
PcscError,
|
|
||||||
|
|
||||||
/// Size error
|
|
||||||
SizeError,
|
|
||||||
|
|
||||||
/// Applet error
|
/// Applet error
|
||||||
AppletError,
|
AppletError,
|
||||||
|
|
||||||
/// Authentication error
|
/// We tried to select an applet that could not be found.
|
||||||
AuthenticationError,
|
AppletNotFound {
|
||||||
|
/// Human-readable name of the applet.
|
||||||
/// Randomness error
|
applet_name: &'static str,
|
||||||
RandomnessError,
|
},
|
||||||
|
|
||||||
/// Generic error
|
|
||||||
GenericError,
|
|
||||||
|
|
||||||
/// Key error
|
|
||||||
KeyError,
|
|
||||||
|
|
||||||
/// Parse error
|
|
||||||
ParseError,
|
|
||||||
|
|
||||||
/// Wrong PIN
|
|
||||||
WrongPin,
|
|
||||||
|
|
||||||
/// Invalid object
|
|
||||||
InvalidObject,
|
|
||||||
|
|
||||||
/// Algorithm error
|
|
||||||
AlgorithmError,
|
|
||||||
|
|
||||||
/// PIN locked
|
|
||||||
PinLocked,
|
|
||||||
|
|
||||||
/// Argument error
|
/// Argument error
|
||||||
ArgumentError,
|
ArgumentError,
|
||||||
|
|
||||||
/// Range error
|
/// Authentication error
|
||||||
RangeError,
|
AuthenticationError,
|
||||||
|
|
||||||
|
/// Error while building a certificate
|
||||||
|
CertificateBuilder,
|
||||||
|
|
||||||
|
/// Generic error
|
||||||
|
GenericError,
|
||||||
|
|
||||||
|
/// Invalid object
|
||||||
|
InvalidObject,
|
||||||
|
|
||||||
|
/// Key error
|
||||||
|
KeyError,
|
||||||
|
|
||||||
|
/// Memory error
|
||||||
|
MemoryError,
|
||||||
|
|
||||||
/// Not supported
|
/// Not supported
|
||||||
NotSupported,
|
NotSupported,
|
||||||
|
|
||||||
|
/// Not found
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// Parse error
|
||||||
|
ParseError,
|
||||||
|
|
||||||
|
/// PCSC error
|
||||||
|
PcscError {
|
||||||
|
/// Original PC/SC error
|
||||||
|
inner: Option<pcsc::Error>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// PIN locked
|
||||||
|
PinLocked,
|
||||||
|
|
||||||
|
/// Range error
|
||||||
|
RangeError,
|
||||||
|
|
||||||
|
/// Size error
|
||||||
|
SizeError,
|
||||||
|
|
||||||
|
/// Wrong PIN
|
||||||
|
WrongPin {
|
||||||
|
/// Number of tries remaining
|
||||||
|
tries: u8,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorKind {
|
impl Error {
|
||||||
/// Name of the error.
|
/// Name of the error.
|
||||||
///
|
///
|
||||||
/// These names map to the legacy names from the Yubico C library, to
|
/// These names map to the legacy names from the Yubico C library, to
|
||||||
/// assist in web searches for relevant information for these errors.
|
/// assist in web searches for relevant information for these errors.
|
||||||
pub fn name(self) -> &'static str {
|
pub fn name(self) -> Option<&'static str> {
|
||||||
match self {
|
Some(match self {
|
||||||
ErrorKind::Ok => "YKPIV_OK",
|
Error::AlgorithmError => "YKPIV_ALGORITHM_ERROR",
|
||||||
ErrorKind::MemoryError => "YKPIV_MEMORY_ERROR",
|
Error::AppletError => "YKPIV_APPLET_ERROR",
|
||||||
ErrorKind::PcscError => "YKPIV_PCSC_ERROR",
|
Error::ArgumentError => "YKPIV_ARGUMENT_ERROR",
|
||||||
ErrorKind::SizeError => "YKPIV_SIZE_ERROR",
|
Error::AuthenticationError => "YKPIV_AUTHENTICATION_ERROR",
|
||||||
ErrorKind::AppletError => "YKPIV_APPLET_ERROR",
|
Error::GenericError => "YKPIV_GENERIC_ERROR",
|
||||||
ErrorKind::AuthenticationError => "YKPIV_AUTHENTICATION_ERROR",
|
Error::InvalidObject => "YKPIV_INVALID_OBJECT",
|
||||||
ErrorKind::RandomnessError => "YKPIV_RANDOMNESS_ERROR",
|
Error::KeyError => "YKPIV_KEY_ERROR",
|
||||||
ErrorKind::GenericError => "YKPIV_GENERIC_ERROR",
|
Error::MemoryError => "YKPIV_MEMORY_ERROR",
|
||||||
ErrorKind::KeyError => "YKPIV_KEY_ERROR",
|
Error::NotSupported => "YKPIV_NOT_SUPPORTED",
|
||||||
ErrorKind::ParseError => "YKPIV_PARSE_ERROR",
|
Error::ParseError => "YKPIV_PARSE_ERROR",
|
||||||
ErrorKind::WrongPin => "YKPIV_WRONG_PIN",
|
Error::PcscError { .. } => "YKPIV_PCSC_ERROR",
|
||||||
ErrorKind::InvalidObject => "YKPIV_INVALID_OBJECT",
|
Error::PinLocked => "YKPIV_PIN_LOCKED",
|
||||||
ErrorKind::AlgorithmError => "YKPIV_ALGORITHM_ERROR",
|
Error::RangeError => "YKPIV_RANGE_ERROR",
|
||||||
ErrorKind::PinLocked => "YKPIV_PIN_LOCKED",
|
Error::SizeError => "YKPIV_SIZE_ERROR",
|
||||||
ErrorKind::ArgumentError => "YKPIV_ARGUMENT_ERROR",
|
Error::WrongPin { .. } => "YKPIV_WRONG_PIN",
|
||||||
ErrorKind::RangeError => "YKPIV_RANGE_ERROR",
|
_ => return None,
|
||||||
ErrorKind::NotSupported => "YKPIV_NOT_SUPPORTED",
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error message
|
/// Error message
|
||||||
pub fn msg(self) -> &'static str {
|
pub fn msg(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ErrorKind::Ok => "OK",
|
Error::AlgorithmError => f.write_str("algorithm error"),
|
||||||
ErrorKind::MemoryError => "memory error",
|
Error::AppletError => f.write_str("applet error"),
|
||||||
ErrorKind::PcscError => "PCSC error",
|
Error::AppletNotFound { applet_name } => {
|
||||||
ErrorKind::SizeError => "size error",
|
f.write_str(&format!("{applet_name} applet not found"))
|
||||||
ErrorKind::AppletError => "applet error",
|
}
|
||||||
ErrorKind::AuthenticationError => "authentication error",
|
Error::ArgumentError => f.write_str("argument error"),
|
||||||
ErrorKind::RandomnessError => "randomness error",
|
Error::AuthenticationError => f.write_str("authentication error"),
|
||||||
ErrorKind::GenericError => "generic error",
|
Error::CertificateBuilder => f.write_str("certificate builder error"),
|
||||||
ErrorKind::KeyError => "key error",
|
Error::GenericError => f.write_str("generic error"),
|
||||||
ErrorKind::ParseError => "parse error",
|
Error::InvalidObject => f.write_str("invalid object"),
|
||||||
ErrorKind::WrongPin => "wrong pin",
|
Error::KeyError => f.write_str("key error"),
|
||||||
ErrorKind::InvalidObject => "invalid object",
|
Error::MemoryError => f.write_str("memory error"),
|
||||||
ErrorKind::AlgorithmError => "algorithm error",
|
Error::NotSupported => f.write_str("not supported"),
|
||||||
ErrorKind::PinLocked => "PIN locked",
|
Error::NotFound => f.write_str("not found"),
|
||||||
ErrorKind::ArgumentError => "argument error",
|
Error::ParseError => f.write_str("parse error"),
|
||||||
ErrorKind::RangeError => "range error",
|
|
||||||
ErrorKind::NotSupported => "not supported",
|
Error::PcscError {
|
||||||
|
inner: Some(pcsc_error),
|
||||||
|
} => f.write_fmt(format_args!("PC/SC error: {pcsc_error}")),
|
||||||
|
|
||||||
|
Error::PcscError { .. } => f.write_str("PC/SC error"),
|
||||||
|
|
||||||
|
Error::PinLocked => f.write_str("PIN locked"),
|
||||||
|
Error::RangeError => f.write_str("range error"),
|
||||||
|
Error::SizeError => f.write_str("size error"),
|
||||||
|
Error::WrongPin { .. } => f.write_str("wrong pin"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ErrorKind {
|
impl Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.msg())
|
self.msg(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for ErrorKind {}
|
impl From<std::array::TryFromSliceError> for Error {
|
||||||
|
fn from(_: std::array::TryFromSliceError) -> Error {
|
||||||
/// Get a string representation of this error
|
Error::SizeError
|
||||||
// TODO(tarcieri): completely replace this with `Display`
|
}
|
||||||
pub fn ykpiv_strerror(err: ErrorKind) -> &'static str {
|
|
||||||
err.msg()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the name of this error
|
impl From<std::time::SystemTimeError> for Error {
|
||||||
// TODO(tarcieri): completely replace this with debug
|
fn from(_: std::time::SystemTimeError) -> Error {
|
||||||
pub fn ykpiv_strerror_name(err: ErrorKind) -> &'static str {
|
Error::GenericError
|
||||||
err.name()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<pcsc::Error> for Error {
|
||||||
|
fn from(err: pcsc::Error) -> Error {
|
||||||
|
Error::PcscError { inner: Some(err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
#[allow(trivial_casts)]
|
||||||
|
Error::PcscError { inner } => inner.as_ref().map(|err| err as &_),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<der::Error> for Error {
|
||||||
|
fn from(_err: der::Error) -> Error {
|
||||||
|
Error::ParseError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<x509_cert::builder::Error> for Error {
|
||||||
|
fn from(_err: x509_cert::builder::Error) -> Error {
|
||||||
|
Error::CertificateBuilder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-427
@@ -1,427 +0,0 @@
|
|||||||
//! Internal functions (mostly 3DES)
|
|
||||||
|
|
||||||
// TODO(tarcieri): replace OpenSSL extern "C" invocations with `des` crate
|
|
||||||
// - crate: https://crates.io/crates/des
|
|
||||||
// - docs: https://docs.rs/des/0
|
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
|
||||||
// <https://github.com/Yubico/yubico-piv-tool/>
|
|
||||||
//
|
|
||||||
// Copyright (c) 2014-2016 Yubico AB
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Redistribution and use in source and binary forms, with or without
|
|
||||||
// modification, are permitted provided that the following conditions are
|
|
||||||
// met:
|
|
||||||
//
|
|
||||||
// * Redistributions of source code must retain the above copyright
|
|
||||||
// notice, this list of conditions and the following disclaimer.
|
|
||||||
//
|
|
||||||
// * Redistributions in binary form must reproduce the above
|
|
||||||
// copyright notice, this list of conditions and the following
|
|
||||||
// disclaimer in the documentation and/or other materials provided
|
|
||||||
// with the distribution.
|
|
||||||
//
|
|
||||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
// TODO(tarcieri): investigate and remove dead code
|
|
||||||
#![allow(non_upper_case_globals, dead_code)]
|
|
||||||
#![allow(clippy::missing_safety_doc)]
|
|
||||||
|
|
||||||
use crate::consts::*;
|
|
||||||
use libc::{
|
|
||||||
c_char, c_int, fclose, feof, fgets, fopen, free, getenv, malloc, memcpy, memset, sscanf,
|
|
||||||
strcasecmp, strcmp,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
ffi::{CStr, CString},
|
|
||||||
mem,
|
|
||||||
os::raw::c_void,
|
|
||||||
};
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
fn DES_ecb3_encrypt(
|
|
||||||
input: *mut [u8; 8],
|
|
||||||
output: *mut [u8; 8],
|
|
||||||
ks1: *mut DesSubKey,
|
|
||||||
ks2: *mut DesSubKey,
|
|
||||||
ks3: *mut DesSubKey,
|
|
||||||
enc: i32,
|
|
||||||
);
|
|
||||||
fn DES_is_weak_key(key: *mut [u8; 8]) -> i32;
|
|
||||||
fn DES_set_key_unchecked(key: *mut [u8; 8], schedule: *mut DesSubKey);
|
|
||||||
fn PKCS5_PBKDF2_HMAC_SHA1(
|
|
||||||
pass: *const u8,
|
|
||||||
passlen: i32,
|
|
||||||
salt: *const u8,
|
|
||||||
saltlen: i32,
|
|
||||||
iter: i32,
|
|
||||||
keylen: i32,
|
|
||||||
out: *mut u8,
|
|
||||||
) -> i32;
|
|
||||||
fn RAND_bytes(buf: *mut u8, num: i32) -> i32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DES-related errors
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum DesErrorKind {
|
|
||||||
/// Ok
|
|
||||||
Ok = 0,
|
|
||||||
|
|
||||||
/// Invalid parameter
|
|
||||||
InvalidParameter = -1,
|
|
||||||
|
|
||||||
/// Buffer too small
|
|
||||||
BufferTooSmall = -2,
|
|
||||||
|
|
||||||
/// Memory error
|
|
||||||
MemoryError = -3,
|
|
||||||
|
|
||||||
/// General error
|
|
||||||
GeneralError = -4,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 3DES subkeys
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct DesSubKey([u8; 16]);
|
|
||||||
|
|
||||||
/// 3DES keys
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub struct DesKey {
|
|
||||||
/// subkey 1
|
|
||||||
pub ks1: DesSubKey,
|
|
||||||
|
|
||||||
/// subkey 2
|
|
||||||
pub ks2: DesSubKey,
|
|
||||||
|
|
||||||
/// subkey 3
|
|
||||||
pub ks3: DesSubKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Import DES key
|
|
||||||
pub unsafe fn des_import_key(
|
|
||||||
key_type: i32,
|
|
||||||
keyraw: *const u8,
|
|
||||||
keyrawlen: usize,
|
|
||||||
key: *mut *mut DesKey,
|
|
||||||
) -> DesErrorKind {
|
|
||||||
let mut key_tmp = [0u8; 8];
|
|
||||||
let cb_expectedkey: usize;
|
|
||||||
let cb_keysize: usize;
|
|
||||||
|
|
||||||
if key_type != DES_TYPE_3DES as i32 {
|
|
||||||
return DesErrorKind::InvalidParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
cb_expectedkey = (8i32 * 3i32) as (usize);
|
|
||||||
cb_keysize = 8usize;
|
|
||||||
|
|
||||||
if cb_keysize > 8 {
|
|
||||||
return DesErrorKind::MemoryError;
|
|
||||||
}
|
|
||||||
|
|
||||||
if key.is_null() || keyraw.is_null() || keyrawlen != cb_expectedkey {
|
|
||||||
return DesErrorKind::InvalidParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
*key = malloc(mem::size_of::<DesKey>()) as (*mut DesKey);
|
|
||||||
|
|
||||||
if (*key).is_null() {
|
|
||||||
return DesErrorKind::MemoryError;
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(*key as (*mut c_void), 0i32, mem::size_of::<DesKey>());
|
|
||||||
|
|
||||||
memcpy(
|
|
||||||
key_tmp.as_mut_ptr() as (*mut c_void),
|
|
||||||
keyraw as (*const c_void),
|
|
||||||
cb_keysize,
|
|
||||||
);
|
|
||||||
|
|
||||||
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks1);
|
|
||||||
|
|
||||||
memcpy(
|
|
||||||
key_tmp.as_mut_ptr() as (*mut c_void),
|
|
||||||
keyraw.add(cb_keysize) as (*const c_void),
|
|
||||||
cb_keysize,
|
|
||||||
);
|
|
||||||
|
|
||||||
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks2);
|
|
||||||
|
|
||||||
memcpy(
|
|
||||||
key_tmp.as_mut_ptr() as (*mut c_void),
|
|
||||||
keyraw.add(2usize.wrapping_mul(cb_keysize)) as (*const c_void),
|
|
||||||
cb_keysize,
|
|
||||||
);
|
|
||||||
|
|
||||||
DES_set_key_unchecked(&mut key_tmp, &mut (**key).ks3);
|
|
||||||
|
|
||||||
DesErrorKind::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Destroy DES key
|
|
||||||
pub unsafe fn des_destroy_key(key: *mut DesKey) -> DesErrorKind {
|
|
||||||
if !key.is_null() {
|
|
||||||
free(key as (*mut c_void));
|
|
||||||
}
|
|
||||||
|
|
||||||
DesErrorKind::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encrypt with DES key
|
|
||||||
pub unsafe fn des_encrypt(
|
|
||||||
key: *mut DesKey,
|
|
||||||
input: *const u8,
|
|
||||||
inputlen: usize,
|
|
||||||
out: *mut u8,
|
|
||||||
outlen: *mut usize,
|
|
||||||
) -> DesErrorKind {
|
|
||||||
if key.is_null() || outlen.is_null() || *outlen < inputlen || input.is_null() || out.is_null() {
|
|
||||||
return DesErrorKind::InvalidParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
DES_ecb3_encrypt(
|
|
||||||
input as *mut [u8; 8],
|
|
||||||
out as *mut [u8; 8],
|
|
||||||
&mut (*key).ks1,
|
|
||||||
&mut (*key).ks2,
|
|
||||||
&mut (*key).ks3,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
DesErrorKind::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt with DES key
|
|
||||||
pub unsafe fn des_decrypt(
|
|
||||||
key: *mut DesKey,
|
|
||||||
in_: *const u8,
|
|
||||||
inlen: usize,
|
|
||||||
out: *mut u8,
|
|
||||||
outlen: *mut usize,
|
|
||||||
) -> DesErrorKind {
|
|
||||||
if key.is_null() || outlen.is_null() || *outlen < inlen || in_.is_null() || out.is_null() {
|
|
||||||
return DesErrorKind::InvalidParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
DES_ecb3_encrypt(
|
|
||||||
in_ as *mut [u8; 8],
|
|
||||||
out as *mut [u8; 8],
|
|
||||||
&mut (*key).ks1,
|
|
||||||
&mut (*key).ks2,
|
|
||||||
&mut (*key).ks3,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
DesErrorKind::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is the given DES key weak?
|
|
||||||
pub unsafe fn yk_des_is_weak_key(key: *const u8, _cb_key: usize) -> bool {
|
|
||||||
DES_is_weak_key(key as (*mut [u8; 8])) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PRNG errors/results
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum PRngErrorKind {
|
|
||||||
/// Ok
|
|
||||||
Ok = 0,
|
|
||||||
|
|
||||||
/// General error
|
|
||||||
GeneralError = -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate bytes with the PRNG
|
|
||||||
pub unsafe fn _ykpiv_prng_generate(buffer: *mut u8, cb_req: usize) -> PRngErrorKind {
|
|
||||||
if RAND_bytes(buffer, cb_req as (i32)) != -1 {
|
|
||||||
PRngErrorKind::Ok
|
|
||||||
} else {
|
|
||||||
PRngErrorKind::GeneralError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PKCS#5 error types
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum Pkcs5ErrorKind {
|
|
||||||
/// OK
|
|
||||||
Ok = 0,
|
|
||||||
|
|
||||||
/// General error
|
|
||||||
GeneralError = -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt a PKCS#5 key
|
|
||||||
pub unsafe fn pkcs5_pbkdf2_sha1(
|
|
||||||
password: *const u8,
|
|
||||||
cb_password: usize,
|
|
||||||
salt: *const u8,
|
|
||||||
cb_salt: usize,
|
|
||||||
iterations: usize,
|
|
||||||
key: *const u8,
|
|
||||||
cb_key: usize,
|
|
||||||
) -> Pkcs5ErrorKind {
|
|
||||||
PKCS5_PBKDF2_HMAC_SHA1(
|
|
||||||
password,
|
|
||||||
cb_password as (i32),
|
|
||||||
salt,
|
|
||||||
cb_salt as (i32),
|
|
||||||
iterations as (i32),
|
|
||||||
cb_key as (i32),
|
|
||||||
key as (*mut u8),
|
|
||||||
);
|
|
||||||
|
|
||||||
Pkcs5ErrorKind::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strip whitespace
|
|
||||||
// TODO(tarcieri): implement this
|
|
||||||
pub unsafe fn _strip_ws(sz: *mut c_char) -> *mut c_char {
|
|
||||||
sz
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Source of how a setting was configured
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum SettingSource {
|
|
||||||
/// User-specified setting
|
|
||||||
User,
|
|
||||||
|
|
||||||
/// Admin-specified setting
|
|
||||||
Admin,
|
|
||||||
|
|
||||||
/// Default setting
|
|
||||||
Default,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setting booleans
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub struct SettingBool {
|
|
||||||
/// Boolean value
|
|
||||||
pub value: bool,
|
|
||||||
|
|
||||||
/// Source of the configuration setting (user/admin/default)
|
|
||||||
pub source: SettingSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a boolean config value
|
|
||||||
pub unsafe fn _get_bool_config(sz_setting: *const c_char) -> SettingBool {
|
|
||||||
let mut setting: SettingBool = SettingBool {
|
|
||||||
value: false,
|
|
||||||
source: SettingSource::Default,
|
|
||||||
};
|
|
||||||
let mut sz_line = [0u8; 256];
|
|
||||||
let mut psz_name: *mut c_char;
|
|
||||||
let mut psz_value: *mut c_char;
|
|
||||||
let mut sz_name = [0u8; 256];
|
|
||||||
let mut sz_value = [0u8; 256];
|
|
||||||
|
|
||||||
let pf = fopen(
|
|
||||||
b"/etc/yubico/yubikeypiv.conf\0".as_ptr() as *const c_char,
|
|
||||||
b"r\0".as_ptr() as *const c_char,
|
|
||||||
);
|
|
||||||
|
|
||||||
if pf.is_null() {
|
|
||||||
return setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
while feof(pf) == 0 {
|
|
||||||
if fgets(
|
|
||||||
sz_line.as_mut_ptr() as *mut c_char,
|
|
||||||
sz_line.len() as c_int,
|
|
||||||
pf,
|
|
||||||
)
|
|
||||||
.is_null()
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if sz_line[0] == b'#' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if sz_line[0] == b'\r' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if sz_line[0] == b'\n' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if sscanf(
|
|
||||||
sz_line.as_ptr() as *const c_char,
|
|
||||||
b"%255[^=]=%255s\0".as_ptr() as *const c_char,
|
|
||||||
sz_name.as_mut_ptr(),
|
|
||||||
sz_value.as_mut_ptr(),
|
|
||||||
) != 2
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
psz_name = _strip_ws(sz_name.as_mut_ptr() as *mut c_char);
|
|
||||||
|
|
||||||
if strcasecmp(psz_name, sz_setting) != 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
psz_value = _strip_ws(sz_value.as_mut_ptr() as *mut c_char);
|
|
||||||
setting.source = SettingSource::Admin;
|
|
||||||
setting.value = strcmp(psz_value, b"1\0".as_ptr() as *const c_char) == 0
|
|
||||||
|| strcasecmp(psz_value, b"true\0".as_ptr() as *const c_char) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(pf);
|
|
||||||
setting
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a setting boolean from an environment variable
|
|
||||||
pub unsafe fn _get_bool_env(sz_setting: *const c_char) -> SettingBool {
|
|
||||||
let mut setting: SettingBool = SettingBool {
|
|
||||||
value: false,
|
|
||||||
source: SettingSource::Default,
|
|
||||||
};
|
|
||||||
|
|
||||||
let sz_name = CString::new(format!(
|
|
||||||
"YUBIKEY_PIV_{}",
|
|
||||||
CStr::from_ptr(sz_setting).to_string_lossy()
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let psz_value = getenv(sz_name.as_ptr());
|
|
||||||
|
|
||||||
if !psz_value.is_null() {
|
|
||||||
setting.source = SettingSource::User;
|
|
||||||
setting.value = strcmp(psz_value, b"1\0".as_ptr() as *const c_char) == 0
|
|
||||||
|| strcasecmp(psz_value, b"true\0".as_ptr() as *const c_char) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setting
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a setting boolean
|
|
||||||
pub unsafe fn setting_get_bool(sz_setting: *const c_char, def: bool) -> SettingBool {
|
|
||||||
let mut setting = _get_bool_config(sz_setting);
|
|
||||||
|
|
||||||
if setting.source == SettingSource::Default {
|
|
||||||
setting = _get_bool_env(sz_setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if setting.source == SettingSource::Default {
|
|
||||||
setting.value = def;
|
|
||||||
}
|
|
||||||
|
|
||||||
setting
|
|
||||||
}
|
|
||||||
+58
-62
@@ -1,46 +1,17 @@
|
|||||||
//! [YubiKey][1] PIV: [Personal Identity Verification][2] support for
|
#![doc = include_str!("../README.md")]
|
||||||
//! [Yubico][3] devices using the Chip Card Interface Device ([CCID][4])
|
#![doc(
|
||||||
//! protocol.
|
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png"
|
||||||
//!
|
)]
|
||||||
//! **PIV** is a [NIST][5] standard for both *signing* and *encryption*
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
//! using SmartCards and SmartCard-based hardware tokens like YubiKeys.
|
#![forbid(unsafe_code)]
|
||||||
//!
|
#![warn(
|
||||||
//! This library natively implements the CCID protocol used to manage and
|
clippy::mod_module_files,
|
||||||
//! utilize PIV encryption and signing keys which can be generated, imported,
|
clippy::unwrap_used,
|
||||||
//! and stored on YubiKey devices.
|
missing_docs,
|
||||||
//!
|
rust_2018_idioms,
|
||||||
//! Supported algorithms:
|
unused_lifetimes,
|
||||||
//!
|
unused_qualifications
|
||||||
//! - **Authentication**: `3DES`
|
)]
|
||||||
//! - **Encryption**: `RSA1024`, `RSA2048`, `ECCP256`, `ECCP384`
|
|
||||||
//! - **Signatures**:
|
|
||||||
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
|
|
||||||
//! - ECDSA: `ECCP256`, `ECCP384`
|
|
||||||
//!
|
|
||||||
//! ## Status
|
|
||||||
//!
|
|
||||||
//! This library is a work-in-progress translation and is not yet usable.
|
|
||||||
//! Check back later for updates.
|
|
||||||
//!
|
|
||||||
//! ## History
|
|
||||||
//!
|
|
||||||
//! This library is a Rust translation of the [yubico-piv-tool][6] utility by
|
|
||||||
//! Yubico, which was originally written in C. It was mechanically translated
|
|
||||||
//! from C into Rust using [Corrode][7], and then subsequently heavily
|
|
||||||
//! refactored into safer, more idiomatic Rust.
|
|
||||||
//!
|
|
||||||
//! For more information on `yubico-piv-tool` and background information on how
|
|
||||||
//! the YubiKey implementation of PIV works in general, see the
|
|
||||||
//! [Yubico PIV Tool Command Line Guide][8].
|
|
||||||
//!
|
|
||||||
//! [1]: https://www.yubico.com/products/yubikey-hardware/
|
|
||||||
//! [2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf
|
|
||||||
//! [3]: https://www.yubico.com/
|
|
||||||
//! [4]: https://en.wikipedia.org/wiki/CCID_(protocol)
|
|
||||||
//! [5]: https://www.nist.gov/
|
|
||||||
//! [6]: https://github.com/Yubico/yubico-piv-tool/
|
|
||||||
//! [7]: https://github.com/jameysharp/corrode
|
|
||||||
//! [8]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
|
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
// Adapted from yubico-piv-tool:
|
||||||
// <https://github.com/Yubico/yubico-piv-tool/>
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
@@ -72,24 +43,49 @@
|
|||||||
// (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.
|
||||||
|
|
||||||
#![doc(
|
|
||||||
html_logo_url = "https://raw.githubusercontent.com/tarcieri/yubikey-piv.rs/develop/img/logo.png",
|
|
||||||
html_root_url = "https://docs.rs/yubikey-piv/0.0.1"
|
|
||||||
)]
|
|
||||||
#![warn(
|
|
||||||
missing_docs,
|
|
||||||
rust_2018_idioms,
|
|
||||||
trivial_casts,
|
|
||||||
trivial_numeric_casts,
|
|
||||||
unused_lifetimes,
|
|
||||||
unused_qualifications
|
|
||||||
)]
|
|
||||||
|
|
||||||
mod apdu;
|
mod apdu;
|
||||||
pub mod consts;
|
mod cccid;
|
||||||
pub mod error;
|
pub mod certificate;
|
||||||
mod internal;
|
mod chuid;
|
||||||
pub mod util;
|
mod config;
|
||||||
pub mod yubikey;
|
mod consts;
|
||||||
|
mod error;
|
||||||
|
mod metadata;
|
||||||
|
pub mod mgm;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
mod mscmap;
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
mod msroots;
|
||||||
|
mod otp;
|
||||||
|
pub mod piv;
|
||||||
|
mod policy;
|
||||||
|
pub mod reader;
|
||||||
|
mod serialization;
|
||||||
|
mod setting;
|
||||||
|
mod transaction;
|
||||||
|
mod yubikey;
|
||||||
|
|
||||||
pub use self::yubikey::YubiKey;
|
pub use crate::{
|
||||||
|
cccid::{CardId, CccId},
|
||||||
|
certificate::Certificate,
|
||||||
|
chuid::ChuId,
|
||||||
|
config::Config,
|
||||||
|
error::{Error, Result},
|
||||||
|
mgm::{MgmAlgorithmId, MgmKey, MgmType},
|
||||||
|
piv::Key,
|
||||||
|
policy::{PinPolicy, TouchPolicy},
|
||||||
|
reader::Context,
|
||||||
|
setting::{Setting, SettingSource},
|
||||||
|
yubikey::{CachedPin, Serial, Version, YubiKey},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub use crate::{mscmap::MsContainer, msroots::MsRoots};
|
||||||
|
|
||||||
|
pub use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Object identifiers: handles to particular objects stored on a YubiKey.
|
||||||
|
pub type ObjectId = u32;
|
||||||
|
|
||||||
|
/// Buffer type (self-zeroizing byte vector)
|
||||||
|
pub type Buffer = zeroize::Zeroizing<Vec<u8>>;
|
||||||
|
|||||||
+247
@@ -0,0 +1,247 @@
|
|||||||
|
//! YubiKey Device Metadata
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use std::{iter, marker::PhantomData};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX},
|
||||||
|
serialization::*,
|
||||||
|
transaction::Transaction,
|
||||||
|
Buffer, Error, Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TAG_ADMIN: u8 = 0x80;
|
||||||
|
const TAG_PROTECTED: u8 = 0x88;
|
||||||
|
pub const OBJ_ADMIN_DATA: u32 = 0x005f_ff00;
|
||||||
|
pub const OBJ_PRINTED: u32 = 0x005f_c109;
|
||||||
|
|
||||||
|
pub(crate) trait MetadataType: private::Sealed {}
|
||||||
|
|
||||||
|
/// A type variable corresponding to PIN-protected metadata.
|
||||||
|
pub(crate) enum Protected {}
|
||||||
|
impl MetadataType for Protected {}
|
||||||
|
|
||||||
|
/// A type variable corresponding to administrative metadata.
|
||||||
|
pub(crate) enum Admin {}
|
||||||
|
impl MetadataType for Admin {}
|
||||||
|
|
||||||
|
/// Metadata stored in a YubiKey.
|
||||||
|
pub(crate) struct Metadata<T: MetadataType> {
|
||||||
|
inner: Buffer,
|
||||||
|
_marker: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PIN-protected metadata stored in a YubiKey.
|
||||||
|
pub(crate) type ProtectedData = Metadata<Protected>;
|
||||||
|
/// Administrative metadata stored in a YubiKey.
|
||||||
|
pub(crate) type AdminData = Metadata<Admin>;
|
||||||
|
|
||||||
|
impl<T: MetadataType> Default for Metadata<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Metadata {
|
||||||
|
inner: Zeroizing::new(vec![]),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: MetadataType> Metadata<T> {
|
||||||
|
/// Read metadata
|
||||||
|
pub(crate) fn read(txn: &Transaction<'_>) -> Result<Self> {
|
||||||
|
let data = txn.fetch_object(T::obj_id())?;
|
||||||
|
Ok(Metadata {
|
||||||
|
inner: Tlv::parse_single(data, T::tag())?,
|
||||||
|
_marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write metadata
|
||||||
|
pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> {
|
||||||
|
if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.inner.is_empty() {
|
||||||
|
return Self::delete(txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Zeroizing::new(vec![0u8; CB_OBJ_MAX]);
|
||||||
|
let len = Tlv::write(&mut buf, T::tag(), &self.inner)?;
|
||||||
|
|
||||||
|
txn.save_object(T::obj_id(), &buf[..len])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete metadata
|
||||||
|
pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> {
|
||||||
|
txn.save_object(T::obj_id(), &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get metadata item
|
||||||
|
pub(crate) fn get_item(&self, tag: u8) -> Result<&[u8]> {
|
||||||
|
let mut data = &self.inner[..];
|
||||||
|
|
||||||
|
while !data.is_empty() {
|
||||||
|
let (remaining, tlv) = Tlv::parse(data)?;
|
||||||
|
data = remaining;
|
||||||
|
|
||||||
|
if tlv.tag == tag {
|
||||||
|
// found tag
|
||||||
|
return Ok(tlv.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::GenericError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set metadata item
|
||||||
|
pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> {
|
||||||
|
let mut cb_temp: usize = 0;
|
||||||
|
let mut tag_temp: u8 = 0;
|
||||||
|
let mut cb_len: usize = 0;
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
while offset < self.inner.len() {
|
||||||
|
tag_temp = self.inner[offset];
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
cb_len = get_length(&self.inner[offset..], &mut cb_temp);
|
||||||
|
offset += cb_len;
|
||||||
|
|
||||||
|
if tag_temp == tag {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += cb_temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag_temp != tag {
|
||||||
|
if item.is_empty() {
|
||||||
|
// We've been asked to delete an existing item that isn't in the blob
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We did not find an existing tag, append
|
||||||
|
assert_eq!(offset, self.inner.len());
|
||||||
|
self.inner.extend(iter::repeat_n(
|
||||||
|
0,
|
||||||
|
1 + get_length_size(item.len()) + item.len(),
|
||||||
|
));
|
||||||
|
Tlv::write(&mut self.inner[offset..], tag, item)?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found tag
|
||||||
|
|
||||||
|
// Check length, if it matches, overwrite
|
||||||
|
if cb_temp == item.len() {
|
||||||
|
self.inner[offset..offset + item.len()].copy_from_slice(item);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length doesn't match, expand/shrink to fit
|
||||||
|
let next_offset = offset + cb_temp;
|
||||||
|
// Must be signed to have negative offsets
|
||||||
|
let cb_moved: isize = (item.len() as isize - cb_temp as isize)
|
||||||
|
+ if item.is_empty() {
|
||||||
|
// For tag, if deleting
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
get_length_size(item.len()) as isize
|
||||||
|
}
|
||||||
|
// Accounts for different length encoding
|
||||||
|
- cb_len as isize;
|
||||||
|
|
||||||
|
// If length would cause buffer overflow, return error
|
||||||
|
if (self.inner.len() as isize + cb_moved) as usize > CB_OBJ_MAX {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move remaining data
|
||||||
|
let orig_len = self.inner.len();
|
||||||
|
if cb_moved > 0 {
|
||||||
|
self.inner.extend(iter::repeat_n(0, cb_moved as usize));
|
||||||
|
}
|
||||||
|
self.inner.copy_within(
|
||||||
|
next_offset..orig_len,
|
||||||
|
(next_offset as isize + cb_moved) as usize,
|
||||||
|
);
|
||||||
|
self.inner
|
||||||
|
.resize((orig_len as isize + cb_moved) as usize, 0);
|
||||||
|
|
||||||
|
// Re-encode item and insert
|
||||||
|
if !item.is_empty() {
|
||||||
|
offset -= cb_len;
|
||||||
|
offset += set_length(&mut self.inner[offset..], item.len())?;
|
||||||
|
self.inner[offset..offset + item.len()].copy_from_slice(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the size of a length tag for the given length
|
||||||
|
fn get_length_size(length: usize) -> usize {
|
||||||
|
if length < 0x80 {
|
||||||
|
1
|
||||||
|
} else if length < 0xff {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
use super::*;
|
||||||
|
pub trait Sealed {
|
||||||
|
fn tag() -> u8;
|
||||||
|
fn obj_id() -> u32;
|
||||||
|
}
|
||||||
|
impl Sealed for Protected {
|
||||||
|
fn tag() -> u8 {
|
||||||
|
TAG_PROTECTED
|
||||||
|
}
|
||||||
|
fn obj_id() -> u32 {
|
||||||
|
OBJ_PRINTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Sealed for Admin {
|
||||||
|
fn tag() -> u8 {
|
||||||
|
TAG_ADMIN
|
||||||
|
}
|
||||||
|
fn obj_id() -> u32 {
|
||||||
|
OBJ_ADMIN_DATA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1033
File diff suppressed because it is too large
Load Diff
+198
@@ -0,0 +1,198 @@
|
|||||||
|
//! MS Container Map Records.
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{consts::CB_OBJ_MAX, piv::SlotId, serialization::*, Error, Result, YubiKey};
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
const OBJ_MSCMAP: u32 = 0x005f_ff10;
|
||||||
|
|
||||||
|
const TAG_MSCMAP: u8 = 0x81;
|
||||||
|
|
||||||
|
/// MS Container Map records.
|
||||||
|
///
|
||||||
|
/// Defined in Microsoft's Smart Card Minidriver Specification:
|
||||||
|
/// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MsContainer {
|
||||||
|
/// Container name.
|
||||||
|
pub name: [u16; Self::NAME_LEN],
|
||||||
|
|
||||||
|
/// Card slot.
|
||||||
|
pub slot: SlotId,
|
||||||
|
|
||||||
|
/// Key spec.
|
||||||
|
pub key_spec: u8,
|
||||||
|
|
||||||
|
/// Key size in bits.
|
||||||
|
pub key_size_bits: u16,
|
||||||
|
|
||||||
|
/// Flags.
|
||||||
|
pub flags: u8,
|
||||||
|
|
||||||
|
/// PIN ID.
|
||||||
|
pub pin_id: u8,
|
||||||
|
|
||||||
|
/// Associated ECHD container.
|
||||||
|
pub associated_echd_container: u8,
|
||||||
|
|
||||||
|
/// Cert fingerprint.
|
||||||
|
pub cert_fingerprint: [u8; Self::CERT_FINGERPRINT_LEN],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MsContainer {
|
||||||
|
/// Container name length in UTF-16 chars.
|
||||||
|
const NAME_LEN: usize = 40;
|
||||||
|
|
||||||
|
/// Container record length: 27 = 80 + 1 + 1 + 2 + 1 + 1 + 1 + 20
|
||||||
|
const REC_LEN: usize = (2 * Self::NAME_LEN) + 27;
|
||||||
|
|
||||||
|
/// Length of a certificate fingerprint.
|
||||||
|
const CERT_FINGERPRINT_LEN: usize = 20;
|
||||||
|
|
||||||
|
/// Read MS Container Map records.
|
||||||
|
pub fn read_mscmap(yubikey: &mut YubiKey) -> Result<Vec<Self>> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
let response = txn.fetch_object(OBJ_MSCMAP)?;
|
||||||
|
let mut containers = vec![];
|
||||||
|
|
||||||
|
let (_, tlv) = match Tlv::parse(&response) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => {
|
||||||
|
// TODO(tarcieri): is this really OK?
|
||||||
|
return Ok(containers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if tlv.tag != TAG_MSCMAP {
|
||||||
|
// TODO(tarcieri): yubico-piv-tool returned success here? should we?
|
||||||
|
return Err(Error::InvalidObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
for chunk in tlv.value.chunks_exact(Self::REC_LEN) {
|
||||||
|
containers.push(MsContainer::new(chunk)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(containers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write MS Container Map records.
|
||||||
|
pub fn write_mscmap(yubikey: &mut YubiKey, containers: &[Self]) -> Result<()> {
|
||||||
|
let n_containers = containers.len();
|
||||||
|
let data_len = n_containers * Self::REC_LEN;
|
||||||
|
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
if n_containers == 0 {
|
||||||
|
return txn.save_object(OBJ_MSCMAP, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
|
let offset = Tlv::write_as(&mut buf, TAG_MSCMAP, data_len, |buf| {
|
||||||
|
for (i, chunk) in buf.chunks_exact_mut(Self::REC_LEN).enumerate() {
|
||||||
|
chunk.copy_from_slice(&containers[i].to_bytes());
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
txn.save_object(OBJ_MSCMAP, &buf[..offset])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a container record from a byte slice.
|
||||||
|
pub fn new(bytes: &[u8]) -> Result<Self> {
|
||||||
|
if bytes.len() != Self::REC_LEN {
|
||||||
|
error!(
|
||||||
|
"couldn't parse PIV container: expected {}-bytes, got {}-bytes",
|
||||||
|
Self::REC_LEN,
|
||||||
|
bytes.len()
|
||||||
|
);
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut name = [0u16; Self::NAME_LEN];
|
||||||
|
let name_bytes_len = Self::NAME_LEN * 2;
|
||||||
|
|
||||||
|
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
|
||||||
|
name[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cert_fingerprint = [0u8; 20];
|
||||||
|
cert_fingerprint.copy_from_slice(&bytes[(bytes.len() - 20)..]);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name,
|
||||||
|
slot: bytes[name_bytes_len].try_into()?,
|
||||||
|
key_spec: bytes[name_bytes_len + 1],
|
||||||
|
key_size_bits: u16::from_le_bytes([
|
||||||
|
bytes[name_bytes_len + 2],
|
||||||
|
bytes[name_bytes_len + 3],
|
||||||
|
]),
|
||||||
|
flags: bytes[name_bytes_len + 4],
|
||||||
|
pin_id: bytes[name_bytes_len + 5],
|
||||||
|
associated_echd_container: bytes[name_bytes_len + 6],
|
||||||
|
cert_fingerprint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the container name as a UTF-16 string.
|
||||||
|
pub fn parse_name(&self) -> Result<String> {
|
||||||
|
String::from_utf16(&self.name).map_err(|_| Error::ParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a container record as a byte size.
|
||||||
|
pub fn to_bytes(&self) -> [u8; Self::REC_LEN] {
|
||||||
|
// TODO(tarcieri): use array instead of `Vec`
|
||||||
|
let mut bytes = Vec::with_capacity(Self::REC_LEN);
|
||||||
|
|
||||||
|
for i in 0..Self::NAME_LEN {
|
||||||
|
bytes.extend_from_slice(&self.name[i].to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.push(self.slot.into());
|
||||||
|
bytes.push(self.key_spec);
|
||||||
|
bytes.extend_from_slice(&self.key_size_bits.to_le_bytes());
|
||||||
|
bytes.push(self.flags);
|
||||||
|
bytes.push(self.pin_id);
|
||||||
|
bytes.push(self.associated_echd_container);
|
||||||
|
bytes.extend_from_slice(&self.cert_fingerprint);
|
||||||
|
bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.expect("should be REC_LEN-sized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for MsContainer {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(bytes: &'a [u8]) -> Result<Self> {
|
||||||
|
Self::new(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
//! PKCS#7 formatted certificate store for enterprise trusted roots.
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX},
|
||||||
|
serialization::*,
|
||||||
|
Error, Result, YubiKey,
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// PKCS#7-formatted certificate store for enterprise trust roots.
|
||||||
|
///
|
||||||
|
/// The `msroots` file contains a bag of certificates with empty content and
|
||||||
|
/// an empty signature, allowing an enterprise root certificate truststore to
|
||||||
|
/// be written to and read from a YubiKey.
|
||||||
|
///
|
||||||
|
/// For more information, see:
|
||||||
|
/// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
|
||||||
|
pub struct MsRoots(Vec<u8>);
|
||||||
|
|
||||||
|
impl MsRoots {
|
||||||
|
/// Initialize a local certificate struct from the given bytebuffer
|
||||||
|
pub fn new(msroots: impl AsRef<[u8]>) -> Result<Self> {
|
||||||
|
Ok(MsRoots(msroots.as_ref().into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `msroots` file from YubiKey
|
||||||
|
pub fn read(yubikey: &mut YubiKey) -> Result<Option<Self>> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
// allocate first page
|
||||||
|
let mut data = Vec::with_capacity(CB_OBJ_MAX);
|
||||||
|
|
||||||
|
for object_id in OBJ_MSROOTS1..OBJ_MSROOTS5 {
|
||||||
|
let buf = txn.fetch_object(object_id)?;
|
||||||
|
|
||||||
|
let (_, tlv) = match Tlv::parse(&buf) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (TAG_MSROOTS_MID != tlv.tag || OBJ_MSROOTS5 == object_id)
|
||||||
|
&& (TAG_MSROOTS_END != tlv.tag)
|
||||||
|
{
|
||||||
|
// the current object doesn't contain a valid part of a msroots file
|
||||||
|
|
||||||
|
// treat condition as object isn't found
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.extend_from_slice(tlv.value);
|
||||||
|
|
||||||
|
if tlv.tag == TAG_MSROOTS_END {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsRoots::new(&data)
|
||||||
|
.map(Some)
|
||||||
|
.inspect_err(|e| error!("error parsing msroots: {:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `msroots` file to YubiKey
|
||||||
|
pub fn write(&self, yubikey: &mut YubiKey) -> Result<()> {
|
||||||
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
|
let mut offset: usize;
|
||||||
|
let mut data_offset: usize = 0;
|
||||||
|
let mut data_chunk: usize;
|
||||||
|
let data = &self.0;
|
||||||
|
let data_len = data.len();
|
||||||
|
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
if data_len == 0 {
|
||||||
|
return txn.save_object(OBJ_MSROOTS1, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate number of objects required to store blob
|
||||||
|
let n_objs: usize = (data_len / (CB_OBJ_MAX - CB_OBJ_TAG_MAX)) + 1;
|
||||||
|
|
||||||
|
if n_objs > 5 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..n_objs {
|
||||||
|
offset = 0;
|
||||||
|
|
||||||
|
data_chunk = if CB_OBJ_MAX - CB_OBJ_TAG_MAX < data_len - data_offset {
|
||||||
|
CB_OBJ_MAX - CB_OBJ_TAG_MAX
|
||||||
|
} else {
|
||||||
|
data_len - data_offset
|
||||||
|
};
|
||||||
|
|
||||||
|
offset += Tlv::write(
|
||||||
|
&mut buf,
|
||||||
|
if i == n_objs - 1 {
|
||||||
|
TAG_MSROOTS_END
|
||||||
|
} else {
|
||||||
|
TAG_MSROOTS_MID
|
||||||
|
},
|
||||||
|
&data[data_offset..(data_offset + data_chunk)],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
txn.save_object(OBJ_MSROOTS1 + i as u32, &buf[..offset])?;
|
||||||
|
|
||||||
|
data_offset += data_chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for MsRoots {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/// YubiKey OTP Applet Name
|
||||||
|
pub(crate) const APPLET_NAME: &str = "YubiKey OTP";
|
||||||
|
|
||||||
|
/// YubiKey OTP Applet ID. Needed to query serial on YK4.
|
||||||
|
pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
|
||||||
+1352
File diff suppressed because it is too large
Load Diff
+120
@@ -0,0 +1,120 @@
|
|||||||
|
//! Enums representing key policies.
|
||||||
|
|
||||||
|
use crate::{serialization::Tlv, Error, Result};
|
||||||
|
|
||||||
|
/// Specifies how often the PIN needs to be entered for access to the credential in a
|
||||||
|
/// given slot.
|
||||||
|
///
|
||||||
|
/// This policy must be set when keys are generated or imported, and cannot be changed later.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, 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 TryFrom<u8> for PinPolicy {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(PinPolicy::Default),
|
||||||
|
1 => Ok(PinPolicy::Never),
|
||||||
|
2 => Ok(PinPolicy::Once),
|
||||||
|
3 => Ok(PinPolicy::Always),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
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 when keys are generated or imported, and cannot be changed later.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, 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> {
|
||||||
|
match self {
|
||||||
|
TouchPolicy::Default => Ok(0),
|
||||||
|
_ => Tlv::write(buf, 0xab, &[self.into()]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for TouchPolicy {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(TouchPolicy::Default),
|
||||||
|
1 => Ok(TouchPolicy::Never),
|
||||||
|
2 => Ok(TouchPolicy::Always),
|
||||||
|
3 => Ok(TouchPolicy::Cached),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
//! Support for enumerating available PC/SC card readers.
|
||||||
|
|
||||||
|
use crate::{Error, Result, YubiKey};
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
ffi::CStr,
|
||||||
|
fmt,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Iterator over connected readers
|
||||||
|
pub type Iter<'ctx> = std::vec::IntoIter<Reader<'ctx>>;
|
||||||
|
|
||||||
|
/// PC/SC reader context: used to enumerate available PC/SC [`Reader`]s.
|
||||||
|
pub struct Context {
|
||||||
|
/// PC/SC context
|
||||||
|
ctx: Arc<Mutex<pcsc::Context>>,
|
||||||
|
|
||||||
|
/// Buffer for storing reader names
|
||||||
|
reader_names: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Context {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Context").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
/// 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> {
|
||||||
|
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<'_>> {
|
||||||
|
let Self { ctx, reader_names } = self;
|
||||||
|
|
||||||
|
let reader_cstrs: Vec<_> = {
|
||||||
|
// TODO(tarcieri): better error?
|
||||||
|
let c = ctx.lock().map_err(|_| Error::GenericError)?;
|
||||||
|
|
||||||
|
// ensure PC/SC context is valid
|
||||||
|
c.is_valid()?;
|
||||||
|
|
||||||
|
c.list_readers(reader_names)?.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[allow(clippy::needless_collect)]
|
||||||
|
let readers: Vec<_> = reader_cstrs
|
||||||
|
.iter()
|
||||||
|
.map(|name| Reader::new(name, Arc::clone(ctx)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(readers.into_iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An individual connected PC/SC card 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> {
|
||||||
|
self.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to this reader, returning its `pcsc::Card`.
|
||||||
|
pub(crate) fn connect(&self) -> Result<pcsc::Card> {
|
||||||
|
// TODO(tarcieri): better error?
|
||||||
|
let ctx = self.ctx.lock().map_err(|_| Error::GenericError)?;
|
||||||
|
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
//! Serialization functions
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
use crate::{consts::CB_OBJ_TAG_MIN, Buffer, Error, ObjectId, Result};
|
||||||
|
|
||||||
|
pub const OBJ_DISCOVERY: u32 = 0x7e;
|
||||||
|
|
||||||
|
// 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)> {
|
||||||
|
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 buffer = buffer.get(offset..).ok_or(Error::SizeError)?;
|
||||||
|
|
||||||
|
if buffer.len() >= len {
|
||||||
|
let (value, buffer) = buffer.split_at(len);
|
||||||
|
Ok((buffer, Tlv { tag, value }))
|
||||||
|
} else {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
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> {
|
||||||
|
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>
|
||||||
|
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
|
||||||
|
pub(crate) fn set_length(buffer: &mut [u8], length: usize) -> Result<usize> {
|
||||||
|
if length < 0x80 {
|
||||||
|
if buffer.is_empty() {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
} else {
|
||||||
|
buffer[0] = length as u8;
|
||||||
|
Ok(1)
|
||||||
|
}
|
||||||
|
} else if length < 0x100 {
|
||||||
|
if buffer.len() < 2 {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
} else {
|
||||||
|
buffer[0] = 0x81;
|
||||||
|
buffer[1] = length as u8;
|
||||||
|
Ok(2)
|
||||||
|
}
|
||||||
|
} else if buffer.len() < 3 {
|
||||||
|
Err(Error::SizeError)
|
||||||
|
} else {
|
||||||
|
buffer[0] = 0x82;
|
||||||
|
buffer[1] = ((length >> 8) & 0xff) as u8;
|
||||||
|
buffer[2] = (length & 0xff) as u8;
|
||||||
|
Ok(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse length tag, returning the size of the length tag itself as the
|
||||||
|
/// returned value, and setting the len parameter to the parsed length.
|
||||||
|
pub(crate) fn get_length(buffer: &[u8], len: &mut usize) -> usize {
|
||||||
|
// This is not valid ASN.1 (0x80 is the indefinite length marker).
|
||||||
|
// See comment in key::generate for more context.
|
||||||
|
if buffer[0] < 0x81 {
|
||||||
|
*len = buffer[0] as usize;
|
||||||
|
1
|
||||||
|
} else if (buffer[0] & 0x7f) == 1 {
|
||||||
|
*len = buffer[1] as usize;
|
||||||
|
2
|
||||||
|
} else if (buffer[0] & 0x7f) == 2 {
|
||||||
|
let tmp = buffer[1] as usize;
|
||||||
|
*len = (tmp << 8) + buffer[2] as usize;
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is length valid?
|
||||||
|
pub(crate) fn has_valid_length(buffer: &[u8], len: usize) -> bool {
|
||||||
|
(buffer[0] < 0x81 && len > 0)
|
||||||
|
|| ((buffer[0] & 0x7f) == 1 && len > 1)
|
||||||
|
|| ((buffer[0] & 0x7f == 2) && (len > 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set an object ID header value in the given buffer, returning a mutable
|
||||||
|
/// slice immediately after the header.
|
||||||
|
///
|
||||||
|
/// Panics if the buffer is too small to contain the header.
|
||||||
|
pub(crate) fn set_object(object_id: ObjectId, mut buffer: &mut [u8]) -> &mut [u8] {
|
||||||
|
buffer[0] = 0x5c;
|
||||||
|
|
||||||
|
if object_id == OBJ_DISCOVERY {
|
||||||
|
buffer[1] = 1;
|
||||||
|
buffer[2] = OBJ_DISCOVERY as u8;
|
||||||
|
buffer = &mut buffer[3..];
|
||||||
|
} else if object_id > 0xffff && object_id <= 0x00ff_ffff {
|
||||||
|
buffer[1] = 3;
|
||||||
|
buffer[2] = ((object_id >> 16) & 0xff) as u8;
|
||||||
|
buffer[3] = ((object_id >> 8) & 0xff) as u8;
|
||||||
|
buffer[4] = (object_id & 0xff) as u8;
|
||||||
|
buffer = &mut buffer[5..];
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
//! Configuration setting values parsed from the environment and config file:
|
||||||
|
//! `/etc/yubico/yubikeypiv.conf`
|
||||||
|
|
||||||
|
// Adapted from yubico-piv-tool:
|
||||||
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
|
//
|
||||||
|
// Copyright (c) 2014-2016 Yubico AB
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
/// Default location of the YubiKey PIV configuration file
|
||||||
|
pub const DEFAULT_CONFIG_FILE: &str = "/etc/yubico/yubikeypiv.conf";
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::File,
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Source of how a setting was configured.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum SettingSource {
|
||||||
|
/// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars.
|
||||||
|
User,
|
||||||
|
|
||||||
|
/// Admin-specified setting: sourced via the `/etc/yubico/yubikeypiv.conf`
|
||||||
|
/// configuration file.
|
||||||
|
Admin,
|
||||||
|
|
||||||
|
/// Default setting.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setting booleans: configuration values sourced from a file or the environment.
|
||||||
|
///
|
||||||
|
/// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a
|
||||||
|
/// system administrator, or by the local user via `YUBIKEY_PIV_*` environment
|
||||||
|
/// variables.
|
||||||
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
|
pub struct Setting {
|
||||||
|
/// Boolean value
|
||||||
|
pub value: bool,
|
||||||
|
|
||||||
|
/// Source of the configuration setting (user, admin, or default)
|
||||||
|
pub source: SettingSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Setting {
|
||||||
|
/// Get a setting by name.
|
||||||
|
pub fn get(key: &str, default: bool) -> Self {
|
||||||
|
Self::from_file(key)
|
||||||
|
.or_else(|| Self::from_env(key))
|
||||||
|
.unwrap_or(Self {
|
||||||
|
value: default,
|
||||||
|
source: SettingSource::Default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a boolean config value from the provided config file
|
||||||
|
fn from_file(key: &str) -> Option<Self> {
|
||||||
|
if let Ok(file) = File::open(DEFAULT_CONFIG_FILE) {
|
||||||
|
for line in BufReader::new(file).lines() {
|
||||||
|
let line = match line {
|
||||||
|
Ok(line) => line,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if line.starts_with('#') || line.starts_with('\r') || line.starts_with('\n') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, value) = {
|
||||||
|
let mut parts = line.splitn(2, '=');
|
||||||
|
let name = parts.next();
|
||||||
|
let value = parts.next();
|
||||||
|
match (name, value, parts.next()) {
|
||||||
|
(Some(name), Some(value), None) => (name.trim(), value.trim()),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if name == key {
|
||||||
|
return Some(Setting {
|
||||||
|
source: SettingSource::Admin,
|
||||||
|
value: value == "1" || value == "true",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a setting boolean from an environment variable
|
||||||
|
fn from_env(key: &str) -> Option<Self> {
|
||||||
|
env::var(format!("YUBIKEY_PIV_{key}"))
|
||||||
|
.ok()
|
||||||
|
.map(|value| Setting {
|
||||||
|
source: SettingSource::User,
|
||||||
|
value: value == "1" || value == "true",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
//! YubiKey PC/SC transactions
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
apdu::Response,
|
||||||
|
apdu::{Apdu, Ins, StatusWords},
|
||||||
|
consts::{CB_BUF_MAX, CB_OBJ_MAX},
|
||||||
|
error::{Error, Result},
|
||||||
|
mgm::MgmKey,
|
||||||
|
otp,
|
||||||
|
piv::{self, AlgorithmId, SlotId},
|
||||||
|
serialization::*,
|
||||||
|
yubikey::*,
|
||||||
|
Buffer, ObjectId,
|
||||||
|
};
|
||||||
|
use log::{error, trace};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::mgm::{DeviceConfig, DeviceInfo, Lock};
|
||||||
|
|
||||||
|
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.
|
||||||
|
pub(crate) struct Transaction<'tx> {
|
||||||
|
inner: pcsc::Transaction<'tx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'tx> Transaction<'tx> {
|
||||||
|
/// Create a new transaction with the given card.
|
||||||
|
pub fn new(card: &'tx mut pcsc::Card) -> Result<Self> {
|
||||||
|
Ok(Transaction {
|
||||||
|
inner: card.transaction()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transmit a single serialized APDU to the card this transaction is open
|
||||||
|
/// with and receive a response.
|
||||||
|
///
|
||||||
|
/// This is a wrapper for the raw `SCardTransmit` function and operates on
|
||||||
|
/// single APDU messages at a time. For larger messages that need to be
|
||||||
|
/// split into multiple APDUs, use the [`Transaction::transfer_data`]
|
||||||
|
/// method instead.
|
||||||
|
pub fn transmit(&self, send_buffer: &[u8], recv_len: usize) -> Result<Vec<u8>> {
|
||||||
|
trace!(">>> {:?}", send_buffer);
|
||||||
|
|
||||||
|
let mut recv_buffer = vec![0u8; recv_len];
|
||||||
|
|
||||||
|
let len = self
|
||||||
|
.inner
|
||||||
|
.transmit(send_buffer, recv_buffer.as_mut())?
|
||||||
|
.len();
|
||||||
|
|
||||||
|
recv_buffer.truncate(len);
|
||||||
|
Ok(recv_buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select PIV application.
|
||||||
|
pub fn select_piv_application(&self) -> Result<()> {
|
||||||
|
self.select_application(
|
||||||
|
piv::APPLET_ID,
|
||||||
|
piv::APPLET_NAME,
|
||||||
|
"failed selecting application",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select application.
|
||||||
|
pub fn select_application(
|
||||||
|
&self,
|
||||||
|
applet: &[u8],
|
||||||
|
applet_name: &'static str,
|
||||||
|
error: &'static str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let response = Apdu::new(Ins::SelectApplication)
|
||||||
|
.p1(0x04)
|
||||||
|
.data(applet)
|
||||||
|
.transmit(self, 0xFF)
|
||||||
|
.inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!("{}: {:04x}", error, response.status_words().code());
|
||||||
|
return Err(match response.status_words() {
|
||||||
|
StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
|
||||||
|
_ => Error::GenericError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the version of the PIV application installed on the YubiKey.
|
||||||
|
pub fn get_version(&self) -> Result<Version> {
|
||||||
|
// get version from device
|
||||||
|
let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?;
|
||||||
|
|
||||||
|
if !response.is_success() || response.data().is_empty() {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.data()[..3].try_into().map(Version::new)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get YubiKey device serial number.
|
||||||
|
pub fn get_serial(&self, version: Version) -> Result<Serial> {
|
||||||
|
match version.major {
|
||||||
|
// YK4 requires switching to the YK applet to retrieve the serial
|
||||||
|
4 => {
|
||||||
|
self.select_application(
|
||||||
|
otp::APPLET_ID,
|
||||||
|
otp::APPLET_NAME,
|
||||||
|
"failed selecting yk application",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
// TODO(tarcieri): still reselect the PIV applet in this case?
|
||||||
|
error!(
|
||||||
|
"failed retrieving serial number: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reselect the PIV applet
|
||||||
|
self.select_application(
|
||||||
|
piv::APPLET_ID,
|
||||||
|
piv::APPLET_NAME,
|
||||||
|
"failed selecting application",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
response.data().try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// YK5 implements getting the serial as a PIV applet command (0xf8)
|
||||||
|
5 => {
|
||||||
|
let response = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!(
|
||||||
|
"failed retrieving serial number: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.data().try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other versions unsupported
|
||||||
|
_ => Err(Error::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read metadata
|
||||||
|
pub(crate) fn get_metadata(&self, slot: SlotId) -> Result<piv::SlotMetadata> {
|
||||||
|
let response = Apdu::new(Ins::GetMetadata)
|
||||||
|
.p2(slot.into())
|
||||||
|
.transmit(self, CB_OBJ_MAX)?;
|
||||||
|
|
||||||
|
match response.status_words() {
|
||||||
|
StatusWords::Success => {
|
||||||
|
let buf = Buffer::new(response.data().into());
|
||||||
|
piv::SlotMetadata::try_from(buf)
|
||||||
|
}
|
||||||
|
StatusWords::ReferenceDataNotFoundError => Err(Error::NotFound),
|
||||||
|
// Requires firmware 5.2.3
|
||||||
|
StatusWords::NotSupportedError => Err(Error::NotSupported),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify device PIN.
|
||||||
|
pub fn verify_pin(&self, pin: &[u8]) -> Result<()> {
|
||||||
|
if pin.len() > CB_PIN_MAX {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query = Apdu::new(Ins::Verify);
|
||||||
|
query.params(0x00, 0x80);
|
||||||
|
|
||||||
|
// Empty pin means we are querying the number of retries. We set no data in this
|
||||||
|
// case; if we instead sent [0xff; CB_PIN_MAX] it would count as an attempt and
|
||||||
|
// decrease the retry counter.
|
||||||
|
if !pin.is_empty() {
|
||||||
|
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
|
||||||
|
data[0..pin.len()].copy_from_slice(pin);
|
||||||
|
query.data(data.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = query.transmit(self, 261)?;
|
||||||
|
|
||||||
|
match response.status_words() {
|
||||||
|
StatusWords::Success => Ok(()),
|
||||||
|
StatusWords::AuthBlockedError => Err(Error::WrongPin { tries: 0 }),
|
||||||
|
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the PIN.
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn change_ref(
|
||||||
|
&self,
|
||||||
|
action: ChangeRefAction,
|
||||||
|
current_pin: &[u8],
|
||||||
|
new_pin: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
if current_pin.len() > CB_PIN_MAX || new_pin.len() > CB_PIN_MAX {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PIN: u8 = 0x80;
|
||||||
|
const PUK: u8 = 0x81;
|
||||||
|
|
||||||
|
let templ = match action {
|
||||||
|
ChangeRefAction::ChangePin => [0, Ins::ChangeReference.code(), 0, PIN],
|
||||||
|
ChangeRefAction::ChangePuk => [0, Ins::ChangeReference.code(), 0, PUK],
|
||||||
|
ChangeRefAction::UnblockPin => [0, Ins::ResetRetry.code(), 0, PIN],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut indata = Zeroizing::new([0xff; CB_PIN_MAX * 2]);
|
||||||
|
indata[0..current_pin.len()].copy_from_slice(current_pin);
|
||||||
|
indata[CB_PIN_MAX..CB_PIN_MAX + new_pin.len()].copy_from_slice(new_pin);
|
||||||
|
|
||||||
|
let status_words = self
|
||||||
|
.transfer_data(&templ, indata.as_ref(), 0xFF)?
|
||||||
|
.status_words();
|
||||||
|
|
||||||
|
match status_words {
|
||||||
|
StatusWords::Success => Ok(()),
|
||||||
|
StatusWords::AuthBlockedError => Err(Error::PinLocked),
|
||||||
|
StatusWords::VerifyFailError { tries } => Err(Error::WrongPin { tries }),
|
||||||
|
_ => {
|
||||||
|
error!(
|
||||||
|
"failed changing pin, token response code: {:x}.",
|
||||||
|
status_words.code()
|
||||||
|
);
|
||||||
|
Err(Error::GenericError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the management key (MGM).
|
||||||
|
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
|
||||||
|
let p2 = if require_touch { 0xfe } else { 0xff };
|
||||||
|
|
||||||
|
let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
|
||||||
|
data.push(new_key.algorithm_id().into());
|
||||||
|
data.push(KEY_CARDMGM);
|
||||||
|
data.push(new_key.key_size());
|
||||||
|
data.extend_from_slice(new_key.as_ref());
|
||||||
|
|
||||||
|
let status_words = Apdu::new(Ins::SetMgmKey)
|
||||||
|
.params(0xff, p2)
|
||||||
|
.data(data)
|
||||||
|
.transmit(self, 261)?
|
||||||
|
.status_words();
|
||||||
|
|
||||||
|
if !status_words.is_success() {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a YubiKey operation which requires authentication.
|
||||||
|
///
|
||||||
|
/// This is the common backend for all public key encryption and signing
|
||||||
|
/// operations.
|
||||||
|
// TODO(tarcieri): refactor this to be less gross/coupled.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn authenticated_command(
|
||||||
|
&self,
|
||||||
|
sign_in: &[u8],
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key: SlotId,
|
||||||
|
decipher: bool,
|
||||||
|
) -> Result<Buffer> {
|
||||||
|
let in_len = sign_in.len();
|
||||||
|
let mut indata = [0u8; 1024];
|
||||||
|
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
|
||||||
|
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::Rsa1024 => {
|
||||||
|
if in_len != 128 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa2048 => {
|
||||||
|
if in_len != 256 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa3072 => {
|
||||||
|
if in_len != 384 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa4096 => {
|
||||||
|
if in_len != 512 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||||
|
let key_len = if let AlgorithmId::EccP256 = algorithm {
|
||||||
|
32
|
||||||
|
} else {
|
||||||
|
48
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!decipher && (in_len > key_len)) || (decipher && (in_len != (key_len * 2) + 1))
|
||||||
|
{
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::X25519 => {
|
||||||
|
if !decipher {
|
||||||
|
return Err(Error::NotSupported);
|
||||||
|
}
|
||||||
|
if in_len != 32 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Ed25519 => {
|
||||||
|
if decipher {
|
||||||
|
return Err(Error::NotSupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = if in_len < 0x80 {
|
||||||
|
1
|
||||||
|
} else if in_len < 0xff {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = Tlv::write_as(&mut indata, 0x7c, in_len + bytes + 3, |buf| {
|
||||||
|
assert_eq!(Tlv::write(buf, 0x82, &[]).expect("large enough"), 2);
|
||||||
|
assert_eq!(
|
||||||
|
Tlv::write(
|
||||||
|
&mut buf[2..],
|
||||||
|
match (algorithm, decipher) {
|
||||||
|
(AlgorithmId::EccP256, true)
|
||||||
|
| (AlgorithmId::EccP384, true)
|
||||||
|
| (AlgorithmId::X25519, true) => 0x85,
|
||||||
|
_ => 0x81,
|
||||||
|
},
|
||||||
|
sign_in
|
||||||
|
)
|
||||||
|
.expect("large enough"),
|
||||||
|
1 + bytes + in_len
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.transfer_data(&templ, &indata[..offset], 1024)
|
||||||
|
.inspect_err(|e| error!("sign command failed to communicate: {}", e))?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!("failed sign command with code {:x}", response.code());
|
||||||
|
|
||||||
|
if response.status_words() == StatusWords::SecurityStatusError {
|
||||||
|
return Err(Error::AuthenticationError);
|
||||||
|
} else {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, outer_tlv) = Tlv::parse(response.data())?;
|
||||||
|
|
||||||
|
// skip the first 7c tag
|
||||||
|
if outer_tlv.tag != 0x7c {
|
||||||
|
error!("failed parsing signature reply (0x7c byte)");
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, inner_tlv) = Tlv::parse(outer_tlv.value)?;
|
||||||
|
|
||||||
|
// skip the 82 tag
|
||||||
|
if inner_tlv.tag != 0x82 {
|
||||||
|
error!("failed parsing signature reply (0x82 byte)");
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Buffer::new(inner_tlv.value.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send/receive large amounts of data to/from the YubiKey, splitting long
|
||||||
|
/// messages into smaller APDU-sized messages (using the provided APDU
|
||||||
|
/// template to construct them), and then sending those via
|
||||||
|
/// [`Transaction::transmit`].
|
||||||
|
pub fn transfer_data(&self, templ: &[u8], in_data: &[u8], max_out: usize) -> Result<Response> {
|
||||||
|
let mut in_offset = 0;
|
||||||
|
let mut out_data = vec![];
|
||||||
|
let mut sw;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut this_size = 0xff;
|
||||||
|
|
||||||
|
let cla = if in_offset + 0xff < in_data.len() {
|
||||||
|
0x10
|
||||||
|
} else {
|
||||||
|
this_size = in_data.len() - in_offset;
|
||||||
|
templ[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("going to send {} bytes in this go", this_size);
|
||||||
|
|
||||||
|
let response = Apdu::new(templ[1])
|
||||||
|
.cla(cla)
|
||||||
|
.params(templ[2], templ[3])
|
||||||
|
.data(&in_data[in_offset..(in_offset + this_size)])
|
||||||
|
.transmit(self, 261)?;
|
||||||
|
|
||||||
|
sw = response.status_words();
|
||||||
|
|
||||||
|
match sw {
|
||||||
|
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
|
||||||
|
// TODO(tarcieri): is this really OK?
|
||||||
|
_ => return Ok(Response::new(sw, out_data)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
|
||||||
|
error!(
|
||||||
|
"output buffer too small: wanted to write {}, max was {}",
|
||||||
|
out_data.len() - response.data().len(),
|
||||||
|
max_out
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
||||||
|
|
||||||
|
in_offset += this_size;
|
||||||
|
if in_offset >= in_data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let StatusWords::BytesRemaining { len } = sw {
|
||||||
|
trace!("The card indicates there is {} bytes more data for us", len);
|
||||||
|
|
||||||
|
let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?;
|
||||||
|
sw = response.status_words();
|
||||||
|
|
||||||
|
match sw {
|
||||||
|
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
|
||||||
|
_ => return Ok(Response::new(sw, vec![])),
|
||||||
|
}
|
||||||
|
|
||||||
|
if out_data.len() + response.data().len() > max_out {
|
||||||
|
error!(
|
||||||
|
"output buffer too small: wanted to write {}, max was {}",
|
||||||
|
out_data.len() + response.data().len(),
|
||||||
|
max_out
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(sw, out_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch an object.
|
||||||
|
pub fn fetch_object(&self, object_id: ObjectId) -> Result<Buffer> {
|
||||||
|
let mut indata = [0u8; 5];
|
||||||
|
let templ = [0, Ins::GetData.code(), 0x3f, 0xff];
|
||||||
|
|
||||||
|
let mut inlen = indata.len();
|
||||||
|
let indata_remaining = set_object(object_id, &mut indata);
|
||||||
|
inlen -= indata_remaining.len();
|
||||||
|
|
||||||
|
let response = self.transfer_data(&templ, &indata[..inlen], CB_BUF_MAX)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
if response.status_words() == StatusWords::NotFoundError {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
} else {
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (remaining, tlv) = Tlv::parse(response.data())?;
|
||||||
|
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
error!(
|
||||||
|
"invalid length indicated in object: total len is {} but indicated length is {}",
|
||||||
|
tlv.value.len() + remaining.len(),
|
||||||
|
tlv.value.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Zeroizing::new(tlv.value.to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save an object.
|
||||||
|
pub fn save_object(&self, object_id: ObjectId, indata: &[u8]) -> Result<()> {
|
||||||
|
let templ = [0, Ins::PutData.code(), 0x3f, 0xff];
|
||||||
|
|
||||||
|
// TODO(tarcieri): replace with vector
|
||||||
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
|
|
||||||
|
if indata.len() > CB_OBJ_MAX {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut len = data.len();
|
||||||
|
let mut data_remaining = set_object(object_id, &mut data);
|
||||||
|
|
||||||
|
let offset = Tlv::write(data_remaining, 0x53, indata)?;
|
||||||
|
data_remaining = &mut data_remaining[offset..];
|
||||||
|
len -= data_remaining.len();
|
||||||
|
|
||||||
|
let status_words = self
|
||||||
|
.transfer_data(&templ, &data[..len], 255)?
|
||||||
|
.status_words();
|
||||||
|
|
||||||
|
match status_words {
|
||||||
|
StatusWords::Success => Ok(()),
|
||||||
|
StatusWords::SecurityStatusError => Err(Error::AuthenticationError),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write configuration to the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn write_config(
|
||||||
|
&mut self,
|
||||||
|
version: Version,
|
||||||
|
config: DeviceConfig,
|
||||||
|
current_lock: Option<Lock>,
|
||||||
|
new_lock: Option<Lock>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if version
|
||||||
|
< (Version {
|
||||||
|
major: 5,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(Error::NotSupported);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = config.as_tlv(true, current_lock, new_lock)?;
|
||||||
|
|
||||||
|
let response = Apdu::new(Ins::WriteConfig)
|
||||||
|
.params(0x00, 0x00)
|
||||||
|
.data(&data)
|
||||||
|
.transmit(self, 2)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!(
|
||||||
|
"Unable to write_config: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write configuration to the YubiKey
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn read_config(&mut self) -> Result<DeviceInfo> {
|
||||||
|
let mut data = [0u8; CB_BUF_MAX];
|
||||||
|
let mut len = data.len();
|
||||||
|
let data_remaining = &mut data[..];
|
||||||
|
|
||||||
|
len -= data_remaining.len();
|
||||||
|
let response = Apdu::new(Ins::ReadConfig)
|
||||||
|
.params(0x00, 0x00)
|
||||||
|
.data(&data[..len])
|
||||||
|
.transmit(self, CB_BUF_MAX + 2)?;
|
||||||
|
|
||||||
|
if !response.is_success() {
|
||||||
|
error!(
|
||||||
|
"Unable to read configuration: {:04x}",
|
||||||
|
response.status_words().code()
|
||||||
|
);
|
||||||
|
return Err(Error::GenericError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = response.data();
|
||||||
|
DeviceInfo::parse(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
-2354
File diff suppressed because it is too large
Load Diff
+627
-2380
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
|||||||
# Copyright (c) 2014-2016 Yubico AB
|
|
||||||
# All rights reserved.
|
|
||||||
#
|
|
||||||
# Redistribution and use in source and binary forms, with or without
|
|
||||||
# modification, are permitted provided that the following conditions are
|
|
||||||
# met:
|
|
||||||
#
|
|
||||||
# * Redistributions of source code must retain the above copyright
|
|
||||||
# notice, this list of conditions and the following disclaimer.
|
|
||||||
#
|
|
||||||
# * Redistributions in binary form must reproduce the above
|
|
||||||
# copyright notice, this list of conditions and the following
|
|
||||||
# disclaimer in the documentation and/or other materials provided
|
|
||||||
# with the distribution.
|
|
||||||
#
|
|
||||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
AM_CFLAGS = $(WARN_CFLAGS) @CHECK_CFLAGS@ $(OPENSSL_CFLAGS)
|
|
||||||
AM_CPPFLAGS = -I$(top_srcdir)/lib -I$(top_builddir)/lib $(OPENSSL_CFLAGS) $(PCSC_CFLAGS)
|
|
||||||
|
|
||||||
AM_LDFLAGS = @CHECK_LIBS@
|
|
||||||
|
|
||||||
if COMPILER_CLANG
|
|
||||||
AM_LDFLAGS += -no-fast-install
|
|
||||||
else
|
|
||||||
AM_LDFLAGS += -no-install
|
|
||||||
endif
|
|
||||||
|
|
||||||
LDADD = ../libykpiv.la $(OPENSSL_LIBS)
|
|
||||||
|
|
||||||
api_LDADD = ../../tool/libpiv_util.la
|
|
||||||
api_SOURCES = api.c
|
|
||||||
check_PROGRAMS = basic parse_key api
|
|
||||||
TESTS = $(check_PROGRAMS)
|
|
||||||
|
|
||||||
LOG_COMPILER = $(VALGRIND)
|
|
||||||
-993
@@ -1,993 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014-2016 Yubico AB
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are
|
|
||||||
* met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above
|
|
||||||
* copyright notice, this list of conditions and the following
|
|
||||||
* disclaimer in the documentation and/or other materials provided
|
|
||||||
* with the distribution.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ykpiv.h"
|
|
||||||
#include "internal.h"
|
|
||||||
#include "../../tool/openssl-compat.h"
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <check.h>
|
|
||||||
|
|
||||||
#ifdef __MINGW32__
|
|
||||||
#define dprintf(fd, ...) fprintf(stdout, __VA_ARGS__)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int destruction_confirmed(void);
|
|
||||||
|
|
||||||
// only defined in libcheck 0.11+ (linux distros still shipping 0.10)
|
|
||||||
#ifndef ck_assert_ptr_nonnull
|
|
||||||
#define ck_assert_ptr_nonnull(a) ck_assert((a) != NULL)
|
|
||||||
#endif
|
|
||||||
#ifndef ck_assert_mem_eq
|
|
||||||
#define ck_assert_mem_eq(a,b,n) ck_assert(memcmp((a), (b), (n)) == 0)
|
|
||||||
#endif
|
|
||||||
// only defined in libcheck 0.10+ (RHEL7 is still shipping 0.9)
|
|
||||||
#ifndef ck_assert_ptr_eq
|
|
||||||
#define ck_assert_ptr_eq(a,b) ck_assert((void *)(a) == (void *)(b))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ykpiv_state *g_state;
|
|
||||||
const uint8_t g_cert[] = {
|
|
||||||
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
|
|
||||||
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
|
|
||||||
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
|
|
||||||
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
|
|
||||||
"0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK0123456789ABCDEFGHIK"
|
|
||||||
};
|
|
||||||
|
|
||||||
void setup(void) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
// Require user confirmation to continue, since this test suite will clear
|
|
||||||
// any data stored on connected keys.
|
|
||||||
if (!destruction_confirmed())
|
|
||||||
exit(77); // exit code 77 == skipped tests
|
|
||||||
|
|
||||||
res = ykpiv_init(&g_state, true);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(g_state);
|
|
||||||
|
|
||||||
res = ykpiv_connect(g_state, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
void teardown(void) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
// This is the expected case, if the allocator test ran, since it de-inits.
|
|
||||||
if (NULL == g_state)
|
|
||||||
return;
|
|
||||||
|
|
||||||
res = ykpiv_disconnect(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_done(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef HW_TESTS
|
|
||||||
START_TEST(test_devicemodel) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
ykpiv_devmodel model;
|
|
||||||
char version[256];
|
|
||||||
char reader_buf[2048];
|
|
||||||
size_t num_readers = sizeof(reader_buf);
|
|
||||||
|
|
||||||
res = ykpiv_get_version(g_state, version, sizeof(version));
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
fprintf(stderr, "Version: %s\n", version);
|
|
||||||
model = ykpiv_util_devicemodel(g_state);
|
|
||||||
fprintf(stdout, "Model: %u\n", model);
|
|
||||||
ck_assert(model == DEVTYPE_YK4 || model == DEVTYPE_NEOr3);
|
|
||||||
|
|
||||||
res = ykpiv_list_readers(g_state, reader_buf, &num_readers);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_gt(num_readers, 0);
|
|
||||||
if (model == DEVTYPE_YK4) {
|
|
||||||
ck_assert_ptr_nonnull(strstr(reader_buf, "Yubikey 4"));
|
|
||||||
ck_assert(version[0] == '4'); // Verify app version 4.x
|
|
||||||
ck_assert(version[1] == '.');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ck_assert_ptr_nonnull(strstr(reader_buf, "Yubikey NEO"));
|
|
||||||
ck_assert(version[0] == '1'); // Verify app version 1.x
|
|
||||||
ck_assert(version[1] == '.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_get_set_cardid) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
ykpiv_cardid set_id;
|
|
||||||
ykpiv_cardid get_id;
|
|
||||||
|
|
||||||
memset(&set_id.data, 'i', sizeof(set_id.data));
|
|
||||||
memset(&get_id.data, 0, sizeof(get_id.data));
|
|
||||||
|
|
||||||
res = ykpiv_util_set_cardid(g_state, &set_id);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_util_get_cardid(g_state, &get_id);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_mem_eq(&set_id.data, &get_id.data, sizeof(set_id.data));
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_list_readers) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
char reader_buf[2048];
|
|
||||||
size_t num_readers = sizeof(reader_buf);
|
|
||||||
char *reader_ptr;
|
|
||||||
res = ykpiv_list_readers(g_state, reader_buf, &num_readers);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_gt(num_readers, 0);
|
|
||||||
for(reader_ptr = reader_buf; *reader_ptr != '\0'; reader_ptr += strlen(reader_ptr) + 1) {
|
|
||||||
fprintf(stdout, "Found device: %s\n", reader_ptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_read_write_list_delete_cert) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
uint8_t *read_cert = NULL;
|
|
||||||
size_t read_cert_len = 0;
|
|
||||||
|
|
||||||
{
|
|
||||||
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(read_cert);
|
|
||||||
ck_assert_int_eq(read_cert_len, sizeof(g_cert));
|
|
||||||
ck_assert_mem_eq(g_cert, read_cert, sizeof(g_cert));
|
|
||||||
|
|
||||||
res = ykpiv_util_free(g_state, read_cert);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ykpiv_key *keys = NULL;
|
|
||||||
size_t data_len;
|
|
||||||
uint8_t key_count;
|
|
||||||
res = ykpiv_util_list_keys(g_state, &key_count, &keys, &data_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(keys);
|
|
||||||
ck_assert_int_gt(key_count, 0);
|
|
||||||
|
|
||||||
res = ykpiv_util_free(g_state, keys);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
res = ykpiv_util_delete_cert(g_state, YKPIV_KEY_AUTHENTICATION);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_GENERIC_ERROR);
|
|
||||||
|
|
||||||
res = ykpiv_util_free(g_state, read_cert);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
#include <openssl/des.h>
|
|
||||||
#include <openssl/pem.h>
|
|
||||||
#include <openssl/pkcs12.h>
|
|
||||||
#include <openssl/rand.h>
|
|
||||||
|
|
||||||
// RSA2048 private key, generated with: `openssl genrsa 2048 -out private.pem`
|
|
||||||
static const char *private_key_pem =
|
|
||||||
"-----BEGIN RSA PRIVATE KEY-----\n"
|
|
||||||
"MIIEpAIBAAKCAQEAwVUwmVbc+ffOy2+RivxBpgleTVN6bUa0q7jNYB+AseFQYaYq\n"
|
|
||||||
"EGfa+VGdxSGo+8DV1KT9+fNEd5243gXn/tcjtMItKeB+oAQc64s9lIFlYuR8bpq1\n"
|
|
||||||
"ibr33iW2elnnv9mpecqohdCVwM2McWveoPyb7MwlwVuhqexOzJO29bqJcazLbtkf\n"
|
|
||||||
"ZETK0oBx53/ylA4Y6nE9Pa46jW2qhj+KShf1iBg+gAyt3eI+wI2Wmub1WxLLH8D2\n"
|
|
||||||
"w+kow8QhQOa8dHCkRRw771JxVO5+d+Y/Y+x9B1HgF4q0q9xUlhWLK2TR4ChBFzXe\n"
|
|
||||||
"47sAHsSqi/pl5JbwYrHPOE/VEBLukmjL8NFCSQIDAQABAoIBADmEyOK2DyRnb6Ti\n"
|
|
||||||
"2qBJEJb/boj+7wuX36S/ZIrWlIlXiXyj3RvoaiOG/rNpokbURknvlIhKsfIMgLW9\n"
|
|
||||||
"eBo/k6Xxp1IwMjwVPS1uzbFjFfDoHYUijiQd9iSnf7TDDsnrThqoCp9VQViNTt1n\n"
|
|
||||||
"xGKNBS7cRddTFbPiVEdVIzfUeZPR2oRrc4maBCRCrQgg8WNknawmc8zhkf2NiPj3\n"
|
|
||||||
"tWLQHMy1/MgW2W1LM9sgzllEtS5CZUnyGy2HbbhS2tbZ6j9kPzOp0pPxxTTzJmmV\n"
|
|
||||||
"fi1vkJcVW4+MdXjWmhALcPA4dO7Y2Ljiu6VxIxQORRO1DyiCjAs1AVMQxgPAAY41\n"
|
|
||||||
"YR4Q2EkCgYEA4zE0oytg97aVaBY9CKi7/PqR+NI/uEvfoQCnT+ddaJgp/qsspuXo\n"
|
|
||||||
"tJt94p13ANd8O7suqQTVNvbZq1rX10xQjJZ9nvlqQa6iHkN6Epq31XBK3Z+acjIV\n"
|
|
||||||
"A2rAgKBByjz9/CpKHqnOsrTWU1Y7x416IG4BZt42hHdrxRH98/wiDH8CgYEA2djj\n"
|
|
||||||
"AjwgK+MwDnshwT1NNgCSP/2ZHatBAykZ5BCs9BJ6MNYqqXVGYoqs5Z5kSkow+Db3\n"
|
|
||||||
"pipkEieo5w2Rd5zkolTThaVCvRkSe5wRiBpZhaeY+b0UFwavGCb6zU/MmJIMDPiI\n"
|
|
||||||
"2iRGeCXgQDvIS/icIqzbTtp6dZaoMgG7LdSR7TcCgYBtxGhaLas8A8tL7vKuLFgn\n"
|
|
||||||
"cij0vyBqOr5hW596y54l2t7vXGTGfm5gVIAN7WaB0ZsEgPuaTet2Eu44DDwcmZKR\n"
|
|
||||||
"WmR3Wqor8eQCGzfvpTEMvqRtT5+fbPMaI4m+m68ttyo/m28UQZbMYPLscM2RLJnE\n"
|
|
||||||
"8WFcAiD0/33iST8ZksggoQKBgQDE/7Yhsj+hkHxHzB+1QPtOp2uaBHnvc4uCESwB\n"
|
|
||||||
"qvbMbN0kxrejsJLqz98UcozdBYSNIiAHmvQN2uGJuCJhGXdEORNjGxRkLoUhVPwh\n"
|
|
||||||
"qTplfC8BQHQncnrqi21oNw6ctg3BuQsAwaccRZwqWiWCVhrT3J8iCr6NEaWeOySK\n"
|
|
||||||
"iF1CNwKBgQCRpkkZArlccwS0kMvkK+tQ1rG2xWm7c05G34gP/g6dHFRy0gPNMyvi\n"
|
|
||||||
"SkiLTJmQIEZSAEiq0FFgcVwM6o556ftvQZuwDp5rHUbwqnHCpMJKpD9aJpStvfPi\n"
|
|
||||||
"4p9JbYdaGqnq4eoNKemmGnbUof0dR9Zr0lGmcMTwwzBib+4E1d7soA==\n"
|
|
||||||
"-----END RSA PRIVATE KEY-----\n";
|
|
||||||
|
|
||||||
// Certificate signed with key above:
|
|
||||||
// `openssl req -x509 -key private.pem -out cert.pem -subj "/CN=bar/OU=test/O=example.com/" -new`
|
|
||||||
static const char *certificate_pem =
|
|
||||||
"-----BEGIN CERTIFICATE-----\n"
|
|
||||||
"MIIC5zCCAc+gAwIBAgIJAOq8A/cmpxF5MA0GCSqGSIb3DQEBCwUAMDMxDDAKBgNV\n"
|
|
||||||
"BAMMA2JhcjENMAsGA1UECwwEdGVzdDEUMBIGA1UECgwLZXhhbXBsZS5jb20wHhcN\n"
|
|
||||||
"MTcwODAzMTE1MDI2WhcNMTgwODAzMTE1MDI2WjAzMQwwCgYDVQQDDANiYXIxDTAL\n"
|
|
||||||
"BgNVBAsMBHRlc3QxFDASBgNVBAoMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0B\n"
|
|
||||||
"AQEFAAOCAQ8AMIIBCgKCAQEAwVUwmVbc+ffOy2+RivxBpgleTVN6bUa0q7jNYB+A\n"
|
|
||||||
"seFQYaYqEGfa+VGdxSGo+8DV1KT9+fNEd5243gXn/tcjtMItKeB+oAQc64s9lIFl\n"
|
|
||||||
"YuR8bpq1ibr33iW2elnnv9mpecqohdCVwM2McWveoPyb7MwlwVuhqexOzJO29bqJ\n"
|
|
||||||
"cazLbtkfZETK0oBx53/ylA4Y6nE9Pa46jW2qhj+KShf1iBg+gAyt3eI+wI2Wmub1\n"
|
|
||||||
"WxLLH8D2w+kow8QhQOa8dHCkRRw771JxVO5+d+Y/Y+x9B1HgF4q0q9xUlhWLK2TR\n"
|
|
||||||
"4ChBFzXe47sAHsSqi/pl5JbwYrHPOE/VEBLukmjL8NFCSQIDAQABMA0GCSqGSIb3\n"
|
|
||||||
"DQEBCwUAA4IBAQCamrwdEhNmY2GCQWq6U90Q3XQT6w0HHW/JmtuGeF+BTpVr12gN\n"
|
|
||||||
"/UvEXTo9geWbGcCTjaMMURTa7mUjVUIttIWEVHZMKqBuvsUM1RcuOEX/vitaJJ8K\n"
|
|
||||||
"Sw4upjCNa3ZxUXmSA1FBixZgDzFqjEeSiaJjMU0yX5W2p1T4iNYtF3YqzMF5AWSI\n"
|
|
||||||
"qCO7gP5ezPyg5kDnrO3V7DBgnDiqawq7Pyn9DynKNULX/hc1yls/R+ebb2u8Z+h5\n"
|
|
||||||
"W4YXbzGZb8qdT27qIZaHD638tL6liLkI6UE4KCXH8X8e3fqdbmqvwrq403nOGmsP\n"
|
|
||||||
"cbJb2PEXibNEQG234riKxm7x7vNDLL79Jwtc\n"
|
|
||||||
"-----END CERTIFICATE-----\n";
|
|
||||||
|
|
||||||
static bool set_component(unsigned char *in_ptr, const BIGNUM *bn, int element_len) {
|
|
||||||
int real_len = BN_num_bytes(bn);
|
|
||||||
|
|
||||||
if(real_len > element_len) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memset(in_ptr, 0, (size_t)(element_len - real_len));
|
|
||||||
in_ptr += element_len - real_len;
|
|
||||||
BN_bn2bin(bn, in_ptr);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool prepare_rsa_signature(const unsigned char *in, unsigned int in_len, unsigned char *out, unsigned int *out_len, int nid) {
|
|
||||||
X509_SIG *digestInfo;
|
|
||||||
X509_ALGOR *algor;
|
|
||||||
ASN1_OCTET_STRING *digest;
|
|
||||||
unsigned char data[1024];
|
|
||||||
|
|
||||||
memcpy(data, in, in_len);
|
|
||||||
|
|
||||||
digestInfo = X509_SIG_new();
|
|
||||||
X509_SIG_getm(digestInfo, &algor, &digest);
|
|
||||||
algor->algorithm = OBJ_nid2obj(nid);
|
|
||||||
X509_ALGOR_set0(algor, OBJ_nid2obj(nid), V_ASN1_NULL, NULL);
|
|
||||||
ASN1_STRING_set(digest, data, in_len);
|
|
||||||
*out_len = (unsigned int)i2d_X509_SIG(digestInfo, &out);
|
|
||||||
X509_SIG_free(digestInfo);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void import_key(unsigned char slot, unsigned char pin_policy) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
{
|
|
||||||
unsigned char pp = pin_policy;
|
|
||||||
unsigned char tp = YKPIV_TOUCHPOLICY_DEFAULT;
|
|
||||||
EVP_PKEY *private_key = NULL;
|
|
||||||
BIO *bio = NULL;
|
|
||||||
RSA *rsa_private_key = NULL;
|
|
||||||
unsigned char e[4];
|
|
||||||
unsigned char p[128];
|
|
||||||
unsigned char q[128];
|
|
||||||
unsigned char dmp1[128];
|
|
||||||
unsigned char dmq1[128];
|
|
||||||
unsigned char iqmp[128];
|
|
||||||
int element_len = 128;
|
|
||||||
const BIGNUM *bn_e, *bn_p, *bn_q, *bn_dmp1, *bn_dmq1, *bn_iqmp;
|
|
||||||
|
|
||||||
bio = BIO_new_mem_buf(private_key_pem, strlen(private_key_pem));
|
|
||||||
private_key = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
|
||||||
ck_assert_ptr_nonnull(private_key);
|
|
||||||
BIO_free(bio);
|
|
||||||
rsa_private_key = EVP_PKEY_get1_RSA(private_key);
|
|
||||||
ck_assert_ptr_nonnull(rsa_private_key);
|
|
||||||
RSA_get0_key(rsa_private_key, NULL, &bn_e, NULL);
|
|
||||||
RSA_get0_factors(rsa_private_key, &bn_p, &bn_q);
|
|
||||||
RSA_get0_crt_params(rsa_private_key, &bn_dmp1, &bn_dmq1, &bn_iqmp);
|
|
||||||
ck_assert(set_component(e, bn_e, 3));
|
|
||||||
ck_assert(set_component(p, bn_p, element_len));
|
|
||||||
ck_assert(set_component(q, bn_q, element_len));
|
|
||||||
ck_assert(set_component(dmp1, bn_dmp1, element_len));
|
|
||||||
ck_assert(set_component(dmq1, bn_dmq1, element_len));
|
|
||||||
ck_assert(set_component(iqmp, bn_iqmp, element_len));
|
|
||||||
|
|
||||||
// Try wrong algorithm, fail.
|
|
||||||
res = ykpiv_import_private_key(g_state,
|
|
||||||
slot,
|
|
||||||
YKPIV_ALGO_RSA1024,
|
|
||||||
p, element_len,
|
|
||||||
q, element_len,
|
|
||||||
dmp1, element_len,
|
|
||||||
dmq1, element_len,
|
|
||||||
iqmp, element_len,
|
|
||||||
NULL, 0,
|
|
||||||
pp, tp);
|
|
||||||
ck_assert_int_eq(res, YKPIV_ALGORITHM_ERROR);
|
|
||||||
|
|
||||||
// Try right algorithm
|
|
||||||
res = ykpiv_import_private_key(g_state,
|
|
||||||
slot,
|
|
||||||
YKPIV_ALGO_RSA2048,
|
|
||||||
p, element_len,
|
|
||||||
q, element_len,
|
|
||||||
dmp1, element_len,
|
|
||||||
dmq1, element_len,
|
|
||||||
iqmp, element_len,
|
|
||||||
NULL, 0,
|
|
||||||
pp, tp);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
RSA_free(rsa_private_key);
|
|
||||||
EVP_PKEY_free(private_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use imported key to decrypt a thing. See that it works.
|
|
||||||
{
|
|
||||||
BIO *bio = NULL;
|
|
||||||
X509 *cert = NULL;
|
|
||||||
EVP_PKEY *pub_key = NULL;
|
|
||||||
unsigned char secret[32];
|
|
||||||
unsigned char secret2[32];
|
|
||||||
unsigned char data[256];
|
|
||||||
int len;
|
|
||||||
size_t len2 = sizeof(data);
|
|
||||||
RSA *rsa = NULL;
|
|
||||||
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
|
|
||||||
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
||||||
ck_assert_ptr_nonnull(cert);
|
|
||||||
BIO_free(bio);
|
|
||||||
pub_key = X509_get_pubkey(cert);
|
|
||||||
ck_assert_ptr_nonnull(pub_key);
|
|
||||||
rsa = EVP_PKEY_get1_RSA(pub_key);
|
|
||||||
ck_assert_ptr_nonnull(rsa);
|
|
||||||
EVP_PKEY_free(pub_key);
|
|
||||||
|
|
||||||
ck_assert_int_gt(RAND_bytes(secret, sizeof(secret)), 0);
|
|
||||||
len = RSA_public_encrypt(sizeof(secret), secret, data, rsa, RSA_PKCS1_PADDING);
|
|
||||||
ck_assert_int_ge(len, 0);
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_decipher_data(g_state, data, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, slot);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
len = RSA_padding_check_PKCS1_type_2(secret2, sizeof(secret2), data + 1, len2 - 1, RSA_size(rsa));
|
|
||||||
ck_assert_int_eq(len, sizeof(secret));
|
|
||||||
ck_assert_int_eq(memcmp(secret, secret2, sizeof(secret)), 0);
|
|
||||||
RSA_free(rsa);
|
|
||||||
X509_free(cert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
START_TEST(test_import_key) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
import_key(0x9a, YKPIV_PINPOLICY_DEFAULT);
|
|
||||||
|
|
||||||
// Verify certificate
|
|
||||||
{
|
|
||||||
BIO *bio = NULL;
|
|
||||||
X509 *cert = NULL;
|
|
||||||
RSA *rsa = NULL;
|
|
||||||
EVP_PKEY *pub_key = NULL;
|
|
||||||
const EVP_MD *md = EVP_sha256();
|
|
||||||
EVP_MD_CTX *mdctx;
|
|
||||||
|
|
||||||
unsigned char signature[1024];
|
|
||||||
unsigned char encoded[1024];
|
|
||||||
unsigned char data[1024];
|
|
||||||
unsigned char signinput[1024];
|
|
||||||
unsigned char rand[128];
|
|
||||||
|
|
||||||
size_t sig_len = sizeof(signature);
|
|
||||||
size_t padlen = 256;
|
|
||||||
unsigned int enc_len;
|
|
||||||
unsigned int data_len;
|
|
||||||
|
|
||||||
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
|
|
||||||
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
||||||
ck_assert_ptr_nonnull(cert);
|
|
||||||
BIO_free(bio);
|
|
||||||
pub_key = X509_get_pubkey(cert);
|
|
||||||
ck_assert_ptr_nonnull(pub_key);
|
|
||||||
rsa = EVP_PKEY_get1_RSA(pub_key);
|
|
||||||
ck_assert_ptr_nonnull(rsa);
|
|
||||||
EVP_PKEY_free(pub_key);
|
|
||||||
|
|
||||||
ck_assert_int_gt(RAND_bytes(rand, 128), 0);
|
|
||||||
mdctx = EVP_MD_CTX_create();
|
|
||||||
EVP_DigestInit_ex(mdctx, md, NULL);
|
|
||||||
EVP_DigestUpdate(mdctx, rand, 128);
|
|
||||||
EVP_DigestFinal_ex(mdctx, data, &data_len);
|
|
||||||
|
|
||||||
prepare_rsa_signature(data, data_len, encoded, &enc_len, EVP_MD_type(md));
|
|
||||||
ck_assert_int_ne(RSA_padding_add_PKCS1_type_1(signinput, padlen, encoded, enc_len), 0);
|
|
||||||
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9a);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
ck_assert_int_eq(RSA_verify(EVP_MD_type(md), data, data_len, signature, sig_len, rsa), 1);
|
|
||||||
|
|
||||||
RSA_free(rsa);
|
|
||||||
X509_free(cert);
|
|
||||||
EVP_MD_CTX_destroy(mdctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that imported key can not be attested
|
|
||||||
{
|
|
||||||
unsigned char attest[2048];
|
|
||||||
size_t attest_len = sizeof(attest);
|
|
||||||
ykpiv_devmodel model;
|
|
||||||
model = ykpiv_util_devicemodel(g_state);
|
|
||||||
res = ykpiv_attest(g_state, 0x9a, attest, &attest_len);
|
|
||||||
if (model == DEVTYPE_YK4) {
|
|
||||||
ck_assert_int_eq(res, YKPIV_GENERIC_ERROR);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ck_assert_int_eq(res, YKPIV_NOT_SUPPORTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_pin_policy_always) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
{
|
|
||||||
ykpiv_devmodel model;
|
|
||||||
model = ykpiv_util_devicemodel(g_state);
|
|
||||||
// Only works with YK4. NEO should skip.
|
|
||||||
if (model != DEVTYPE_YK4) {
|
|
||||||
fprintf(stderr, "WARNING: Not supported with Yubikey NEO. Test skipped.\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import_key(0x9e, YKPIV_PINPOLICY_ALWAYS);
|
|
||||||
|
|
||||||
// Verify certificate
|
|
||||||
{
|
|
||||||
BIO *bio = NULL;
|
|
||||||
X509 *cert = NULL;
|
|
||||||
RSA *rsa = NULL;
|
|
||||||
EVP_PKEY *pub_key = NULL;
|
|
||||||
const EVP_MD *md = EVP_sha256();
|
|
||||||
EVP_MD_CTX *mdctx;
|
|
||||||
|
|
||||||
unsigned char signature[1024];
|
|
||||||
unsigned char encoded[1024];
|
|
||||||
unsigned char data[1024];
|
|
||||||
unsigned char signinput[1024];
|
|
||||||
unsigned char rand[128];
|
|
||||||
|
|
||||||
size_t sig_len = sizeof(signature);
|
|
||||||
size_t padlen = 256;
|
|
||||||
unsigned int enc_len;
|
|
||||||
unsigned int data_len;
|
|
||||||
|
|
||||||
bio = BIO_new_mem_buf(certificate_pem, strlen(certificate_pem));
|
|
||||||
cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
||||||
ck_assert_ptr_nonnull(cert);
|
|
||||||
BIO_free(bio);
|
|
||||||
pub_key = X509_get_pubkey(cert);
|
|
||||||
ck_assert_ptr_nonnull(pub_key);
|
|
||||||
rsa = EVP_PKEY_get1_RSA(pub_key);
|
|
||||||
ck_assert_ptr_nonnull(rsa);
|
|
||||||
EVP_PKEY_free(pub_key);
|
|
||||||
|
|
||||||
ck_assert_int_gt(RAND_bytes(rand, 128), 0);
|
|
||||||
mdctx = EVP_MD_CTX_create();
|
|
||||||
EVP_DigestInit_ex(mdctx, md, NULL);
|
|
||||||
EVP_DigestUpdate(mdctx, rand, 128);
|
|
||||||
EVP_DigestFinal_ex(mdctx, data, &data_len);
|
|
||||||
|
|
||||||
prepare_rsa_signature(data, data_len, encoded, &enc_len, EVP_MD_type(md));
|
|
||||||
ck_assert_int_ne(RSA_padding_add_PKCS1_type_1(signinput, padlen, encoded, enc_len), 0);
|
|
||||||
|
|
||||||
// Sign without verify: fail
|
|
||||||
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
|
|
||||||
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
|
|
||||||
|
|
||||||
// Sign with verify: pass
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Sign again without verify: fail
|
|
||||||
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
|
|
||||||
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
|
|
||||||
|
|
||||||
// Sign again with verify: pass
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_sign_data(g_state, signinput, padlen, signature, &sig_len, YKPIV_ALGO_RSA2048, 0x9e);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
ck_assert_int_eq(RSA_verify(EVP_MD_type(md), data, data_len, signature, sig_len, rsa), 1);
|
|
||||||
|
|
||||||
RSA_free(rsa);
|
|
||||||
X509_free(cert);
|
|
||||||
EVP_MD_CTX_destroy(mdctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_generate_key) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
uint8_t *mod, *exp;
|
|
||||||
size_t mod_len, exp_len;
|
|
||||||
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_util_generate_key(g_state,
|
|
||||||
YKPIV_KEY_AUTHENTICATION,
|
|
||||||
YKPIV_ALGO_RSA2048,
|
|
||||||
YKPIV_PINPOLICY_DEFAULT,
|
|
||||||
YKPIV_TOUCHPOLICY_DEFAULT,
|
|
||||||
&mod,
|
|
||||||
&mod_len,
|
|
||||||
&exp,
|
|
||||||
&exp_len,
|
|
||||||
NULL,
|
|
||||||
NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_util_free(g_state, mod);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_util_free(g_state, exp);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify that imported key can be attested
|
|
||||||
{
|
|
||||||
ykpiv_devmodel model;
|
|
||||||
unsigned char attest[2048];
|
|
||||||
size_t attest_len = sizeof(attest);
|
|
||||||
model = ykpiv_util_devicemodel(g_state);
|
|
||||||
res = ykpiv_attest(g_state, YKPIV_KEY_AUTHENTICATION, attest, &attest_len);
|
|
||||||
// Only works with YK4. NEO should error.
|
|
||||||
if (model == DEVTYPE_YK4) {
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_gt(attest_len, 0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ck_assert_int_eq(res, YKPIV_NOT_SUPPORTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_authenticate) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
const char *default_mgm_key = "010203040506070801020304050607080102030405060708";
|
|
||||||
const char *mgm_key = "112233445566778811223344556677881122334455667788";
|
|
||||||
const char *weak_mgm_key = "FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE";
|
|
||||||
unsigned char key[24];
|
|
||||||
size_t key_len = sizeof(key);
|
|
||||||
|
|
||||||
// Try new key, fail.
|
|
||||||
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
|
|
||||||
|
|
||||||
// Try default key, succeed
|
|
||||||
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify same key works twice
|
|
||||||
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Change to new key
|
|
||||||
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_set_mgmkey(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Try new key, succeed.
|
|
||||||
res = ykpiv_hex_decode(mgm_key, strlen(mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Change back to default key
|
|
||||||
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_set_mgmkey(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Try default key, succeed
|
|
||||||
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Try to set a weak key, fail
|
|
||||||
res = ykpiv_hex_decode(weak_mgm_key, strlen(weak_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_set_mgmkey(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_KEY_ERROR);
|
|
||||||
|
|
||||||
// Try default key, succeed
|
|
||||||
res = ykpiv_hex_decode(default_mgm_key, strlen(default_mgm_key), key, &key_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_authenticate(g_state, key);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_change_pin) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_change_pin(g_state, "123456", 6, "ABCDEF", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
|
|
||||||
res = ykpiv_verify(g_state, "ABCDEF", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_change_pin(g_state, "ABCDEF", 6, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_verify(g_state, "ABCDEF", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_change_puk) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
|
|
||||||
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_change_puk(g_state, "12345678", 8, "ABCDEFGH", 8, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
|
|
||||||
res = ykpiv_unblock_pin(g_state, "ABCDEFGH", 8, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_change_puk(g_state, "ABCDEFGH", 8, "12345678", 8, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_unblock_pin(g_state, "ABCDEFGH", 8, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
|
|
||||||
res = ykpiv_unblock_pin(g_state, "12345678", 8, "123456", 6, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
static int block_and_reset() {
|
|
||||||
ykpiv_rc res;
|
|
||||||
int tries = 100;
|
|
||||||
int tries_until_blocked;
|
|
||||||
|
|
||||||
tries_until_blocked = 0;
|
|
||||||
while (tries) {
|
|
||||||
res = ykpiv_verify(g_state, "AAAAAA", &tries);
|
|
||||||
if (res == YKPIV_PIN_LOCKED)
|
|
||||||
break;
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
tries_until_blocked++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify no PIN retries remaining
|
|
||||||
tries = 100;
|
|
||||||
res = ykpiv_get_pin_retries(g_state, &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_eq(tries, 0);
|
|
||||||
|
|
||||||
tries = 100;
|
|
||||||
while (tries) {
|
|
||||||
res = ykpiv_change_puk(g_state, "AAAAAAAA", 8, "AAAAAAAA", 8, &tries);
|
|
||||||
if (res == YKPIV_PIN_LOCKED)
|
|
||||||
break;
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
}
|
|
||||||
res = ykpiv_util_reset(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
return tries_until_blocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
START_TEST(test_reset) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
int tries = 100;
|
|
||||||
int tries_until_blocked;
|
|
||||||
|
|
||||||
// Block and reset, with default PIN retries
|
|
||||||
tries_until_blocked = block_and_reset();
|
|
||||||
ck_assert_int_eq(tries_until_blocked, 3);
|
|
||||||
|
|
||||||
// Authenticate and increase PIN retries
|
|
||||||
test_authenticate(0);
|
|
||||||
res = ykpiv_verify(g_state, "123456", NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_set_pin_retries(g_state, 8, 3);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Block and reset again, verifying increased PIN retries
|
|
||||||
tries_until_blocked = block_and_reset();
|
|
||||||
ck_assert_int_eq(tries_until_blocked, 8);
|
|
||||||
// Note: defaults back to 3 retries after reset
|
|
||||||
|
|
||||||
// Verify default (3) PIN retries remaining
|
|
||||||
tries = 0;
|
|
||||||
res = ykpiv_get_pin_retries(g_state, &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_eq(tries, 3);
|
|
||||||
|
|
||||||
// Verify still (3) PIN retries remaining
|
|
||||||
tries = 0;
|
|
||||||
res = ykpiv_get_pin_retries(g_state, &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_eq(tries, 3);
|
|
||||||
|
|
||||||
// Try wrong PIN
|
|
||||||
res = ykpiv_verify(g_state, "AAAAAA", &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_WRONG_PIN);
|
|
||||||
|
|
||||||
// Verify 2 PIN retries remaining
|
|
||||||
tries = 0;
|
|
||||||
res = ykpiv_get_pin_retries(g_state, &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_eq(tries, 2);
|
|
||||||
|
|
||||||
// Verify correct PIN
|
|
||||||
tries = 100;
|
|
||||||
res = ykpiv_verify(g_state, "123456", &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify back to 3 PIN retries remaining
|
|
||||||
tries = 0;
|
|
||||||
res = ykpiv_get_pin_retries(g_state, &tries);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_int_eq(tries, 3);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
|
|
||||||
struct t_alloc_data{
|
|
||||||
uint32_t count;
|
|
||||||
} g_alloc_data;
|
|
||||||
|
|
||||||
static void* _test_alloc(void *data, size_t cb) {
|
|
||||||
ck_assert_ptr_eq(data, &g_alloc_data);
|
|
||||||
((struct t_alloc_data*)data)->count++;
|
|
||||||
return calloc(cb, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void * _test_realloc(void *data, void *p, size_t cb) {
|
|
||||||
ck_assert_ptr_eq(data, &g_alloc_data);
|
|
||||||
return realloc(p, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _test_free(void *data, void *p) {
|
|
||||||
fflush(stderr);
|
|
||||||
ck_assert_ptr_eq(data, &g_alloc_data);
|
|
||||||
((struct t_alloc_data*)data)->count--;
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
ykpiv_allocator test_allocator_cbs = {
|
|
||||||
.pfn_alloc = _test_alloc,
|
|
||||||
.pfn_realloc = _test_realloc,
|
|
||||||
.pfn_free = _test_free,
|
|
||||||
.alloc_data = &g_alloc_data
|
|
||||||
};
|
|
||||||
|
|
||||||
uint8_t *alloc_auth_cert() {
|
|
||||||
ykpiv_rc res;
|
|
||||||
uint8_t *read_cert = NULL;
|
|
||||||
size_t read_cert_len = 0;
|
|
||||||
|
|
||||||
res = ykpiv_util_write_cert(g_state, YKPIV_KEY_AUTHENTICATION, (uint8_t*)g_cert, sizeof(g_cert), YKPIV_CERTINFO_UNCOMPRESSED);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
res = ykpiv_util_read_cert(g_state, YKPIV_KEY_AUTHENTICATION, &read_cert, &read_cert_len);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(read_cert);
|
|
||||||
ck_assert_int_eq(read_cert_len, sizeof(g_cert));
|
|
||||||
ck_assert_mem_eq(g_cert, read_cert, sizeof(g_cert));
|
|
||||||
return read_cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
START_TEST(test_allocator) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
const ykpiv_allocator allocator;
|
|
||||||
uint8_t *cert1, *cert2;
|
|
||||||
|
|
||||||
res = ykpiv_done(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
g_state = NULL;
|
|
||||||
|
|
||||||
res = ykpiv_init_with_allocator(&g_state, false, &test_allocator_cbs);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(g_state);
|
|
||||||
|
|
||||||
// Verify we can communicate with device and make some allocations
|
|
||||||
res = ykpiv_connect(g_state, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
test_authenticate(0);
|
|
||||||
cert1 = alloc_auth_cert();
|
|
||||||
cert2 = alloc_auth_cert();
|
|
||||||
|
|
||||||
// Verify allocations went through custom allocator, and still live
|
|
||||||
ck_assert_int_gt(g_alloc_data.count, 1);
|
|
||||||
|
|
||||||
// Free and shutdown everything
|
|
||||||
ykpiv_util_free(g_state, cert2);
|
|
||||||
ykpiv_util_free(g_state, cert1);
|
|
||||||
res = ykpiv_disconnect(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_done(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify equal number of frees as allocations
|
|
||||||
ck_assert_int_eq(g_alloc_data.count, 0);
|
|
||||||
|
|
||||||
// Clear g_state so teardown() is skipped
|
|
||||||
g_state = NULL;
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_pin_cache) {
|
|
||||||
ykpiv_rc res;
|
|
||||||
ykpiv_state *local_state;
|
|
||||||
unsigned char data[256];
|
|
||||||
unsigned char data_in[256] = {0};
|
|
||||||
int len = sizeof(data);
|
|
||||||
size_t len2 = sizeof(data);
|
|
||||||
|
|
||||||
import_key(0x9a, YKPIV_PINPOLICY_DEFAULT);
|
|
||||||
|
|
||||||
// Disconnect and reconnect to device to guarantee it is not authed
|
|
||||||
res = ykpiv_disconnect(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_done(g_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_init(&g_state, true);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(g_state);
|
|
||||||
res = ykpiv_connect(g_state, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify decryption does not work without auth
|
|
||||||
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
|
|
||||||
ck_assert_int_eq(res, YKPIV_AUTHENTICATION_ERROR);
|
|
||||||
|
|
||||||
// Verify decryption does work when authed
|
|
||||||
res = ykpiv_verify_select(g_state, "123456", 6, NULL, true);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify PIN policy allows continuing to decrypt without re-verifying
|
|
||||||
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Create a new ykpiv state, connect, and close it.
|
|
||||||
// This forces a card reset from another context, so the original global
|
|
||||||
// context will require a reconnect for its next transaction.
|
|
||||||
res = ykpiv_init(&local_state, true);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
ck_assert_ptr_nonnull(local_state);
|
|
||||||
res = ykpiv_connect(local_state, NULL);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_disconnect(local_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
res = ykpiv_done(local_state);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
|
|
||||||
// Verify we are still authenticated on the global context. This will
|
|
||||||
// require an automatic reconnect and re-verify with the cached PIN.
|
|
||||||
//
|
|
||||||
// Note that you can verify that this fails by rebuilding with
|
|
||||||
// DISABLE_PIN_CACHE set to 1.
|
|
||||||
res = ykpiv_decipher_data(g_state, data_in, (size_t)len, data, &len2, YKPIV_ALGO_RSA2048, 0x9a);
|
|
||||||
ck_assert_int_eq(res, YKPIV_OK);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int destruction_confirmed(void) {
|
|
||||||
char *confirmed = getenv("YKPIV_ENV_HWTESTS_CONFIRMED");
|
|
||||||
if (confirmed && confirmed[0] == '1')
|
|
||||||
return 1;
|
|
||||||
// Use dprintf() to write directly to stdout, since automake eats the standard stdout/stderr pointers.
|
|
||||||
dprintf(0, "\n***\n*** Hardware tests skipped. Run \"make hwcheck\".\n***\n\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Suite *test_suite(void) {
|
|
||||||
Suite *s;
|
|
||||||
TCase *tc;
|
|
||||||
|
|
||||||
s = suite_create("libykpiv api");
|
|
||||||
tc = tcase_create("api");
|
|
||||||
#ifdef HW_TESTS
|
|
||||||
tcase_add_unchecked_fixture(tc, setup, teardown);
|
|
||||||
|
|
||||||
// Must be first: Reset device. Tests run serially, and depend on a clean slate.
|
|
||||||
tcase_add_test(tc, test_reset);
|
|
||||||
|
|
||||||
// Authenticate after reset.
|
|
||||||
tcase_add_test(tc, test_authenticate);
|
|
||||||
|
|
||||||
// Test API functionality
|
|
||||||
tcase_add_test(tc, test_change_pin);
|
|
||||||
tcase_add_test(tc, test_change_puk);
|
|
||||||
tcase_add_test(tc, test_devicemodel);
|
|
||||||
tcase_add_test(tc, test_get_set_cardid);
|
|
||||||
tcase_add_test(tc, test_list_readers);
|
|
||||||
tcase_add_test(tc, test_read_write_list_delete_cert);
|
|
||||||
tcase_add_test(tc, test_import_key);
|
|
||||||
tcase_add_test(tc, test_pin_policy_always);
|
|
||||||
tcase_add_test(tc, test_generate_key);
|
|
||||||
tcase_add_test(tc, test_pin_cache);
|
|
||||||
|
|
||||||
// Must be last: tear down and re-test with custom memory allocator
|
|
||||||
tcase_add_test(tc, test_allocator);
|
|
||||||
#endif
|
|
||||||
suite_add_tcase(s, tc);
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int number_failed;
|
|
||||||
Suite *s;
|
|
||||||
SRunner *sr;
|
|
||||||
|
|
||||||
s = test_suite();
|
|
||||||
sr = srunner_create(s);
|
|
||||||
srunner_set_fork_status(sr, CK_NOFORK);
|
|
||||||
srunner_run_all(sr, CK_VERBOSE);
|
|
||||||
number_failed = srunner_ntests_failed(sr);
|
|
||||||
srunner_free(sr);
|
|
||||||
|
|
||||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
Certificate:
|
||||||
|
Data:
|
||||||
|
Version: 3 (0x2)
|
||||||
|
Serial Number:
|
||||||
|
d4:29:8f:df:8a:af:7b:c0:d7:bf:19:9d:90:d5:ef:ca
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
Issuer: CN=Ferdinand Linnenberg CA
|
||||||
|
Validity
|
||||||
|
Not Before: Feb 10 12:25:37 2022 GMT
|
||||||
|
Not After : May 15 12:25:37 2024 GMT
|
||||||
|
Subject: CN=Bob
|
||||||
|
Subject Public Key Info:
|
||||||
|
Public Key Algorithm: rsaEncryption
|
||||||
|
Public-Key: (2048 bit)
|
||||||
|
Modulus:
|
||||||
|
00:d5:27:9b:99:1b:3a:36:64:36:c8:e5:78:64:b6:
|
||||||
|
9d:70:9d:29:6c:0e:85:91:4b:78:3b:dc:16:c3:09:
|
||||||
|
8c:d3:74:20:8c:6f:ed:c3:90:c9:1b:4d:80:d5:46:
|
||||||
|
da:52:7f:d2:2f:bc:b2:f7:40:8d:ad:dd:24:b9:5c:
|
||||||
|
dc:a2:21:2f:48:ec:06:93:8b:89:f0:cd:63:ff:a1:
|
||||||
|
fd:ce:36:d5:07:7a:1e:0e:cf:68:a8:c1:b3:7f:62:
|
||||||
|
84:b7:e1:cf:25:7b:3f:a8:3c:ac:07:1a:fd:c2:e1:
|
||||||
|
e0:9e:26:24:c1:0d:6d:9d:c6:57:6a:b4:39:28:3d:
|
||||||
|
88:3e:c9:6a:89:90:72:4a:7b:75:c5:5e:1b:5e:5c:
|
||||||
|
32:54:a3:ff:eb:01:68:7f:89:b4:4c:01:3f:08:8e:
|
||||||
|
6c:61:49:60:26:0b:26:58:81:d7:1a:57:ee:52:5c:
|
||||||
|
05:47:de:da:eb:b5:92:9d:5b:ce:26:18:44:59:3e:
|
||||||
|
27:d0:61:86:e2:f4:c6:d9:c7:2b:1f:cb:ea:78:f0:
|
||||||
|
a1:a9:57:d7:98:4c:c1:2f:ae:6a:38:b4:34:53:2e:
|
||||||
|
5a:9e:f8:58:c7:51:e7:fd:b8:27:cd:87:72:26:c1:
|
||||||
|
7d:14:c7:cd:fb:f2:04:8a:c4:8f:61:cf:a8:78:bd:
|
||||||
|
21:be:28:cb:e8:a8:65:29:28:82:46:2f:18:e6:ff:
|
||||||
|
6f:53
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
X509v3 extensions:
|
||||||
|
X509v3 Basic Constraints:
|
||||||
|
CA:FALSE
|
||||||
|
X509v3 Subject Key Identifier:
|
||||||
|
B5:A5:F0:37:25:97:AD:BE:F1:43:52:45:4D:8B:A0:5E:E9:78:21:B8
|
||||||
|
X509v3 Authority Key Identifier:
|
||||||
|
keyid:26:4E:EB:B0:A5:1B:08:A8:90:2A:85:04:73:84:B5:A5:2C:61:D6:91
|
||||||
|
DirName:/CN=Ferdinand Linnenberg CA
|
||||||
|
serial:8C:E0:40:D9:D8:60:E5:77
|
||||||
|
|
||||||
|
X509v3 Extended Key Usage:
|
||||||
|
TLS Web Client Authentication
|
||||||
|
X509v3 Key Usage:
|
||||||
|
Digital Signature
|
||||||
|
Signature Algorithm: sha256WithRSAEncryption
|
||||||
|
19:f3:eb:c1:95:e6:d5:a9:33:d7:2e:02:d8:3a:91:84:81:14:
|
||||||
|
93:fc:03:4d:b1:4b:9d:0b:9b:94:93:9f:1a:0d:87:31:a1:fa:
|
||||||
|
a6:c7:3a:6b:18:24:12:ab:28:fb:c8:e3:09:a2:5d:50:49:00:
|
||||||
|
d9:18:e6:4a:09:18:e0:1c:da:d3:19:96:3d:74:72:fe:e0:8f:
|
||||||
|
ee:59:54:66:2e:57:72:b8:91:55:06:13:e5:9e:89:a2:3a:13:
|
||||||
|
3b:45:30:d3:cd:15:0e:81:eb:4f:b0:6a:a4:6d:00:7d:5b:c0:
|
||||||
|
4a:7f:97:d0:27:27:31:ae:3e:72:f1:74:fe:86:8e:29:a9:42:
|
||||||
|
23:26:22:db:08:8b:df:e9:d3:83:8d:81:10:36:d7:33:68:5e:
|
||||||
|
cb:93:cb:1e:12:c8:cb:be:5e:5c:8e:58:b0:1d:06:5e:c9:98:
|
||||||
|
b7:f1:49:fe:c4:03:de:b4:2b:da:9d:2c:7d:98:37:1c:6c:a8:
|
||||||
|
95:21:6f:23:e3:2e:09:bc:6c:e5:ed:e2:50:d8:f7:da:45:39:
|
||||||
|
d8:34:8a:57:0c:4f:d0:0d:80:06:d6:34:63:72:27:d1:50:d1:
|
||||||
|
d2:21:2c:97:57:17:98:02:95:3a:96:ed:75:9f:cc:f3:b8:f1:
|
||||||
|
3a:85:f9:58:08:9b:a0:75:fd:9b:fd:31:dd:08:dc:14:3d:f4:
|
||||||
|
68:aa:d4:30
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDXzCCAkegAwIBAgIRANQpj9+Kr3vA178ZnZDV78owDQYJKoZIhvcNAQELBQAw
|
||||||
|
IjEgMB4GA1UEAwwXRmVyZGluYW5kIExpbm5lbmJlcmcgQ0EwHhcNMjIwMjEwMTIy
|
||||||
|
NTM3WhcNMjQwNTE1MTIyNTM3WjAOMQwwCgYDVQQDDANCb2IwggEiMA0GCSqGSIb3
|
||||||
|
DQEBAQUAA4IBDwAwggEKAoIBAQDVJ5uZGzo2ZDbI5Xhktp1wnSlsDoWRS3g73BbD
|
||||||
|
CYzTdCCMb+3DkMkbTYDVRtpSf9IvvLL3QI2t3SS5XNyiIS9I7AaTi4nwzWP/of3O
|
||||||
|
NtUHeh4Oz2iowbN/YoS34c8lez+oPKwHGv3C4eCeJiTBDW2dxldqtDkoPYg+yWqJ
|
||||||
|
kHJKe3XFXhteXDJUo//rAWh/ibRMAT8IjmxhSWAmCyZYgdcaV+5SXAVH3trrtZKd
|
||||||
|
W84mGERZPifQYYbi9MbZxysfy+p48KGpV9eYTMEvrmo4tDRTLlqe+FjHUef9uCfN
|
||||||
|
h3ImwX0Ux8378gSKxI9hz6h4vSG+KMvoqGUpKIJGLxjm/29TAgMBAAGjgaMwgaAw
|
||||||
|
CQYDVR0TBAIwADAdBgNVHQ4EFgQUtaXwNyWXrb7xQ1JFTYugXul4IbgwUgYDVR0j
|
||||||
|
BEswSYAUJk7rsKUbCKiQKoUEc4S1pSxh1pGhJqQkMCIxIDAeBgNVBAMMF0ZlcmRp
|
||||||
|
bmFuZCBMaW5uZW5iZXJnIENBggkAjOBA2dhg5XcwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||||
|
AwIwCwYDVR0PBAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQAZ8+vBlebVqTPXLgLY
|
||||||
|
OpGEgRST/ANNsUudC5uUk58aDYcxofqmxzprGCQSqyj7yOMJol1QSQDZGOZKCRjg
|
||||||
|
HNrTGZY9dHL+4I/uWVRmLldyuJFVBhPlnomiOhM7RTDTzRUOgetPsGqkbQB9W8BK
|
||||||
|
f5fQJycxrj5y8XT+ho4pqUIjJiLbCIvf6dODjYEQNtczaF7Lk8seEsjLvl5cjliw
|
||||||
|
HQZeyZi38Un+xAPetCvanSx9mDccbKiVIW8j4y4JvGzl7eJQ2PfaRTnYNIpXDE/Q
|
||||||
|
DYAG1jRjcifRUNHSISyXVxeYApU6lu11n8zzuPE6hflYCJugdf2b/THdCNwUPfRo
|
||||||
|
qtQw
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIi6DixMpf5PQCAggA
|
||||||
|
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECLgvCsIAjjXRBIIEyDAss0V4NrI5
|
||||||
|
W7KXPRgRJ1tqvQrWZQTIFu4Crzs9Inb4TtSv5mATI9ZU2RMFF6MYXlhIxJng861P
|
||||||
|
5IWcU6VeOjRFej8wcB3uTvD2z7NB2cyA5BZSojrZfX5OIEKL9sBzn0vinqmm5N1z
|
||||||
|
oXhLMgf0FZssA3+zjIf04vtvmk5pxTCE6dq6vlsEIJyQ0xGc39bStIwk2a4E9wvi
|
||||||
|
XayKNJnRFSrTahuI3DvQJPd9TdmM6sBKnrcJrDa6LvH51SrGzW8bBEjDAmC7yJdi
|
||||||
|
mBckFTjZ6rGrSxOI6HdnF9RP+y9OiLE4ou5OJ9MbBtngq9OrTImAMZ5ftWowaqX/
|
||||||
|
Do1UTqixi4ecWrr5fr1+A2Vch0I1drds2e/mmLR+5GEQXQXZPZKjtPMwxM/AYnTZ
|
||||||
|
w/M3T9KtwSj0s5G6Saz4WpzaUL7wATb/UNqMr8Ifl8mHEVoFZhvoRpMWA7Yj1oPb
|
||||||
|
cHz8lsfoSrTnK+zLR8ZK3HRu4MtpdCNVwQIJ67T6Feb8YLwYSccTNHBSqUmWRD95
|
||||||
|
wOOqY33xcfplaQ2Y+/8+mHScGSEPNmC4f7EggDeUnG3ow0f4n95DENO+aYqGLjSF
|
||||||
|
+XdCjhD+NTNdlV0z50B6P2XWUoBZOOnPfgFf4nAgn7xQbkZZOk5bQDKo2Zu9jW9/
|
||||||
|
uJyTHqI58tcopI3cjd5iQXOJUrM4OWpPIu3p+VWaIAA8JzJIfDN6fyQ3qsMr305L
|
||||||
|
30JcjrH6if/6J+2g+DpMAK928JY2hfE8VNWH7096ZnArp42/hLYsNuYnSrL1eS+g
|
||||||
|
F/4mvyZyLLTFyB0Frnic4I1QTuNkNmwSrm/B5wIWLqkS7XAyyDDXAcaTHdZCN6nM
|
||||||
|
O2OuF7DfBsFcNMM4VagG5adPS9CYkvz0EEh6ho5XiP2yL3tZfsHyuB4njAsV3aFi
|
||||||
|
D0Yq7QiCf5iA2d4KsYO6yr1wPfVhlsmPi3++mrHulBwwCQWHTlPgRZnjj1xgmPcQ
|
||||||
|
00KsUVh+CMWlf20O5sKhzjvkzbwUj1K+ZfMDuuq1RbzFRSx+Gx8vIaThFg9kVyoP
|
||||||
|
zuvzsT6qc3BGNHmaGZ3d5Re25AuGRTF4cTpDfjW0UL7Wnvnis7iMrUasDhyF7CFn
|
||||||
|
/KG7eKzxqS08o6D4AM5S/fzZEtszoEgAga6DS2R75FVskDweWuEIsar9UGg3UlmW
|
||||||
|
q3+rRPRf1CzrLtyYenkkLg/ajr8JOnFGqZVaLmMnegZQH6rF3aEzlQLNgbNepcuA
|
||||||
|
ObSmAO6MR3MlQgdsH/lNzOPdj1gKcE25hOjGfmwgbOXSJv9Cz0bcBLFEyLZSNpRk
|
||||||
|
HhNejj6BEz/Cmqg1wm7SOBHsXJGcOTnLO1Y3FBt0I7heWvWmj9rOLG9tvvx9dtrP
|
||||||
|
pQyDbIcWonuXLrXYSPyOjmeWoQSzdH3NsCswBV4G+iOiLCJDkElR+mrwKbhtS88T
|
||||||
|
3YeZnsCTsmH+jZpxGgPTObjIG91U4UE4Pnkwc2df355VuOKrf0rB/NK0A9hZGqsV
|
||||||
|
nxtodjn92P6UFzmfEdt95pMcmurK9wkm1kRkP7cIyAs2lCOIdbgGsszz6Mk16Xqy
|
||||||
|
49RdhLxJrJ4gkYZIAbY+KNGVc7uPhm/T9xGrIstbEsoUM7jy7nHMOCCDgdbwX/4X
|
||||||
|
caMeSMZcZ+RvraDDBEbSbg==
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014-2016 Yubico AB
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are
|
|
||||||
* met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above
|
|
||||||
* copyright notice, this list of conditions and the following
|
|
||||||
* disclaimer in the documentation and/or other materials provided
|
|
||||||
* with the distribution.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ykpiv.h"
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <check.h>
|
|
||||||
|
|
||||||
START_TEST(test_version_string) {
|
|
||||||
if (strcmp(YKPIV_VERSION_STRING, ykpiv_check_version(NULL)) != 0) {
|
|
||||||
ck_abort_msg("version mismatch %s != %s\n", YKPIV_VERSION_STRING,
|
|
||||||
ykpiv_check_version(NULL));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ykpiv_check_version(YKPIV_VERSION_STRING) == NULL) {
|
|
||||||
ck_abort_msg("version NULL?\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ykpiv_check_version("99.99.99") != NULL) {
|
|
||||||
ck_abort_msg("version not NULL?\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(stderr, "ykpiv version: header %s library %s\n",
|
|
||||||
YKPIV_VERSION_STRING, ykpiv_check_version (NULL));
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
START_TEST(test_strerror) {
|
|
||||||
const char *s;
|
|
||||||
|
|
||||||
if (ykpiv_strerror(YKPIV_OK) == NULL) {
|
|
||||||
ck_abort_msg("ykpiv_strerror NULL\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
s = ykpiv_strerror_name(YKPIV_OK);
|
|
||||||
if (s == NULL || strcmp(s, "YKPIV_OK") != 0) {
|
|
||||||
ck_abort_msg("ykpiv_strerror_name %s\n", s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
Suite *basic_suite(void) {
|
|
||||||
Suite *s;
|
|
||||||
TCase *tc;
|
|
||||||
|
|
||||||
s = suite_create("libykpiv basic");
|
|
||||||
tc = tcase_create("basic");
|
|
||||||
tcase_add_test(tc, test_version_string);
|
|
||||||
tcase_add_test(tc, test_strerror);
|
|
||||||
suite_add_tcase(s, tc);
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int number_failed;
|
|
||||||
Suite *s;
|
|
||||||
SRunner *sr;
|
|
||||||
|
|
||||||
s = basic_suite();
|
|
||||||
sr = srunner_create(s);
|
|
||||||
srunner_run_all(sr, CK_NORMAL);
|
|
||||||
number_failed = srunner_ntests_failed(sr);
|
|
||||||
srunner_free(sr);
|
|
||||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
//! Integration tests
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
||||||
|
|
||||||
|
use cipher::common::{getrandom::SysRng, Generate};
|
||||||
|
use log::trace;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rsa::{pkcs1v15, RsaPublicKey};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use signature::hazmat::PrehashVerifier;
|
||||||
|
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||||
|
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
|
||||||
|
use yubikey::{
|
||||||
|
certificate::{yubikey_signer, Certificate},
|
||||||
|
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
|
||||||
|
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
|
||||||
|
// Only show logs if `RUST_LOG` is set
|
||||||
|
if env::var("RUST_LOG").is_ok() {
|
||||||
|
env_logger::builder().format_timestamp(None).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
let yubikey = if let Ok(serial) = env::var("YUBIKEY_SERIAL") {
|
||||||
|
let serial = Serial::from_str(&serial).unwrap();
|
||||||
|
YubiKey::open_by_serial(serial).unwrap()
|
||||||
|
} else {
|
||||||
|
YubiKey::open().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!("serial: {}", yubikey.serial());
|
||||||
|
trace!("version: {}", yubikey.version());
|
||||||
|
|
||||||
|
Mutex::new(yubikey)
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// CCCID support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_get_cccid() {
|
||||||
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
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 = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
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 = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
assert!(yubikey.verify_pin(b"000000").is_err());
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Management key support
|
||||||
|
//
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_set_mgmkey() {
|
||||||
|
let mut rng = SysRng;
|
||||||
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
// Set a protected management key.
|
||||||
|
assert!(MgmKey::generate_for(&yubikey, &mut rng)
|
||||||
|
.unwrap()
|
||||||
|
.set_protected(&mut yubikey)
|
||||||
|
.is_ok());
|
||||||
|
let protected = MgmKey::get_protected(&mut yubikey).unwrap();
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_err());
|
||||||
|
assert!(yubikey.authenticate(&protected).is_ok());
|
||||||
|
|
||||||
|
// Set a manual management key.
|
||||||
|
let manual = MgmKey::generate_for(&yubikey, &mut rng).unwrap();
|
||||||
|
assert!(manual.set_manual(&mut yubikey, false).is_ok());
|
||||||
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_err());
|
||||||
|
assert!(yubikey.authenticate(&protected).is_err());
|
||||||
|
assert!(yubikey.authenticate(&manual).is_ok());
|
||||||
|
|
||||||
|
// Set back to the default management key.
|
||||||
|
assert!(MgmKey::set_default(&mut yubikey).is_ok());
|
||||||
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
|
assert!(yubikey.authenticate(&protected).is_err());
|
||||||
|
assert!(yubikey.authenticate(&manual).is_err());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Certificate support
|
||||||
|
//
|
||||||
|
|
||||||
|
fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
|
// Generate a new key in the selected slot.
|
||||||
|
let generated = piv::generate(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
KT::ALGORITHM,
|
||||||
|
PinPolicy::Default,
|
||||||
|
TouchPolicy::Default,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
|
||||||
|
// we'll limit ourselves to 19 bytes serial.
|
||||||
|
let serial = <[u8; 19]>::generate();
|
||||||
|
let serial = SerialNumber::new(&serial[..]).expect("serial can't be more than 20 bytes long");
|
||||||
|
let validity = Validity::from_now(Duration::new(500000, 0)).unwrap();
|
||||||
|
|
||||||
|
// Generate a self-signed certificate for the new key.
|
||||||
|
let cert_result = Certificate::generate_self_signed::<_, KT>(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
serial,
|
||||||
|
validity,
|
||||||
|
Name::from_str("CN=testSubject").expect("parse name"),
|
||||||
|
generated,
|
||||||
|
|_builder| Ok(()),
|
||||||
|
);
|
||||||
|
|
||||||
|
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::<yubikey_signer::YubiRsa<yubikey_signer::Rsa1024>>();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that the certificate is signed correctly
|
||||||
|
//
|
||||||
|
|
||||||
|
let pubkey = RsaPublicKey::try_from(cert.subject_pki()).expect("valid rsa key");
|
||||||
|
let pubkey = pkcs1v15::VerifyingKey::<Sha256>::new(pubkey);
|
||||||
|
|
||||||
|
let data = cert.cert.to_der().expect("serialize certificate");
|
||||||
|
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 = pkcs1v15::Signature::try_from(&data[data.len() - 128..]).unwrap();
|
||||||
|
let hash = Sha256::digest(msg);
|
||||||
|
|
||||||
|
assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_rsa3072() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let version = yubikey.version();
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
|
// Generate a new key in the selected slot.
|
||||||
|
let generated = piv::generate(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
AlgorithmId::Rsa3072,
|
||||||
|
PinPolicy::Default,
|
||||||
|
TouchPolicy::Default,
|
||||||
|
);
|
||||||
|
|
||||||
|
match generated {
|
||||||
|
Ok(key) => {
|
||||||
|
let pubkey = key.subject_public_key;
|
||||||
|
assert!(pubkey.bit_len() > 3072)
|
||||||
|
}
|
||||||
|
Err(e) => assert!((version.major, version.minor) < (5, 7) && e == Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_self_signed_ec_cert() {
|
||||||
|
let cert = generate_self_signed_cert::<p256::NistP256>();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that the certificate is signed correctly
|
||||||
|
//
|
||||||
|
|
||||||
|
let vk = p256::ecdsa::VerifyingKey::try_from(cert.subject_pki()).expect("ecdsa key expected");
|
||||||
|
|
||||||
|
let data = cert.cert.to_der().expect("serialize certificate");
|
||||||
|
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 = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap();
|
||||||
|
|
||||||
|
use p256::ecdsa::signature::Verifier;
|
||||||
|
assert!(vk.verify(msg, &sig).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_self_signed_cv_cert() {
|
||||||
|
let cert = generate_self_signed_cert::<ed25519_dalek::SigningKey>();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that the certificate is signed correctly
|
||||||
|
//
|
||||||
|
|
||||||
|
let pubkey =
|
||||||
|
ed25519_dalek::VerifyingKey::try_from(cert.subject_pki()).expect("ed25519 key expected");
|
||||||
|
|
||||||
|
let data = cert.cert.to_der().expect("serialize certificate");
|
||||||
|
let cert_len = data[2] as usize;
|
||||||
|
let tbs_cert_len = data[5] as usize;
|
||||||
|
let sig_algo_len: usize = 64;
|
||||||
|
let sig_start = cert_len - sig_algo_len + 3;
|
||||||
|
let msg = &data[3..6 + tbs_cert_len];
|
||||||
|
let sig =
|
||||||
|
ed25519_dalek::Signature::from_slice(&data[sig_start..sig_start + sig_algo_len]).unwrap();
|
||||||
|
|
||||||
|
use ed25519_dalek::Verifier;
|
||||||
|
assert!(pubkey.verify(msg, &sig).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_slot_id_display() {
|
||||||
|
assert_eq!(format!("{}", SlotId::Authentication), "Authentication");
|
||||||
|
assert_eq!(format!("{}", SlotId::Signature), "Signature");
|
||||||
|
assert_eq!(format!("{}", SlotId::KeyManagement), "KeyManagement");
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", SlotId::CardAuthentication),
|
||||||
|
"CardAuthentication"
|
||||||
|
);
|
||||||
|
assert_eq!(format!("{}", SlotId::Attestation), "Attestation");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R1)), "R1");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R2)), "R2");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R3)), "R3");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R4)), "R4");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R5)), "R5");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R6)), "R6");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R7)), "R7");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R8)), "R8");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R9)), "R9");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R10)), "R10");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R11)), "R11");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R12)), "R12");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R13)), "R13");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R14)), "R14");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R15)), "R15");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R16)), "R16");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R17)), "R17");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R18)), "R18");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R19)), "R19");
|
||||||
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R20)), "R20");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", SlotId::Management(ManagementSlotId::Pin)),
|
||||||
|
"Pin"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", SlotId::Management(ManagementSlotId::Puk)),
|
||||||
|
"Puk"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", SlotId::Management(ManagementSlotId::Management)),
|
||||||
|
"Management"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Metadata
|
||||||
|
//
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_read_metadata() {
|
||||||
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
|
// Generate a new key in the selected slot.
|
||||||
|
let generated = piv::generate(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
AlgorithmId::EccP256,
|
||||||
|
PinPolicy::Default,
|
||||||
|
TouchPolicy::Default,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match piv::metadata(&mut yubikey, slot) {
|
||||||
|
Ok(metadata) => assert_eq!(metadata.public, Some(generated)),
|
||||||
|
Err(Error::NotSupported) => {
|
||||||
|
// Some YubiKeys don't support metadata
|
||||||
|
eprintln!("metadata not supported by this YubiKey");
|
||||||
|
}
|
||||||
|
Err(err) => panic!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_read_metadata_missing_key() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
// we assume that at least one of these slots is empty
|
||||||
|
let slots_to_check = [
|
||||||
|
RetiredSlotId::R10,
|
||||||
|
RetiredSlotId::R11,
|
||||||
|
RetiredSlotId::R12,
|
||||||
|
RetiredSlotId::R13,
|
||||||
|
RetiredSlotId::R14,
|
||||||
|
RetiredSlotId::R15,
|
||||||
|
RetiredSlotId::R16,
|
||||||
|
RetiredSlotId::R17,
|
||||||
|
RetiredSlotId::R18,
|
||||||
|
RetiredSlotId::R19,
|
||||||
|
RetiredSlotId::R20,
|
||||||
|
];
|
||||||
|
|
||||||
|
for slot in slots_to_check {
|
||||||
|
let slot = SlotId::Retired(slot);
|
||||||
|
|
||||||
|
match piv::metadata(&mut yubikey, slot) {
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("Key {} exists", slot);
|
||||||
|
}
|
||||||
|
Err(Error::NotSupported) => {
|
||||||
|
// Some YubiKeys don't support metadata
|
||||||
|
eprintln!("metadata not supported by this YubiKey");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(Error::NotFound) => {
|
||||||
|
eprintln!("Key {} doesn't exist, ok.", slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => panic!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("No empty slots to check");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_parse_cert_from_der() {
|
||||||
|
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
|
||||||
|
let cert = Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
|
||||||
|
assert_eq!(
|
||||||
|
cert.subject(),
|
||||||
|
"CN=Bob",
|
||||||
|
"Subject is {} should be CN=Bob",
|
||||||
|
cert.subject()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cert.issuer(),
|
||||||
|
"CN=Ferdinand Linnenberg CA",
|
||||||
|
"Issuer is {} should be {}",
|
||||||
|
cert.issuer(),
|
||||||
|
"CN=Ferdinand Linnenberg CA"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2014-2016 Yubico AB
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
|
||||||
* modification, are permitted provided that the following conditions are
|
|
||||||
* met:
|
|
||||||
*
|
|
||||||
* * Redistributions of source code must retain the above copyright
|
|
||||||
* notice, this list of conditions and the following disclaimer.
|
|
||||||
*
|
|
||||||
* * Redistributions in binary form must reproduce the above
|
|
||||||
* copyright notice, this list of conditions and the following
|
|
||||||
* disclaimer in the documentation and/or other materials provided
|
|
||||||
* with the distribution.
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <check.h>
|
|
||||||
|
|
||||||
#include "ykpiv.h"
|
|
||||||
|
|
||||||
struct key {
|
|
||||||
const char text[49];
|
|
||||||
const unsigned char formatted[24];
|
|
||||||
int valid;
|
|
||||||
} keys[] = {
|
|
||||||
{"010203040506070801020304050607080102030405060708",
|
|
||||||
{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
|
|
||||||
1},
|
|
||||||
{"a1a2a3a4a5a6a7a8a1a2a3a4a5a6a7a8a1a2a3a4a5a6a7a8",
|
|
||||||
{0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8},
|
|
||||||
1},
|
|
||||||
{"A1A2A3A4A5A6A7A8A1A2A3A4A5A6A7A8A1A2A3A4A5A6A7A8",
|
|
||||||
{0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8},
|
|
||||||
1},
|
|
||||||
{"This is not something considered valid hex......",
|
|
||||||
{},
|
|
||||||
0},
|
|
||||||
};
|
|
||||||
|
|
||||||
static int parse_key(const char *text, const unsigned char *expected, int valid) {
|
|
||||||
unsigned char key[24];
|
|
||||||
size_t len = sizeof(key);
|
|
||||||
ykpiv_rc res = ykpiv_hex_decode(text, strlen(text), key, &len);
|
|
||||||
if (valid) {
|
|
||||||
ck_assert(res == YKPIV_OK);
|
|
||||||
ck_assert(memcmp(expected, key, 24) == 0);
|
|
||||||
} else {
|
|
||||||
ck_assert(res != YKPIV_OK);
|
|
||||||
}
|
|
||||||
return EXIT_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
START_TEST(test_parse_key) {
|
|
||||||
int res = parse_key(keys[_i].text, keys[_i].formatted, keys[_i].valid);
|
|
||||||
ck_assert(res == EXIT_SUCCESS);
|
|
||||||
}
|
|
||||||
END_TEST
|
|
||||||
|
|
||||||
Suite *parsekey_suite(void) {
|
|
||||||
Suite *s;
|
|
||||||
TCase *tc;
|
|
||||||
|
|
||||||
s = suite_create("libykpiv parsekey");
|
|
||||||
tc = tcase_create("parsekey");
|
|
||||||
tcase_add_loop_test(tc, test_parse_key, 0, sizeof(keys) / sizeof(struct key));
|
|
||||||
suite_add_tcase(s, tc);
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int number_failed;
|
|
||||||
Suite *s;
|
|
||||||
SRunner *sr;
|
|
||||||
|
|
||||||
s = parsekey_suite();
|
|
||||||
sr = srunner_create(s);
|
|
||||||
srunner_run_all(sr, CK_NORMAL);
|
|
||||||
number_failed = srunner_ntests_failed(sr);
|
|
||||||
srunner_free(sr);
|
|
||||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user