Compare commits
101 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 |
+4
-3
@@ -1,5 +1,6 @@
|
|||||||
[advisories]
|
[advisories]
|
||||||
ignore = [
|
ignore = [
|
||||||
"RUSTSEC-2020-0071", # time
|
"RUSTSEC-2020-0071", # chrono
|
||||||
"RUSTSEC-2020-0159", # chrono
|
"RUSTSEC-2021-0145", # atty
|
||||||
]
|
"RUSTSEC-2023-0071", # rsa: Marvin Attack: potential key recovery
|
||||||
|
] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
|
||||||
|
|||||||
+13
-2
@@ -1,8 +1,19 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: cargo
|
- 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: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
time: '13:00'
|
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v6
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -36,17 +36,17 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
deps: true
|
deps: true
|
||||||
- platform: ubuntu-latest
|
- platform: ubuntu-latest
|
||||||
toolchain: 1.57.0 # MSRV
|
toolchain: 1.85.0 # MSRV
|
||||||
deps: sudo apt-get install libpcsclite-dev
|
deps: sudo apt-get install libpcsclite-dev
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
toolchain: 1.57.0 # MSRV
|
toolchain: 1.85.0 # MSRV
|
||||||
deps: true
|
deps: true
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
toolchain: 1.57.0 # MSRV
|
toolchain: 1.85.0 # MSRV
|
||||||
deps: true
|
deps: true
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v6
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
@@ -78,11 +78,11 @@ jobs:
|
|||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v6
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: 1.57.0 # MSRV
|
toolchain: 1.85.0 # MSRV
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
override: true
|
||||||
- run: sudo apt-get install libpcsclite-dev
|
- run: sudo apt-get install libpcsclite-dev
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
path: ~/.cargo/git
|
||||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }}
|
||||||
@@ -42,4 +42,4 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: audit
|
command: audit
|
||||||
args: --deny warnings --ignore RUSTSEC-2019-0031 # spin
|
args: --deny warnings
|
||||||
|
|||||||
+112
@@ -4,6 +4,118 @@ 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).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### 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)
|
## 0.6.0 (2022-08-10)
|
||||||
### Changed
|
### Changed
|
||||||
- 2021 edition upgrade ([#343])
|
- 2021 edition upgrade ([#343])
|
||||||
|
|||||||
Generated
+921
-822
File diff suppressed because it is too large
Load Diff
+38
-29
@@ -1,59 +1,68 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey"
|
name = "yubikey"
|
||||||
version = "0.6.0"
|
version = "0.9.0-pre.0"
|
||||||
description = """
|
description = """
|
||||||
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
|
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
|
||||||
support for hardware-backed public-key decryption and digital signatures using
|
support for hardware-backed public-key decryption and digital signatures using
|
||||||
the Personal Identity Verification (PIV) application. Supports RSA (1024/2048)
|
the Personal Identity Verification (PIV) application. Supports RSA (1024/2048/3072/4096)
|
||||||
or ECC (NIST P-256/P-384) 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 <tony@iqlusion.io>", "Yubico AB"]
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
repository = "https://github.com/iqlusioninc/yubikey.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 = ["ecdsa", "encryption", "rsa", "piv", "signature"]
|
keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.57"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "cli"]
|
members = [".", "cli"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
sha2 = "0.11"
|
||||||
|
x509-cert = { version = "0.3.0-rc.4", features = ["builder", "hazmat"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4"
|
aes = { version = "0.9.0-rc.4", features = ["zeroize"] }
|
||||||
cookie-factory = "0.3"
|
bitflags = "2.5.0"
|
||||||
der-parser = "8"
|
cipher = { version = "0.5", features = ["getrandom", "rand_core"] }
|
||||||
des = "0.8"
|
curve25519-dalek = "5.0.0-pre.6"
|
||||||
elliptic-curve = "0.12"
|
der = "0.8"
|
||||||
hmac = "0.12"
|
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"
|
log = "0.4"
|
||||||
nom = "7"
|
nom = "8"
|
||||||
num-bigint-dig = { version = "0.8", features = ["rand"] }
|
p256 = "0.14.0-rc.8"
|
||||||
num-traits = "0.2"
|
p384 = "0.14.0-rc.8"
|
||||||
num-integer = "0.1"
|
pbkdf2 = { version = "0.13.0-rc.10", default-features = false, features = ["hmac"] }
|
||||||
pbkdf2 = { version = "0.11", default-features = false }
|
pcsc = "2.3.1"
|
||||||
p256 = "0.11"
|
rand = "0.10"
|
||||||
p384 = "0.11"
|
rand_core = "0.10"
|
||||||
pcsc = "2"
|
rsa = { version = "0.10.0-rc.17", features = ["sha2"] }
|
||||||
rand_core = { version = "0.6", features = ["std"] }
|
sha1 = { version = "0.11", features = ["oid"] }
|
||||||
rsa = "0.6"
|
sha2 = { workspace = true, features = ["oid"] }
|
||||||
secrecy = "0.8"
|
signature = "3.0.0-rc.10"
|
||||||
sha1 = "0.10"
|
|
||||||
sha2 = "0.10"
|
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
subtle-encoding = "0.5"
|
uuid = { version = "1.2", features = ["v4"] }
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
x25519-dalek = "3.0.0-pre.6"
|
||||||
x509 = "0.2"
|
x509-cert.workspace = true
|
||||||
x509-parser = "0.14"
|
|
||||||
zeroize = "1"
|
zeroize = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.9"
|
env_logger = "0.11"
|
||||||
lazy_static = "1"
|
once_cell = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
untested = []
|
untested = []
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "change-mode"
|
||||||
|
required-features = ["untested"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|||||||
@@ -4,22 +4,25 @@
|
|||||||
|
|
||||||
[![crate][crate-image]][crate-link]
|
[![crate][crate-image]][crate-link]
|
||||||
[![Docs][docs-image]][docs-link]
|
[![Docs][docs-image]][docs-link]
|
||||||
|
[![Build Status][build-image]][build-link]
|
||||||
|
[![Safety Dance][safety-image]][safety-link]
|
||||||
|
[![Dependency Status][deps-image]][deps-link]
|
||||||
[![2-Clause BSD Licensed][license-image]][license-link]
|
[![2-Clause BSD Licensed][license-image]][license-link]
|
||||||
![MSRV][msrv-image]
|
![MSRV][msrv-image]
|
||||||
[![Safety Dance][safety-image]][safety-link]
|
|
||||||
[![Build Status][build-image]][build-link]
|
|
||||||
[![dependency status][deps-image]][deps-link]
|
|
||||||
|
|
||||||
Pure Rust cross-platform host-side driver for [YubiKey] devices from [Yubico]
|
Pure Rust cross-platform host-side driver for [YubiKey] devices from [Yubico]
|
||||||
with support for public-key encryption and digital signatures using the
|
with support for public-key encryption and digital signatures using the
|
||||||
[Personal Identity Verification (PIV)][PIV] application.
|
[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.
|
||||||
|
|
||||||
@@ -34,17 +37,47 @@ Note that while this project started as a fork of a [Yubico] project,
|
|||||||
this fork is **NOT** an official Yubico project and is in no way supported or
|
this fork is **NOT** an official Yubico project and is in no way supported or
|
||||||
endorsed by Yubico.
|
endorsed by Yubico.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 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
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
Rust **1.57** or newer.
|
Rust **1.60** or newer.
|
||||||
|
|
||||||
## Supported YubiKeys
|
## Supported YubiKeys
|
||||||
|
|
||||||
- [YubiKey 4] series
|
- [YubiKey 4] series
|
||||||
- [YubiKey 5] series
|
- [YubiKey 5] series
|
||||||
|
|
||||||
NOTE: Nano and USB-C variants of the above are also supported.
|
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
|
||||||
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
|
|
||||||
|
|
||||||
## Supported Operating Systems
|
## Supported Operating Systems
|
||||||
|
|
||||||
@@ -59,16 +92,27 @@ an experimental stage and may still contain high-severity issues.
|
|||||||
|
|
||||||
USE AT YOUR OWN RISK!
|
USE AT YOUR OWN RISK!
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
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
|
## Testing
|
||||||
|
|
||||||
To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in
|
To run the full test suite, you'll need a supported YubiKey device connected
|
||||||
the default state (i.e. default PIN/PUK).
|
which is in the default state (i.e. default PIN/PUK).
|
||||||
|
|
||||||
Tests which run live against a YubiKey device are marked as `#[ignore]` by
|
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
|
default in order to pass when running in a CI environment. To run these
|
||||||
tests locally, invoke the following command:
|
tests locally, invoke the following command:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
cargo test -- --ignored
|
cargo test -- --ignored
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,14 +121,14 @@ information about what is happening. If you'd like to print this logging
|
|||||||
information while running the tests, set the `RUST_LOG` environment variable
|
information while running the tests, set the `RUST_LOG` environment variable
|
||||||
to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`):
|
to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`):
|
||||||
|
|
||||||
```
|
```shell
|
||||||
RUST_LOG=info cargo test -- --ignored
|
RUST_LOG=info cargo test -- --ignored
|
||||||
```
|
```
|
||||||
|
|
||||||
To trace every message sent to/from the card i.e. the raw
|
To trace every message sent to/from the card i.e. the raw
|
||||||
Application Protocol Data Unit (APDU) messages, use the `trace` log level:
|
Application Protocol Data Unit (APDU) messages, use the `trace` log level:
|
||||||
|
|
||||||
```
|
```text
|
||||||
running 1 test
|
running 1 test
|
||||||
[INFO yubikey::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
[INFO yubikey::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
|
||||||
[INFO yubikey::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
[INFO yubikey::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
|
||||||
@@ -110,6 +154,14 @@ Yubico, which was originally written in C. It was mechanically translated
|
|||||||
from C into Rust using [Corrode], and then subsequently heavily
|
from C into Rust using [Corrode], and then subsequently heavily
|
||||||
refactored into safer, more idiomatic Rust.
|
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][cc-md] and ask that you do as well.
|
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
||||||
@@ -123,7 +175,7 @@ Yubico's [yubico-piv-tool], a C library/CLI program. The original library
|
|||||||
was licensed under a [2-Clause BSD License][BSDL], which this library inherits
|
was licensed under a [2-Clause BSD License][BSDL], which this library inherits
|
||||||
as a derived work.
|
as a derived work.
|
||||||
|
|
||||||
Copyright (c) 2014-2021 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
|
||||||
@@ -159,16 +211,16 @@ or conditions.
|
|||||||
|
|
||||||
[//]: # (badges)
|
[//]: # (badges)
|
||||||
|
|
||||||
[crate-image]: https://img.shields.io/crates/v/yubikey.svg
|
[crate-image]: https://img.shields.io/crates/v/yubikey?logo=rust
|
||||||
[crate-link]: https://crates.io/crates/yubikey
|
[crate-link]: https://crates.io/crates/yubikey
|
||||||
[docs-image]: https://docs.rs/yubikey/badge.svg
|
[docs-image]: https://docs.rs/yubikey/badge.svg
|
||||||
[docs-link]: https://docs.rs/yubikey/
|
[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
|
||||||
[license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING
|
[license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING
|
||||||
[msrv-image]: https://img.shields.io/badge/rustc-1.57+-blue.svg
|
[msrv-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
|
||||||
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||||
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
[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-image]: https://github.com/iqlusioninc/yubikey.rs/actions/workflows/ci.yml/badge.svg
|
||||||
[build-link]: https://github.com/iqlusioninc/yubikey.rs/actions
|
[build-link]: https://github.com/iqlusioninc/yubikey.rs/actions
|
||||||
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
|
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
|
||||||
[deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs
|
[deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs
|
||||||
@@ -176,17 +228,21 @@ or conditions.
|
|||||||
[//]: # (general links)
|
[//]: # (general links)
|
||||||
|
|
||||||
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
|
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
|
||||||
[PIV]: https://piv.idmanagement.gov/
|
|
||||||
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
|
||||||
[Yubico]: https://www.yubico.com/
|
[Yubico]: https://www.yubico.com/
|
||||||
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
|
[PIV]: https://piv.idmanagement.gov/
|
||||||
|
[NIST]: https://www.nist.gov/
|
||||||
|
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC
|
||||||
|
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
|
||||||
|
[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 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
|
||||||
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
||||||
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
[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
|
[Corrode]: https://github.com/jameysharp/corrode
|
||||||
[cc-web]: https://contributor-covenant.org/
|
[cc-web]: https://contributor-covenant.org/
|
||||||
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
|
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
|
||||||
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
[BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
||||||
|
[`untested` functionality tracking issue]: https://github.com/iqlusioninc/yubikey.rs/issues/280
|
||||||
|
|
||||||
[//]: # (github issues)
|
[//]: # (github issues)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,34 @@ 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).
|
||||||
|
|
||||||
|
## 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)
|
## 0.5.0 (2021-11-21)
|
||||||
### Changed
|
### Changed
|
||||||
- Bump `yubikey` dependency to v0.5 ([#327])
|
- Bump `yubikey` dependency to v0.5 ([#327])
|
||||||
|
|||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey-cli"
|
name = "yubikey-cli"
|
||||||
version = "0.6.0-pre"
|
version = "0.9.0-pre"
|
||||||
description = """
|
description = """
|
||||||
Command-line interface for performing encryption and signing using RSA/ECC keys
|
Command-line interface for performing encryption and signing using RSA/ECC keys
|
||||||
stored on YubiKey devices.
|
stored on YubiKey devices.
|
||||||
@@ -12,15 +12,15 @@ readme = "README.md"
|
|||||||
categories = ["command-line-utilities", "cryptography", "hardware-support"]
|
categories = ["command-line-utilities", "cryptography", "hardware-support"]
|
||||||
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.11"
|
||||||
lazy_static = "1"
|
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
sha2 = "0.9"
|
once_cell = "1"
|
||||||
subtle-encoding = "0.5"
|
sha2.workspace = true
|
||||||
termcolor = "1"
|
termcolor = "1"
|
||||||
x509-parser = "0.12"
|
x509-cert.workspace = true
|
||||||
yubikey = { version = "0.6", path = ".." }
|
yubikey = { version = "=0.9.0-pre.0", path = ".." }
|
||||||
|
|||||||
+4
-10
@@ -18,15 +18,14 @@ utility with general-purpose public-key encryption and signing support.
|
|||||||
|
|
||||||
## Minimum Supported Rust Version
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
Rust **1.51** or newer.
|
Rust **1.60** or newer.
|
||||||
|
|
||||||
## Supported YubiKeys
|
## Supported YubiKeys
|
||||||
|
|
||||||
- [YubiKey 4] series
|
- [YubiKey 4] series
|
||||||
- [YubiKey 5] series
|
- [YubiKey 5] series
|
||||||
|
|
||||||
NOTE: Nano and USB-C variants of the above are also supported.
|
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
|
||||||
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
|
|
||||||
|
|
||||||
## Security Warning
|
## Security Warning
|
||||||
|
|
||||||
@@ -35,10 +34,6 @@ an experimental stage and may still contain high-severity issues.
|
|||||||
|
|
||||||
USE AT YOUR OWN RISK!
|
USE AT YOUR OWN RISK!
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
WIP. Check back later.
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
||||||
@@ -47,7 +42,7 @@ For more information, please see [CODE_OF_CONDUCT.md][cc-md].
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2014-2021 Yubico AB, Tony Arcieri
|
Copyright (c) 2014-2023 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
|
||||||
@@ -88,7 +83,7 @@ or conditions.
|
|||||||
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
|
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
|
||||||
[docs-link]: https://docs.rs/yubikey-cli/
|
[docs-link]: https://docs.rs/yubikey-cli/
|
||||||
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
|
||||||
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg
|
[rustc-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
|
||||||
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-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-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||||
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
[safety-link]: https://github.com/rust-secure-code/safety-dance/
|
||||||
@@ -102,7 +97,6 @@ or conditions.
|
|||||||
[PIV]: https://piv.idmanagement.gov/
|
[PIV]: https://piv.idmanagement.gov/
|
||||||
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
||||||
[Yubico]: https://www.yubico.com/
|
[Yubico]: https://www.yubico.com/
|
||||||
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
|
|
||||||
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
|
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
|
||||||
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
||||||
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
||||||
|
|||||||
+21
-33
@@ -1,16 +1,15 @@
|
|||||||
//! Status messages
|
//! Status messages
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
str,
|
str,
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
};
|
};
|
||||||
use subtle_encoding::hex;
|
|
||||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
|
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
|
||||||
use x509_parser::parse_x509_certificate;
|
use x509_cert::der::Encode;
|
||||||
use yubikey::{certificate::Certificate, piv::*, YubiKey};
|
use yubikey::{certificate::Certificate, piv::*, YubiKey};
|
||||||
|
|
||||||
/// Print a success status message (in green if colors are enabled)
|
/// Print a success status message (in green if colors are enabled)
|
||||||
@@ -59,16 +58,14 @@ macro_rules! status_err {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
/// Color configuration
|
||||||
/// Color configuration
|
static COLOR_CHOICE: Lazy<Mutex<Option<ColorChoice>>> = Lazy::new(|| Mutex::new(None));
|
||||||
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
|
|
||||||
|
|
||||||
/// Standard output
|
/// Standard output
|
||||||
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice());
|
pub static STDOUT: Lazy<StandardStream> = Lazy::new(|| StandardStream::stdout(get_color_choice()));
|
||||||
|
|
||||||
/// Standard error
|
/// Standard error
|
||||||
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice());
|
pub static STDERR: Lazy<StandardStream> = Lazy::new(|| StandardStream::stderr(get_color_choice()));
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain the color configuration.
|
/// Obtain the color configuration.
|
||||||
///
|
///
|
||||||
@@ -140,14 +137,12 @@ impl Status {
|
|||||||
|
|
||||||
/// Print the given message to stdout
|
/// Print the given message to stdout
|
||||||
pub fn print_stdout(self, msg: impl AsRef<str>) {
|
pub fn print_stdout(self, msg: impl AsRef<str>) {
|
||||||
self.print(&*STDOUT, msg)
|
self.print(&STDOUT, msg).expect("error printing to stdout!")
|
||||||
.expect("error printing to stdout!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print the given message to stderr
|
/// Print the given message to stderr
|
||||||
pub fn print_stderr(self, msg: impl AsRef<str>) {
|
pub fn print_stderr(self, msg: impl AsRef<str>) {
|
||||||
self.print(&*STDERR, msg)
|
self.print(&STDERR, msg).expect("error printing to stderr!")
|
||||||
.expect("error printing to stderr!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print the given message
|
/// Print the given message
|
||||||
@@ -185,44 +180,37 @@ pub fn print_cert_info(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let buf = cert.into_buffer();
|
let cert = &cert.cert;
|
||||||
|
|
||||||
if !buf.is_empty() {
|
let fingerprint = Sha256::digest(cert.to_der().unwrap());
|
||||||
let fingerprint = Sha256::digest(&buf);
|
|
||||||
let slot_id: u8 = slot.into();
|
let slot_id: u8 = slot.into();
|
||||||
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
|
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
|
||||||
match parse_x509_certificate(&buf) {
|
|
||||||
Ok((_rem, cert)) => {
|
|
||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Algorithm",
|
"Algorithm",
|
||||||
cert.tbs_certificate.subject_pki.algorithm.algorithm,
|
cert.tbs_certificate()
|
||||||
|
.subject_public_key_info()
|
||||||
|
.algorithm
|
||||||
|
.oid,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?;
|
print_cert_attr(stream, "Subject", cert.tbs_certificate().subject())?;
|
||||||
print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?;
|
print_cert_attr(stream, "Issuer", cert.tbs_certificate().issuer())?;
|
||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Fingerprint",
|
"Fingerprint",
|
||||||
str::from_utf8(hex::encode(fingerprint).as_slice()).unwrap(),
|
hex::upper::encode_string(&fingerprint),
|
||||||
)?;
|
)?;
|
||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Not Before",
|
"Not Before",
|
||||||
cert.tbs_certificate.validity.not_before.to_rfc2822(),
|
cert.tbs_certificate().validity().not_before,
|
||||||
)?;
|
)?;
|
||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Not After",
|
"Not After",
|
||||||
cert.tbs_certificate.validity.not_after.to_rfc2822(),
|
cert.tbs_certificate().validity().not_after,
|
||||||
)?;
|
)?;
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("Failed to parse certificate");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
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 |
+107
-4
@@ -82,6 +82,12 @@ impl Apdu {
|
|||||||
self
|
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
|
/// Set both parameters for this APDU
|
||||||
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
|
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
|
||||||
self.p1 = p1;
|
self.p1 = p1;
|
||||||
@@ -108,11 +114,31 @@ impl Apdu {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transmit this APDU using the given card transaction
|
/// 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> {
|
pub fn transmit(&self, txn: &Transaction<'_>, recv_len: usize) -> Result<Response> {
|
||||||
trace!(">>> {:?}", self);
|
trace!(">>> {:?}", self);
|
||||||
let response = Response::from(txn.transmit(&self.to_bytes(), recv_len)?);
|
let mut response = Response::from(txn.transmit(&self.to_bytes(), recv_len)?);
|
||||||
trace!("<<< {:?}", &response);
|
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)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +221,18 @@ pub enum Ins {
|
|||||||
/// Get device serial
|
/// Get device serial
|
||||||
GetSerial,
|
GetSerial,
|
||||||
|
|
||||||
|
/// Get slot metadata
|
||||||
|
GetMetadata,
|
||||||
|
|
||||||
|
/// Management // Read Config
|
||||||
|
ReadConfig,
|
||||||
|
|
||||||
|
/// Management // Write Config
|
||||||
|
WriteConfig,
|
||||||
|
|
||||||
|
/// Management // DeviceReset
|
||||||
|
DeviceReset,
|
||||||
|
|
||||||
/// Other/unrecognized instruction codes
|
/// Other/unrecognized instruction codes
|
||||||
Other(u8),
|
Other(u8),
|
||||||
}
|
}
|
||||||
@@ -219,6 +257,13 @@ impl Ins {
|
|||||||
Ins::SetPinRetries => 0xfa,
|
Ins::SetPinRetries => 0xfa,
|
||||||
Ins::Attest => 0xf9,
|
Ins::Attest => 0xf9,
|
||||||
Ins::GetSerial => 0xf8,
|
Ins::GetSerial => 0xf8,
|
||||||
|
Ins::GetMetadata => 0xf7,
|
||||||
|
|
||||||
|
// Management
|
||||||
|
Ins::ReadConfig => 0x1d,
|
||||||
|
Ins::WriteConfig => 0x1c,
|
||||||
|
Ins::DeviceReset => 0x1f,
|
||||||
|
|
||||||
Ins::Other(code) => code,
|
Ins::Other(code) => code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,6 +272,11 @@ impl Ins {
|
|||||||
impl From<u8> for Ins {
|
impl From<u8> for Ins {
|
||||||
fn from(code: u8) -> Self {
|
fn from(code: u8) -> Self {
|
||||||
match code {
|
match code {
|
||||||
|
// Management
|
||||||
|
0x1d => Ins::ReadConfig,
|
||||||
|
0x1c => Ins::WriteConfig,
|
||||||
|
0x1f => Ins::DeviceReset,
|
||||||
|
|
||||||
0x20 => Ins::Verify,
|
0x20 => Ins::Verify,
|
||||||
0x24 => Ins::ChangeReference,
|
0x24 => Ins::ChangeReference,
|
||||||
0x2c => Ins::ResetRetry,
|
0x2c => Ins::ResetRetry,
|
||||||
@@ -243,6 +293,7 @@ impl From<u8> for Ins {
|
|||||||
0xfa => Ins::SetPinRetries,
|
0xfa => Ins::SetPinRetries,
|
||||||
0xf9 => Ins::Attest,
|
0xf9 => Ins::Attest,
|
||||||
0xf8 => Ins::GetSerial,
|
0xf8 => Ins::GetSerial,
|
||||||
|
0xf7 => Ins::GetMetadata,
|
||||||
code => Ins::Other(code),
|
code => Ins::Other(code),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,7 +364,7 @@ impl From<Vec<u8>> for Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sw = StatusWords::from(
|
let sw = StatusWords::from(
|
||||||
(bytes[bytes.len() - 2] as u16) << 8 | (bytes[bytes.len() - 1] as u16),
|
((bytes[bytes.len() - 2] as u16) << 8) | (bytes[bytes.len() - 1] as u16),
|
||||||
);
|
);
|
||||||
|
|
||||||
let len = bytes.len() - 2;
|
let len = bytes.len() - 2;
|
||||||
@@ -348,6 +399,12 @@ pub(crate) enum StatusWords {
|
|||||||
/// Successful execution
|
/// Successful execution
|
||||||
Success,
|
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
|
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L53
|
||||||
NoInputDataError,
|
NoInputDataError,
|
||||||
|
|
||||||
@@ -384,6 +441,9 @@ pub(crate) enum StatusWords {
|
|||||||
/// Not enough memory
|
/// Not enough memory
|
||||||
NoSpaceError,
|
NoSpaceError,
|
||||||
|
|
||||||
|
/// Referenced data or reference data not found
|
||||||
|
ReferenceDataNotFoundError,
|
||||||
|
|
||||||
//
|
//
|
||||||
// Custom Yubico Status Word extensions
|
// Custom Yubico Status Word extensions
|
||||||
//
|
//
|
||||||
@@ -405,8 +465,9 @@ impl StatusWords {
|
|||||||
pub fn code(self) -> u16 {
|
pub fn code(self) -> u16 {
|
||||||
match self {
|
match self {
|
||||||
StatusWords::None => 0,
|
StatusWords::None => 0,
|
||||||
|
StatusWords::BytesRemaining { len } => 0x6100 | len as u16,
|
||||||
StatusWords::NoInputDataError => 0x6285,
|
StatusWords::NoInputDataError => 0x6285,
|
||||||
StatusWords::VerifyFailError { tries } => 0x63c0 & tries as u16,
|
StatusWords::VerifyFailError { tries } => 0x63c0 | tries as u16,
|
||||||
StatusWords::WrongLengthError => 0x6700,
|
StatusWords::WrongLengthError => 0x6700,
|
||||||
StatusWords::SecurityStatusError => 0x6982,
|
StatusWords::SecurityStatusError => 0x6982,
|
||||||
StatusWords::AuthBlockedError => 0x6983,
|
StatusWords::AuthBlockedError => 0x6983,
|
||||||
@@ -416,6 +477,7 @@ impl StatusWords {
|
|||||||
StatusWords::IncorrectParamError => 0x6a80,
|
StatusWords::IncorrectParamError => 0x6a80,
|
||||||
StatusWords::NotFoundError => 0x6a82,
|
StatusWords::NotFoundError => 0x6a82,
|
||||||
StatusWords::NoSpaceError => 0x6a84,
|
StatusWords::NoSpaceError => 0x6a84,
|
||||||
|
StatusWords::ReferenceDataNotFoundError => 0x6a88,
|
||||||
StatusWords::IncorrectSlotError => 0x6b00,
|
StatusWords::IncorrectSlotError => 0x6b00,
|
||||||
StatusWords::NotSupportedError => 0x6d00,
|
StatusWords::NotSupportedError => 0x6d00,
|
||||||
StatusWords::CommandAbortedError => 0x6f00,
|
StatusWords::CommandAbortedError => 0x6f00,
|
||||||
@@ -434,6 +496,9 @@ impl From<u16> for StatusWords {
|
|||||||
fn from(sw: u16) -> Self {
|
fn from(sw: u16) -> Self {
|
||||||
match sw {
|
match sw {
|
||||||
0x0000 => StatusWords::None,
|
0x0000 => StatusWords::None,
|
||||||
|
sw if sw & 0xff00 == 0x6100 => Self::BytesRemaining {
|
||||||
|
len: (sw & 0x00ff) as u8,
|
||||||
|
},
|
||||||
0x6285 => StatusWords::NoInputDataError,
|
0x6285 => StatusWords::NoInputDataError,
|
||||||
sw if sw & 0xfff0 == 0x63c0 => StatusWords::VerifyFailError {
|
sw if sw & 0xfff0 == 0x63c0 => StatusWords::VerifyFailError {
|
||||||
tries: (sw & 0x000f) as u8,
|
tries: (sw & 0x000f) as u8,
|
||||||
@@ -447,6 +512,7 @@ impl From<u16> for StatusWords {
|
|||||||
0x6a80 => StatusWords::IncorrectParamError,
|
0x6a80 => StatusWords::IncorrectParamError,
|
||||||
0x6a82 => StatusWords::NotFoundError,
|
0x6a82 => StatusWords::NotFoundError,
|
||||||
0x6a84 => StatusWords::NoSpaceError,
|
0x6a84 => StatusWords::NoSpaceError,
|
||||||
|
0x6a88 => StatusWords::ReferenceDataNotFoundError,
|
||||||
0x6b00 => StatusWords::IncorrectSlotError,
|
0x6b00 => StatusWords::IncorrectSlotError,
|
||||||
0x6d00 => StatusWords::NotSupportedError,
|
0x6d00 => StatusWords::NotSupportedError,
|
||||||
0x6f00 => StatusWords::CommandAbortedError,
|
0x6f00 => StatusWords::CommandAbortedError,
|
||||||
@@ -461,3 +527,40 @@ impl From<StatusWords> for u16 {
|
|||||||
sw.code()
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-20
@@ -30,13 +30,10 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{Error, Result, YubiKey};
|
use crate::{Result, YubiKey};
|
||||||
use rand_core::{OsRng, RngCore};
|
use cipher::common::Generate;
|
||||||
use std::{
|
use rand_core::CryptoRng;
|
||||||
fmt::{self, Debug, Display},
|
use std::fmt::{self, Debug, Display};
|
||||||
str,
|
|
||||||
};
|
|
||||||
use subtle_encoding::hex;
|
|
||||||
|
|
||||||
/// CCCID offset
|
/// CCCID offset
|
||||||
const CCC_ID_OFFS: usize = 9;
|
const CCC_ID_OFFS: usize = 9;
|
||||||
@@ -52,6 +49,7 @@ const OBJ_CAPABILITY: u32 = 0x005f_c107;
|
|||||||
/// - 0xff == Manufacturer ID (dummy)
|
/// - 0xff == Manufacturer ID (dummy)
|
||||||
/// - 0x02 == Card type (javaCard)
|
/// - 0x02 == Card type (javaCard)
|
||||||
/// - next 14 bytes: card ID
|
/// - next 14 bytes: card ID
|
||||||
|
#[allow(dead_code)]
|
||||||
const CCC_TMPL: &[u8] = &[
|
const CCC_TMPL: &[u8] = &[
|
||||||
0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
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,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4,
|
||||||
@@ -69,9 +67,12 @@ impl CardId {
|
|||||||
|
|
||||||
/// Generate a random CCC Card ID
|
/// Generate a random CCC Card ID
|
||||||
pub fn generate() -> Self {
|
pub fn generate() -> Self {
|
||||||
let mut id = [0u8; Self::BYTE_SIZE];
|
Self(Generate::generate())
|
||||||
OsRng.fill_bytes(&mut id);
|
}
|
||||||
Self(id)
|
|
||||||
|
/// 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +95,11 @@ impl CccId {
|
|||||||
pub fn get(yubikey: &mut YubiKey) -> Result<Self> {
|
pub fn get(yubikey: &mut YubiKey) -> Result<Self> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let response = txn.fetch_object(OBJ_CAPABILITY)?;
|
let response = txn.fetch_object(OBJ_CAPABILITY)?;
|
||||||
|
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
|
||||||
if response.len() != CCC_TMPL.len() {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(response[..Self::BYTE_SIZE].try_into().unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Cardholder Capability Container (CCC) ID
|
/// Set Cardholder Capability Container (CCC) ID
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
||||||
let mut buf = CCC_TMPL.to_vec();
|
let mut buf = CCC_TMPL.to_vec();
|
||||||
buf[0..self.0.len()].copy_from_slice(&self.0);
|
buf[0..self.0.len()].copy_from_slice(&self.0);
|
||||||
@@ -114,8 +109,14 @@ impl CccId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for CccId {
|
impl AsRef<[u8]> for CccId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn as_ref(&self) -> &[u8] {
|
||||||
f.write_str(str::from_utf8(&hex::encode(&self.0[..])).unwrap())
|
&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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+358
-491
@@ -33,71 +33,29 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
consts::CB_OBJ_MAX,
|
consts::CB_OBJ_MAX,
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
piv::{sign_data, AlgorithmId, SlotId},
|
piv::SlotId,
|
||||||
serialization::*,
|
serialization::*,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
yubikey::YubiKey,
|
yubikey::YubiKey,
|
||||||
Buffer,
|
Buffer,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use num_bigint_dig::BigUint;
|
use x509_cert::{
|
||||||
use p256::NistP256;
|
builder::{profile::BuilderProfile, Builder, CertificateBuilder},
|
||||||
use p384::NistP384;
|
der::{referenced::OwnedToRef, Decode, Encode},
|
||||||
use rsa::{PublicKeyParts, RsaPublicKey};
|
name::Name,
|
||||||
use sha2::{Digest, Sha256};
|
serial_number::SerialNumber,
|
||||||
use std::{fmt, ops::DerefMut};
|
spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
|
||||||
use x509::{der::Oid, RelativeDistinguishedName};
|
time::Validity,
|
||||||
use x509_parser::{parse_x509_certificate, x509::SubjectPublicKeyInfo};
|
};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
// TODO: Make these der_parser::oid::Oid constants when it has const fn support.
|
|
||||||
const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
|
|
||||||
const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
|
|
||||||
const OID_NIST_P256: &str = "1.2.840.10045.3.1.7";
|
|
||||||
const OID_NIST_P384: &str = "1.3.132.0.34";
|
|
||||||
|
|
||||||
const TAG_CERT: u8 = 0x70;
|
const TAG_CERT: u8 = 0x70;
|
||||||
const TAG_CERT_COMPRESS: u8 = 0x71;
|
const TAG_CERT_COMPRESS: u8 = 0x71;
|
||||||
const TAG_CERT_LRC: u8 = 0xFE;
|
const TAG_CERT_LRC: u8 = 0xFE;
|
||||||
|
|
||||||
/// A serial number for a [`Certificate`].
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Serial(BigUint);
|
|
||||||
|
|
||||||
impl From<BigUint> for Serial {
|
|
||||||
fn from(num: BigUint) -> Serial {
|
|
||||||
Serial(num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<[u8; 20]> for Serial {
|
|
||||||
fn from(bytes: [u8; 20]) -> Serial {
|
|
||||||
Serial(BigUint::from_bytes_be(&bytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&[u8]> for Serial {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(bytes: &[u8]) -> Result<Serial> {
|
|
||||||
if bytes.len() <= 20 {
|
|
||||||
Ok(Serial(BigUint::from_bytes_be(bytes)))
|
|
||||||
} else {
|
|
||||||
Err(Error::ParseError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serial {
|
|
||||||
fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
self.0.to_bytes_be()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about how a [`Certificate`] is stored within a YubiKey.
|
/// Information about how a [`Certificate`] is stored within a YubiKey.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum CertInfo {
|
pub enum CertInfo {
|
||||||
/// The certificate is uncompressed.
|
/// The certificate is uncompressed.
|
||||||
Uncompressed,
|
Uncompressed,
|
||||||
@@ -127,324 +85,42 @@ impl From<CertInfo> for u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl x509::AlgorithmIdentifier for AlgorithmId {
|
|
||||||
type AlgorithmOid = &'static [u64];
|
|
||||||
|
|
||||||
fn algorithm(&self) -> Self::AlgorithmOid {
|
|
||||||
match self {
|
|
||||||
// RSA encryption
|
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => &[1, 2, 840, 113_549, 1, 1, 1],
|
|
||||||
// EC Public Key
|
|
||||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => &[1, 2, 840, 10045, 2, 1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters<W: std::io::Write>(
|
|
||||||
&self,
|
|
||||||
w: cookie_factory::WriteContext<W>,
|
|
||||||
) -> cookie_factory::GenResult<W> {
|
|
||||||
use x509::der::write::der_oid;
|
|
||||||
|
|
||||||
// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
|
|
||||||
// ```text
|
|
||||||
// ECParameters ::= CHOICE {
|
|
||||||
// namedCurve OBJECT IDENTIFIER
|
|
||||||
// -- implicitCurve NULL
|
|
||||||
// -- specifiedCurve SpecifiedECDomain
|
|
||||||
// }
|
|
||||||
// ```
|
|
||||||
match self {
|
|
||||||
AlgorithmId::EccP256 => der_oid(&[1, 2, 840, 10045, 3, 1, 7][..])(w),
|
|
||||||
AlgorithmId::EccP384 => der_oid(&[1, 3, 132, 0, 34][..])(w),
|
|
||||||
_ => Ok(w),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about a public key within a [`Certificate`].
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
|
||||||
pub enum PublicKeyInfo {
|
|
||||||
/// RSA keys
|
|
||||||
Rsa {
|
|
||||||
/// RSA algorithm
|
|
||||||
algorithm: AlgorithmId,
|
|
||||||
|
|
||||||
/// Public key
|
|
||||||
pubkey: RsaPublicKey,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// EC P-256 keys
|
|
||||||
EcP256(EcPublicKey<NistP256>),
|
|
||||||
|
|
||||||
/// EC P-384 keys
|
|
||||||
EcP384(EcPublicKey<NistP384>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for PublicKeyInfo {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "PublicKeyInfo({:?})", self.algorithm())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PublicKeyInfo {
|
|
||||||
fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result<Self> {
|
|
||||||
match subject_pki.algorithm.algorithm.to_string().as_str() {
|
|
||||||
OID_RSA_ENCRYPTION => {
|
|
||||||
let pubkey = read_pki::rsa_pubkey(&subject_pki.subject_public_key.data)?;
|
|
||||||
|
|
||||||
Ok(PublicKeyInfo::Rsa {
|
|
||||||
algorithm: match pubkey.n().bits() {
|
|
||||||
1024 => AlgorithmId::Rsa1024,
|
|
||||||
2048 => AlgorithmId::Rsa2048,
|
|
||||||
_ => return Err(Error::AlgorithmError),
|
|
||||||
},
|
|
||||||
pubkey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
OID_EC_PUBLIC_KEY => {
|
|
||||||
let key_bytes = &subject_pki.subject_public_key.data;
|
|
||||||
let algorithm_parameters = subject_pki
|
|
||||||
.algorithm
|
|
||||||
.parameters
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::InvalidObject)?;
|
|
||||||
|
|
||||||
match read_pki::ec_parameters(algorithm_parameters)? {
|
|
||||||
AlgorithmId::EccP256 => EcPublicKey::<NistP256>::from_bytes(key_bytes)
|
|
||||||
.map(PublicKeyInfo::EcP256)
|
|
||||||
.map_err(|_| Error::InvalidObject),
|
|
||||||
AlgorithmId::EccP384 => EcPublicKey::<NistP384>::from_bytes(key_bytes)
|
|
||||||
.map(PublicKeyInfo::EcP384)
|
|
||||||
.map_err(|_| Error::InvalidObject),
|
|
||||||
_ => Err(Error::AlgorithmError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidObject),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the algorithm that this public key can be used with.
|
|
||||||
pub fn algorithm(&self) -> AlgorithmId {
|
|
||||||
match self {
|
|
||||||
PublicKeyInfo::Rsa { algorithm, .. } => *algorithm,
|
|
||||||
PublicKeyInfo::EcP256(_) => AlgorithmId::EccP256,
|
|
||||||
PublicKeyInfo::EcP384(_) => AlgorithmId::EccP384,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl x509::SubjectPublicKeyInfo for PublicKeyInfo {
|
|
||||||
type AlgorithmId = AlgorithmId;
|
|
||||||
type SubjectPublicKey = Vec<u8>;
|
|
||||||
|
|
||||||
fn algorithm_id(&self) -> AlgorithmId {
|
|
||||||
self.algorithm()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn public_key(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
PublicKeyInfo::Rsa { pubkey, .. } => {
|
|
||||||
cookie_factory::gen_simple(write_pki::rsa_pubkey(pubkey), vec![])
|
|
||||||
.expect("can write to Vec")
|
|
||||||
}
|
|
||||||
PublicKeyInfo::EcP256(pubkey) => pubkey.as_bytes().to_vec(),
|
|
||||||
PublicKeyInfo::EcP384(pubkey) => pubkey.as_bytes().to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Digest algorithms.
|
|
||||||
///
|
|
||||||
/// See RFC 4055 and RFC 8017.
|
|
||||||
enum DigestId {
|
|
||||||
/// Secure Hash Algorithm 256 (SHA256)
|
|
||||||
Sha256,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl x509::AlgorithmIdentifier for DigestId {
|
|
||||||
type AlgorithmOid = &'static [u64];
|
|
||||||
|
|
||||||
fn algorithm(&self) -> Self::AlgorithmOid {
|
|
||||||
match self {
|
|
||||||
// See https://tools.ietf.org/html/rfc4055#section-2.1
|
|
||||||
DigestId::Sha256 => &[2, 16, 840, 1, 101, 3, 4, 2, 1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters<W: std::io::Write>(
|
|
||||||
&self,
|
|
||||||
w: cookie_factory::WriteContext<W>,
|
|
||||||
) -> cookie_factory::GenResult<W> {
|
|
||||||
// Parameters are an explicit NULL
|
|
||||||
// See https://tools.ietf.org/html/rfc8017#appendix-A.2.4
|
|
||||||
x509::der::write::der_null()(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SignatureId {
|
|
||||||
/// Public-Key Cryptography Standards (PKCS) #1 version 1.5 signature algorithm with
|
|
||||||
/// Secure Hash Algorithm 256 (SHA256) and Rivest, Shamir and Adleman (RSA) encryption
|
|
||||||
///
|
|
||||||
/// See RFC 4055 and RFC 8017.
|
|
||||||
Sha256WithRsaEncryption,
|
|
||||||
|
|
||||||
/// Elliptic Curve Digital Signature Algorithm (DSA) coupled with the Secure Hash
|
|
||||||
/// Algorithm 256 (SHA256) algorithm
|
|
||||||
///
|
|
||||||
/// See RFC 5758.
|
|
||||||
EcdsaWithSha256,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl x509::AlgorithmIdentifier for SignatureId {
|
|
||||||
type AlgorithmOid = &'static [u64];
|
|
||||||
|
|
||||||
fn algorithm(&self) -> Self::AlgorithmOid {
|
|
||||||
match self {
|
|
||||||
SignatureId::Sha256WithRsaEncryption => &[1, 2, 840, 113_549, 1, 1, 11],
|
|
||||||
SignatureId::EcdsaWithSha256 => &[1, 2, 840, 10045, 4, 3, 2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameters<W: std::io::Write>(
|
|
||||||
&self,
|
|
||||||
w: cookie_factory::WriteContext<W>,
|
|
||||||
) -> cookie_factory::GenResult<W> {
|
|
||||||
// No parameters for any SignatureId
|
|
||||||
Ok(w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Certificates
|
/// Certificates
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Certificate {
|
pub struct Certificate {
|
||||||
serial: Serial,
|
/// Inner certificate
|
||||||
#[allow(dead_code)]
|
pub cert: x509_cert::Certificate,
|
||||||
issuer: String,
|
|
||||||
subject: String,
|
|
||||||
subject_pki: PublicKeyInfo,
|
|
||||||
data: Buffer,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for Certificate {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(bytes: &'a [u8]) -> Result<Self> {
|
|
||||||
Self::from_bytes(bytes.to_vec())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Certificate {
|
impl Certificate {
|
||||||
/// Creates a new self-signed certificate for the given key. Writes the resulting
|
/// Creates a new self-signed certificate for the given key. Writes the resulting
|
||||||
/// certificate to the slot before returning it.
|
/// certificate to the slot before returning it.
|
||||||
///
|
///
|
||||||
/// `extensions` is optional; if empty, no extensions will be included. Due to the
|
/// `extensions` is a required argument; users who do not have any extensions
|
||||||
/// need for an `O: Oid` type parameter, users who do not have any extensions should
|
/// should set the `extensions` argument to `|_| Ok(())`.
|
||||||
/// use the workaround `let extensions: &[x509::Extension<'_, &[u64]>] = &[];`.
|
pub fn generate_self_signed<F, KT: yubikey_signer::KeyType>(
|
||||||
pub fn generate_self_signed<O: Oid>(
|
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
key: SlotId,
|
key: SlotId,
|
||||||
serial: impl Into<Serial>,
|
serial: SerialNumber,
|
||||||
not_after: Option<DateTime<Utc>>,
|
validity: Validity,
|
||||||
subject: &[RelativeDistinguishedName<'_>],
|
subject: Name,
|
||||||
subject_pki: PublicKeyInfo,
|
subject_pki: SubjectPublicKeyInfoOwned,
|
||||||
extensions: &[x509::Extension<'_, O>],
|
extensions: F,
|
||||||
) -> Result<Self> {
|
) -> Result<Self>
|
||||||
let serial = serial.into();
|
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)?;
|
||||||
|
|
||||||
let mut tbs_cert = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
|
// Add custom extensions
|
||||||
|
extensions(&mut builder)?;
|
||||||
let signature_algorithm = match subject_pki.algorithm() {
|
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => SignatureId::Sha256WithRsaEncryption,
|
|
||||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => SignatureId::EcdsaWithSha256,
|
|
||||||
};
|
|
||||||
|
|
||||||
cookie_factory::gen(
|
|
||||||
x509::write::tbs_certificate(
|
|
||||||
&serial.to_bytes(),
|
|
||||||
&signature_algorithm,
|
|
||||||
// Issuer and subject are the same in self-signed certificates.
|
|
||||||
subject,
|
|
||||||
Utc::now(),
|
|
||||||
not_after,
|
|
||||||
subject,
|
|
||||||
&subject_pki,
|
|
||||||
extensions,
|
|
||||||
),
|
|
||||||
tbs_cert.deref_mut(),
|
|
||||||
)
|
|
||||||
.expect("can serialize to Vec");
|
|
||||||
|
|
||||||
let signature = match signature_algorithm {
|
|
||||||
SignatureId::Sha256WithRsaEncryption => {
|
|
||||||
use cookie_factory::{combinator::slice, sequence::tuple};
|
|
||||||
use x509::{
|
|
||||||
der::write::{der_octet_string, der_sequence},
|
|
||||||
write::algorithm_identifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
let em_len = if let AlgorithmId::Rsa1024 = subject_pki.algorithm() {
|
|
||||||
128
|
|
||||||
} else {
|
|
||||||
256
|
|
||||||
};
|
|
||||||
|
|
||||||
let h = Sha256::digest(&tbs_cert);
|
|
||||||
|
|
||||||
let t = cookie_factory::gen_simple(
|
|
||||||
der_sequence((
|
|
||||||
algorithm_identifier(&DigestId::Sha256),
|
|
||||||
der_octet_string(&h),
|
|
||||||
)),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.expect("can serialize into Vec");
|
|
||||||
|
|
||||||
let em = cookie_factory::gen_simple(
|
|
||||||
tuple((
|
|
||||||
slice(&[0x00, 0x01]),
|
|
||||||
slice(&vec![0xff; em_len - t.len() - 3]),
|
|
||||||
slice(&[0x00]),
|
|
||||||
slice(t),
|
|
||||||
)),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.expect("can serialize to Vec");
|
|
||||||
|
|
||||||
sign_data(yubikey, &em, subject_pki.algorithm(), key)
|
|
||||||
}
|
|
||||||
SignatureId::EcdsaWithSha256 => sign_data(
|
|
||||||
yubikey,
|
|
||||||
&Sha256::digest(&tbs_cert),
|
|
||||||
subject_pki.algorithm(),
|
|
||||||
key,
|
|
||||||
),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let mut data = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
|
|
||||||
|
|
||||||
cookie_factory::gen(
|
|
||||||
x509::write::certificate(&tbs_cert, &signature_algorithm, &signature),
|
|
||||||
data.deref_mut(),
|
|
||||||
)
|
|
||||||
.expect("can serialize to Vec");
|
|
||||||
|
|
||||||
let (issuer, subject) = parse_x509_certificate(&data)
|
|
||||||
.map(|(_, cert)| {
|
|
||||||
(
|
|
||||||
cert.tbs_certificate.issuer.to_string(),
|
|
||||||
cert.tbs_certificate.subject.to_string(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.expect("We just serialized this correctly");
|
|
||||||
|
|
||||||
let cert = Certificate {
|
|
||||||
serial,
|
|
||||||
issuer,
|
|
||||||
subject,
|
|
||||||
subject_pki,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let cert = builder.build(&signer).map_err(|_| Error::KeyError)?;
|
||||||
|
let cert = Self { cert };
|
||||||
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
||||||
|
|
||||||
Ok(cert)
|
Ok(cert)
|
||||||
@@ -459,18 +135,18 @@ impl Certificate {
|
|||||||
return Err(Error::InvalidObject);
|
return Err(Error::InvalidObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
Certificate::from_bytes(buf)
|
Self::from_bytes(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write this certificate into the YubiKey in the given slot
|
/// Write this certificate into the YubiKey in the given slot
|
||||||
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> {
|
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
write_certificate(&txn, slot, Some(&self.data), certinfo)
|
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
|
/// Delete a certificate located at the given slot of the given YubiKey
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> {
|
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
write_certificate(&txn, slot, None, CertInfo::Uncompressed)
|
write_certificate(&txn, slot, None, CertInfo::Uncompressed)
|
||||||
@@ -485,55 +161,59 @@ impl Certificate {
|
|||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed_cert = match parse_x509_certificate(&cert) {
|
x509_cert::Certificate::from_der(&cert)
|
||||||
Ok((_, cert)) => cert,
|
.map(|cert| Self { cert })
|
||||||
_ => return Err(Error::InvalidObject),
|
.map_err(|_| Error::InvalidObject)
|
||||||
};
|
|
||||||
|
|
||||||
let serial = Serial::try_from(parsed_cert.tbs_certificate.serial.to_bytes_be().as_slice())
|
|
||||||
.map_err(|_| Error::InvalidObject)?;
|
|
||||||
let issuer = parsed_cert.tbs_certificate.issuer.to_string();
|
|
||||||
let subject = parsed_cert.tbs_certificate.subject.to_string();
|
|
||||||
let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?;
|
|
||||||
|
|
||||||
Ok(Certificate {
|
|
||||||
serial,
|
|
||||||
issuer,
|
|
||||||
subject,
|
|
||||||
subject_pki,
|
|
||||||
data: cert,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the serial number of the certificate.
|
|
||||||
pub fn serial(&self) -> &Serial {
|
|
||||||
&self.serial
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Issuer field of the certificate.
|
/// Returns the Issuer field of the certificate.
|
||||||
pub fn issuer(&self) -> &str {
|
pub fn issuer(&self) -> String {
|
||||||
&self.subject
|
self.cert.tbs_certificate().issuer().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SubjectName field of the certificate.
|
/// Returns the SubjectName field of the certificate.
|
||||||
pub fn subject(&self) -> &str {
|
pub fn subject(&self) -> String {
|
||||||
&self.subject
|
self.cert.tbs_certificate().subject().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the SubjectPublicKeyInfo field of the certificate.
|
/// Returns the SubjectPublicKeyInfo field of the certificate.
|
||||||
pub fn subject_pki(&self) -> &PublicKeyInfo {
|
pub fn subject_pki(&self) -> SubjectPublicKeyInfoRef<'_> {
|
||||||
&self.subject_pki
|
self.cert
|
||||||
}
|
.tbs_certificate()
|
||||||
|
.subject_public_key_info()
|
||||||
/// Extract the inner buffer
|
.owned_to_ref()
|
||||||
pub fn into_buffer(self) -> Buffer {
|
|
||||||
self.data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<[u8]> for Certificate {
|
/// A [`BuilderProfile`] for self-signed certificates.
|
||||||
fn as_ref(&self) -> &[u8] {
|
///
|
||||||
self.data.as_ref()
|
/// 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![])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,12 +249,7 @@ pub(crate) fn write_certificate(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let object_id = slot.object_id();
|
let object_id = slot.object_id();
|
||||||
|
|
||||||
if data.is_none() {
|
if let Some(data) = data {
|
||||||
return txn.save_object(object_id, &[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = data.unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0u8; CB_OBJ_MAX];
|
let mut buf = [0u8; CB_OBJ_MAX];
|
||||||
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
|
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
|
||||||
|
|
||||||
@@ -583,99 +258,291 @@ pub(crate) fn write_certificate(
|
|||||||
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
|
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
|
||||||
|
|
||||||
txn.save_object(object_id, &buf[..offset])
|
txn.save_object(object_id, &buf[..offset])
|
||||||
}
|
} else {
|
||||||
|
txn.save_object(object_id, &[])
|
||||||
mod read_pki {
|
|
||||||
use der_parser::{
|
|
||||||
asn1_rs::Any,
|
|
||||||
ber::BerObjectContent,
|
|
||||||
der::{parse_der_integer, parse_der_sequence_defined_g, DerObject},
|
|
||||||
error::BerError,
|
|
||||||
};
|
|
||||||
use nom::{combinator, sequence::pair, IResult};
|
|
||||||
use rsa::{BigUint, RsaPublicKey};
|
|
||||||
|
|
||||||
use super::{OID_NIST_P256, OID_NIST_P384};
|
|
||||||
use crate::{piv::AlgorithmId, Error, Result};
|
|
||||||
|
|
||||||
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
|
|
||||||
/// ```text
|
|
||||||
/// RSAPublicKey ::= SEQUENCE {
|
|
||||||
/// modulus INTEGER, -- n
|
|
||||||
/// publicExponent INTEGER -- e
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result<RsaPublicKey> {
|
|
||||||
fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], (DerObject<'_>, DerObject<'_>), BerError> {
|
|
||||||
parse_der_sequence_defined_g(|i, _| pair(parse_der_integer, parse_der_integer)(i))(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> {
|
|
||||||
combinator::map(parse_rsa_pubkey, |(modulus, public_exponent)| {
|
|
||||||
let n = match modulus.content {
|
|
||||||
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
|
|
||||||
_ => panic!("expected DER integer"),
|
|
||||||
};
|
|
||||||
let e = match public_exponent.content {
|
|
||||||
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
|
|
||||||
_ => panic!("expected DER integer"),
|
|
||||||
};
|
|
||||||
|
|
||||||
(n, e)
|
|
||||||
})(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
let (n, e) = match rsa_pubkey_parts(encoded) {
|
|
||||||
Ok((_, res)) => res,
|
|
||||||
_ => return Err(Error::InvalidObject),
|
|
||||||
};
|
|
||||||
|
|
||||||
RsaPublicKey::new(n, e).map_err(|_| Error::InvalidObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
|
|
||||||
/// ```text
|
|
||||||
/// ECParameters ::= CHOICE {
|
|
||||||
/// namedCurve OBJECT IDENTIFIER
|
|
||||||
/// -- implicitCurve NULL
|
|
||||||
/// -- specifiedCurve SpecifiedECDomain
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub(super) fn ec_parameters(parameters: &Any<'_>) -> Result<AlgorithmId> {
|
|
||||||
let curve_oid = parameters.as_oid().map_err(|_| Error::InvalidObject)?;
|
|
||||||
|
|
||||||
match curve_oid.to_string().as_str() {
|
|
||||||
OID_NIST_P256 => Ok(AlgorithmId::EccP256),
|
|
||||||
OID_NIST_P384 => Ok(AlgorithmId::EccP384),
|
|
||||||
_ => Err(Error::AlgorithmError),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod write_pki {
|
pub mod yubikey_signer {
|
||||||
use cookie_factory::{SerializeFn, WriteContext};
|
//! Signer implementation for yubikey
|
||||||
use rsa::{BigUint, PublicKeyParts, RsaPublicKey};
|
|
||||||
use std::io::Write;
|
|
||||||
use x509::der::write::{der_integer, der_sequence};
|
|
||||||
|
|
||||||
/// Encodes a usize as an ASN.1 integer using DER.
|
use crate::{
|
||||||
fn der_integer_biguint<'a, W: Write + 'a>(num: &'a BigUint) -> impl SerializeFn<W> + 'a {
|
error::{Error, Result},
|
||||||
move |w: WriteContext<W>| der_integer(&num.to_bytes_be())(w)
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
|
impl KeyType for ed25519_dalek::SigningKey {
|
||||||
/// ```text
|
const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
|
||||||
/// RSAPublicKey ::= SEQUENCE {
|
type Error = ed25519_dalek::SignatureError;
|
||||||
/// modulus INTEGER, -- n
|
type Signature = ed25519_dalek::Signature;
|
||||||
/// publicExponent INTEGER -- e
|
type VerifyingKey = ed25519_dalek::VerifyingKey;
|
||||||
/// }
|
type PublicKey = ed25519_dalek::VerifyingKey;
|
||||||
/// ```
|
|
||||||
pub(super) fn rsa_pubkey<'a, W: Write + 'a>(
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
pubkey: &'a RsaPublicKey,
|
Ok(Sha512::digest(input).to_vec())
|
||||||
) -> impl SerializeFn<W> + 'a {
|
}
|
||||||
der_sequence((
|
|
||||||
der_integer_biguint(pubkey.n()),
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
der_integer_biguint(pubkey.e()),
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-19
@@ -30,12 +30,8 @@
|
|||||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
use crate::{Error, Result, YubiKey};
|
use crate::{Result, YubiKey};
|
||||||
use std::{
|
use std::fmt::{self, Debug, Display};
|
||||||
fmt::{self, Debug, Display},
|
|
||||||
str,
|
|
||||||
};
|
|
||||||
use subtle_encoding::hex;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// FASC-N offset
|
/// FASC-N offset
|
||||||
@@ -65,6 +61,7 @@ const OBJ_CHUID: u32 = 0x005f_c102;
|
|||||||
/// - 0x35: Exp. Date (hard-coded)
|
/// - 0x35: Exp. Date (hard-coded)
|
||||||
/// - 0x3e: Signature (hard-coded, empty)
|
/// - 0x3e: Signature (hard-coded, empty)
|
||||||
/// - 0xfe: Error Detection Code (hard-coded)
|
/// - 0xfe: Error Detection Code (hard-coded)
|
||||||
|
#[allow(dead_code)]
|
||||||
const CHUID_TMPL: &[u8] = &[
|
const CHUID_TMPL: &[u8] = &[
|
||||||
0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58,
|
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,
|
0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00,
|
||||||
@@ -90,12 +87,13 @@ impl ChuId {
|
|||||||
pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] {
|
pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] {
|
||||||
self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)]
|
self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)]
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap()
|
.expect("should be FASCN_SIZE")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return Card UUID/GUID component of CHUID
|
/// Return Card UUID/GUID component of CHUID
|
||||||
pub fn uuid(&self) -> Uuid {
|
pub fn uuid(&self) -> Uuid {
|
||||||
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)]).unwrap()
|
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)])
|
||||||
|
.expect("should be UUID-sized")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return expiration date component of CHUID
|
/// Return expiration date component of CHUID
|
||||||
@@ -103,24 +101,18 @@ impl ChuId {
|
|||||||
pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] {
|
pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] {
|
||||||
self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)]
|
self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)]
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap()
|
.expect("should be EXPIRATION_SIZE")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Cardholder Unique Identifier (CHUID)
|
/// Get Cardholder Unique Identifier (CHUID)
|
||||||
pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> {
|
pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let response = txn.fetch_object(OBJ_CHUID)?;
|
let response = txn.fetch_object(OBJ_CHUID)?;
|
||||||
|
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
|
||||||
if response.len() != CHUID_TMPL.len() {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ChuId(response[..Self::BYTE_SIZE].try_into().unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set Cardholder Unique Identifier (CHUID)
|
/// Set Cardholder Unique Identifier (CHUID)
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
|
||||||
let mut buf = CHUID_TMPL.to_vec();
|
let mut buf = CHUID_TMPL.to_vec();
|
||||||
buf[..Self::BYTE_SIZE].copy_from_slice(&self.0);
|
buf[..Self::BYTE_SIZE].copy_from_slice(&self.0);
|
||||||
@@ -130,8 +122,14 @@ impl ChuId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ChuId {
|
impl AsRef<[u8]> for ChuId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn as_ref(&self) -> &[u8] {
|
||||||
f.write_str(str::from_utf8(&hex::encode(&self.0[..])).unwrap())
|
&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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -111,7 +111,7 @@ impl Config {
|
|||||||
error!("pin timestamp in admin metadata is an invalid size");
|
error!("pin timestamp in admin metadata is an invalid size");
|
||||||
} else {
|
} else {
|
||||||
// TODO(tarcieri): double-check endianness is correct
|
// TODO(tarcieri): double-check endianness is correct
|
||||||
let pin_last_changed = u32::from_le_bytes(item.try_into().unwrap());
|
let pin_last_changed = u32::from_le_bytes([item[0], item[1], item[2], item[3]]);
|
||||||
|
|
||||||
if pin_last_changed != 0 {
|
if pin_last_changed != 0 {
|
||||||
config.pin_last_changed =
|
config.pin_last_changed =
|
||||||
|
|||||||
+18
-1
@@ -1,5 +1,7 @@
|
|||||||
//! Miscellaneous constant values
|
//! Miscellaneous constant values
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
/// YubiKey max buffer size
|
/// YubiKey max buffer size
|
||||||
pub(crate) const CB_BUF_MAX: usize = 3072;
|
pub(crate) const CB_BUF_MAX: usize = 3072;
|
||||||
|
|
||||||
@@ -7,7 +9,6 @@ pub(crate) const CB_BUF_MAX: usize = 3072;
|
|||||||
pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9;
|
pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9;
|
||||||
|
|
||||||
pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len
|
pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len
|
||||||
|
|
||||||
// Admin tags
|
// Admin tags
|
||||||
@@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
|
|||||||
// Protected tags
|
// Protected tags
|
||||||
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
|
||||||
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
|
||||||
|
|
||||||
|
// Management
|
||||||
|
pub(crate) const TAG_USB_SUPPORTED: u8 = 0x01;
|
||||||
|
pub(crate) const TAG_SERIAL: u8 = 0x02;
|
||||||
|
pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
|
||||||
|
pub(crate) const TAG_FORM_FACTOR: u8 = 0x04;
|
||||||
|
pub(crate) const TAG_VERSION: u8 = 0x05;
|
||||||
|
pub(crate) const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
|
||||||
|
pub(crate) const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
|
||||||
|
pub(crate) const TAG_DEVICE_FLAGS: u8 = 0x08;
|
||||||
|
pub(crate) const TAG_APP_VERSIONS: u8 = 0x09;
|
||||||
|
pub(crate) const TAG_CONFIG_LOCK: u8 = 0x0A;
|
||||||
|
pub(crate) const TAG_UNLOCK: u8 = 0x0B;
|
||||||
|
pub(crate) const TAG_REBOOT: u8 = 0x0C;
|
||||||
|
pub(crate) const TAG_NFC_SUPPORTED: u8 = 0x0D;
|
||||||
|
pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
|
||||||
|
|||||||
+61
-18
@@ -45,12 +45,21 @@ pub enum Error {
|
|||||||
/// Applet error
|
/// Applet error
|
||||||
AppletError,
|
AppletError,
|
||||||
|
|
||||||
|
/// We tried to select an applet that could not be found.
|
||||||
|
AppletNotFound {
|
||||||
|
/// Human-readable name of the applet.
|
||||||
|
applet_name: &'static str,
|
||||||
|
},
|
||||||
|
|
||||||
/// Argument error
|
/// Argument error
|
||||||
ArgumentError,
|
ArgumentError,
|
||||||
|
|
||||||
/// Authentication error
|
/// Authentication error
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
|
||||||
|
/// Error while building a certificate
|
||||||
|
CertificateBuilder,
|
||||||
|
|
||||||
/// Generic error
|
/// Generic error
|
||||||
GenericError,
|
GenericError,
|
||||||
|
|
||||||
@@ -121,31 +130,53 @@ impl Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error message
|
/// Error message
|
||||||
pub fn msg(self) -> &'static str {
|
pub fn msg(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::AlgorithmError => "algorithm error",
|
Error::AlgorithmError => f.write_str("algorithm error"),
|
||||||
Error::AppletError => "applet error",
|
Error::AppletError => f.write_str("applet error"),
|
||||||
Error::ArgumentError => "argument error",
|
Error::AppletNotFound { applet_name } => {
|
||||||
Error::AuthenticationError => "authentication error",
|
f.write_str(&format!("{applet_name} applet not found"))
|
||||||
Error::GenericError => "generic error",
|
}
|
||||||
Error::InvalidObject => "invalid object",
|
Error::ArgumentError => f.write_str("argument error"),
|
||||||
Error::KeyError => "key error",
|
Error::AuthenticationError => f.write_str("authentication error"),
|
||||||
Error::MemoryError => "memory error",
|
Error::CertificateBuilder => f.write_str("certificate builder error"),
|
||||||
Error::NotSupported => "not supported",
|
Error::GenericError => f.write_str("generic error"),
|
||||||
Error::NotFound => "not found",
|
Error::InvalidObject => f.write_str("invalid object"),
|
||||||
Error::ParseError => "parse error",
|
Error::KeyError => f.write_str("key error"),
|
||||||
Error::PcscError { .. } => "PC/SC error",
|
Error::MemoryError => f.write_str("memory error"),
|
||||||
Error::PinLocked => "PIN locked",
|
Error::NotSupported => f.write_str("not supported"),
|
||||||
Error::RangeError => "range error",
|
Error::NotFound => f.write_str("not found"),
|
||||||
Error::SizeError => "size error",
|
Error::ParseError => f.write_str("parse error"),
|
||||||
Error::WrongPin { .. } => "wrong pin",
|
|
||||||
|
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 Display for Error {
|
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 From<std::array::TryFromSliceError> for Error {
|
||||||
|
fn from(_: std::array::TryFromSliceError) -> Error {
|
||||||
|
Error::SizeError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::time::SystemTimeError> for Error {
|
||||||
|
fn from(_: std::time::SystemTimeError) -> Error {
|
||||||
|
Error::GenericError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,3 +195,15 @@ impl std::error::Error for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-108
@@ -1,102 +1,17 @@
|
|||||||
//! **yubikey.rs**: pure Rust cross-platform host-side driver for [YubiKey]
|
#![doc = include_str!("../README.md")]
|
||||||
//! devices from [Yubico] using the Personal Computer/Smart Card ([PC/SC])
|
#![doc(
|
||||||
//! interface as provided by the [`pcsc` crate].
|
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png"
|
||||||
//!
|
)]
|
||||||
//! # Features
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
//! ## Personal Identity Verification (PIV)
|
#![forbid(unsafe_code)]
|
||||||
//! [PIV] is a [NIST] standard for both *signing* and *encryption*
|
#![warn(
|
||||||
//! using SmartCards and SmartCard-based hardware tokens like YubiKeys.
|
clippy::mod_module_files,
|
||||||
//!
|
clippy::unwrap_used,
|
||||||
//! PIV-related functionality can be found in the [`piv`] module.
|
missing_docs,
|
||||||
//!
|
rust_2018_idioms,
|
||||||
//! This library natively implements the protocol used to manage and
|
unused_lifetimes,
|
||||||
//! utilize PIV encryption and signing keys which can be generated, imported,
|
unused_qualifications
|
||||||
//! 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.
|
|
||||||
//!
|
|
||||||
//! # Minimum Supported Rust Version
|
|
||||||
//! Rust **1.57** or newer.
|
|
||||||
//!
|
|
||||||
//! # Supported YubiKeys
|
|
||||||
//! - [YubiKey 4] series
|
|
||||||
//! - [YubiKey 5] series
|
|
||||||
//!
|
|
||||||
//! NOTE: Nano and USB-C variants of the above are also supported.
|
|
||||||
//! Pre-YK4 [YubiKey NEO] series is **NOT** supported.
|
|
||||||
//!
|
|
||||||
//! # Supported Operating Systems
|
|
||||||
//! - Linux
|
|
||||||
//! - macOS
|
|
||||||
//! - Windows
|
|
||||||
//!
|
|
||||||
//! # Supported Algorithms
|
|
||||||
//! - **Authentication**: `3DES`
|
|
||||||
//! - **Encryption**:
|
|
||||||
//! - RSA: `RSA1024`, `RSA2048`
|
|
||||||
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
|
||||||
//! - **Signatures**:
|
|
||||||
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
|
|
||||||
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
|
||||||
//!
|
|
||||||
//! NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
|
|
||||||
//!
|
|
||||||
//! # Status
|
|
||||||
//! 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.
|
|
||||||
//!
|
|
||||||
//! # 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. Presently it is in
|
|
||||||
//! an experimental stage and may still contain high-severity issues.
|
|
||||||
//!
|
|
||||||
//! USE AT YOUR OWN RISK!
|
|
||||||
//!
|
|
||||||
//! # Code of Conduct
|
|
||||||
//! We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
|
|
||||||
//!
|
|
||||||
//! For more information, please see [CODE_OF_CONDUCT.md][cc-md].
|
|
||||||
//!
|
|
||||||
//! # License
|
|
||||||
//! **yubikey.rs** is a fork of and originally a mechanical translation from
|
|
||||||
//! Yubico's [yubico-piv-tool], a C library/CLI program.
|
|
||||||
//!
|
|
||||||
//! The original library was licensed under a [2-Clause BSD License][BSDL],
|
|
||||||
//! which this library inherits as a derived work.
|
|
||||||
//!
|
|
||||||
//! [YubiKey]: https://www.yubico.com/products/yubikey-hardware/
|
|
||||||
//! [PIV]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf
|
|
||||||
//! [Yubico]: https://www.yubico.com/
|
|
||||||
//! [PC/SC]: https://en.wikipedia.org/wiki/PC/SC
|
|
||||||
//! [`pcsc` crate]: https://github.com/bluetech/pcsc-rust
|
|
||||||
//! [NIST]: https://www.nist.gov/
|
|
||||||
//! [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
|
||||||
//! [YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
|
|
||||||
//! [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
|
|
||||||
//! [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
|
|
||||||
//! [`untested` functionality tracking issue]: https://github.com/iqlusioninc/yubikey.rs/issues/280
|
|
||||||
//! [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
|
|
||||||
//! [Corrode]: https://github.com/jameysharp/corrode
|
|
||||||
//! [piv-tool-guide]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
|
|
||||||
//! [cc-web]: https://contributor-covenant.org/
|
|
||||||
//! [cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
|
|
||||||
//! [BSDL]: https://opensource.org/licenses/BSD-2-Clause
|
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
// Adapted from yubico-piv-tool:
|
||||||
// <https://github.com/Yubico/yubico-piv-tool/>
|
// <https://github.com/Yubico/yubico-piv-tool/>
|
||||||
@@ -128,13 +43,6 @@
|
|||||||
// (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.
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
||||||
#![doc(
|
|
||||||
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo.png"
|
|
||||||
)]
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
|
||||||
|
|
||||||
mod apdu;
|
mod apdu;
|
||||||
mod cccid;
|
mod cccid;
|
||||||
pub mod certificate;
|
pub mod certificate;
|
||||||
@@ -143,11 +51,12 @@ mod config;
|
|||||||
mod consts;
|
mod consts;
|
||||||
mod error;
|
mod error;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod mgm;
|
pub mod mgm;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
mod mscmap;
|
mod mscmap;
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
mod msroots;
|
mod msroots;
|
||||||
|
mod otp;
|
||||||
pub mod piv;
|
pub mod piv;
|
||||||
mod policy;
|
mod policy;
|
||||||
pub mod reader;
|
pub mod reader;
|
||||||
@@ -162,7 +71,7 @@ pub use crate::{
|
|||||||
chuid::ChuId,
|
chuid::ChuId,
|
||||||
config::Config,
|
config::Config,
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
mgm::{MgmKey, MgmType},
|
mgm::{MgmAlgorithmId, MgmKey, MgmType},
|
||||||
piv::Key,
|
piv::Key,
|
||||||
policy::{PinPolicy, TouchPolicy},
|
policy::{PinPolicy, TouchPolicy},
|
||||||
reader::Context,
|
reader::Context,
|
||||||
|
|||||||
+14
-20
@@ -30,16 +30,15 @@
|
|||||||
// (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::marker::PhantomData;
|
use std::{iter, marker::PhantomData};
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use crate::{serialization::*, transaction::Transaction, Buffer, Error, Result};
|
use crate::{
|
||||||
|
consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX},
|
||||||
#[cfg(feature = "untested")]
|
serialization::*,
|
||||||
use crate::consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX};
|
transaction::Transaction,
|
||||||
|
Buffer, Error, Result,
|
||||||
#[cfg(feature = "untested")]
|
};
|
||||||
use std::iter;
|
|
||||||
|
|
||||||
const TAG_ADMIN: u8 = 0x80;
|
const TAG_ADMIN: u8 = 0x80;
|
||||||
const TAG_PROTECTED: u8 = 0x88;
|
const TAG_PROTECTED: u8 = 0x88;
|
||||||
@@ -71,7 +70,7 @@ impl<T: MetadataType> Default for Metadata<T> {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Metadata {
|
Metadata {
|
||||||
inner: Zeroizing::new(vec![]),
|
inner: Zeroizing::new(vec![]),
|
||||||
_marker: PhantomData::default(),
|
_marker: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,13 +81,11 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
let data = txn.fetch_object(T::obj_id())?;
|
let data = txn.fetch_object(T::obj_id())?;
|
||||||
Ok(Metadata {
|
Ok(Metadata {
|
||||||
inner: Tlv::parse_single(data, T::tag())?,
|
inner: Tlv::parse_single(data, T::tag())?,
|
||||||
_marker: PhantomData::default(),
|
_marker: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write metadata
|
/// Write metadata
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> {
|
pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> {
|
||||||
if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
|
if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
@@ -105,8 +102,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delete metadata
|
/// Delete metadata
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> {
|
pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> {
|
||||||
txn.save_object(T::obj_id(), &[])
|
txn.save_object(T::obj_id(), &[])
|
||||||
}
|
}
|
||||||
@@ -129,8 +124,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set metadata item
|
/// Set metadata item
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> {
|
pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> {
|
||||||
let mut cb_temp: usize = 0;
|
let mut cb_temp: usize = 0;
|
||||||
let mut tag_temp: u8 = 0;
|
let mut tag_temp: u8 = 0;
|
||||||
@@ -160,8 +153,10 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
|
|
||||||
// We did not find an existing tag, append
|
// We did not find an existing tag, append
|
||||||
assert_eq!(offset, self.inner.len());
|
assert_eq!(offset, self.inner.len());
|
||||||
self.inner
|
self.inner.extend(iter::repeat_n(
|
||||||
.extend(iter::repeat(0).take(1 + get_length_size(item.len()) + item.len()));
|
0,
|
||||||
|
1 + get_length_size(item.len()) + item.len(),
|
||||||
|
));
|
||||||
Tlv::write(&mut self.inner[offset..], tag, item)?;
|
Tlv::write(&mut self.inner[offset..], tag, item)?;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -196,7 +191,7 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
// Move remaining data
|
// Move remaining data
|
||||||
let orig_len = self.inner.len();
|
let orig_len = self.inner.len();
|
||||||
if cb_moved > 0 {
|
if cb_moved > 0 {
|
||||||
self.inner.extend(iter::repeat(0).take(cb_moved as usize));
|
self.inner.extend(iter::repeat_n(0, cb_moved as usize));
|
||||||
}
|
}
|
||||||
self.inner.copy_within(
|
self.inner.copy_within(
|
||||||
next_offset..orig_len,
|
next_offset..orig_len,
|
||||||
@@ -217,7 +212,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the size of a length tag for the given length
|
/// Get the size of a length tag for the given length
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
fn get_length_size(length: usize) -> usize {
|
fn get_length_size(length: usize) -> usize {
|
||||||
if length < 0x80 {
|
if length < 0x80 {
|
||||||
1
|
1
|
||||||
|
|||||||
+780
-171
File diff suppressed because it is too large
Load Diff
+9
-8
@@ -41,7 +41,6 @@ const TAG_MSCMAP: u8 = 0x81;
|
|||||||
///
|
///
|
||||||
/// Defined in Microsoft's Smart Card Minidriver Specification:
|
/// Defined in Microsoft's Smart Card Minidriver Specification:
|
||||||
/// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
|
/// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MsContainer {
|
pub struct MsContainer {
|
||||||
/// Container name.
|
/// Container name.
|
||||||
@@ -141,7 +140,7 @@ impl MsContainer {
|
|||||||
let name_bytes_len = Self::NAME_LEN * 2;
|
let name_bytes_len = Self::NAME_LEN * 2;
|
||||||
|
|
||||||
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
|
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
|
||||||
name[i] = u16::from_le_bytes(chunk.try_into().unwrap());
|
name[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cert_fingerprint = [0u8; 20];
|
let mut cert_fingerprint = [0u8; 20];
|
||||||
@@ -151,11 +150,10 @@ impl MsContainer {
|
|||||||
name,
|
name,
|
||||||
slot: bytes[name_bytes_len].try_into()?,
|
slot: bytes[name_bytes_len].try_into()?,
|
||||||
key_spec: bytes[name_bytes_len + 1],
|
key_spec: bytes[name_bytes_len + 1],
|
||||||
key_size_bits: u16::from_le_bytes(
|
key_size_bits: u16::from_le_bytes([
|
||||||
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
|
bytes[name_bytes_len + 2],
|
||||||
.try_into()
|
bytes[name_bytes_len + 3],
|
||||||
.unwrap(),
|
]),
|
||||||
),
|
|
||||||
flags: bytes[name_bytes_len + 4],
|
flags: bytes[name_bytes_len + 4],
|
||||||
pin_id: bytes[name_bytes_len + 5],
|
pin_id: bytes[name_bytes_len + 5],
|
||||||
associated_echd_container: bytes[name_bytes_len + 6],
|
associated_echd_container: bytes[name_bytes_len + 6],
|
||||||
@@ -184,7 +182,10 @@ impl MsContainer {
|
|||||||
bytes.push(self.pin_id);
|
bytes.push(self.pin_id);
|
||||||
bytes.push(self.associated_echd_container);
|
bytes.push(self.associated_echd_container);
|
||||||
bytes.extend_from_slice(&self.cert_fingerprint);
|
bytes.extend_from_slice(&self.cert_fingerprint);
|
||||||
bytes.as_slice().try_into().unwrap()
|
bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.expect("should be REC_LEN-sized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -57,7 +57,6 @@ const TAG_MSROOTS_MID: u8 = 0x83;
|
|||||||
///
|
///
|
||||||
/// For more information, see:
|
/// For more information, see:
|
||||||
/// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
|
/// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub struct MsRoots(Vec<u8>);
|
pub struct MsRoots(Vec<u8>);
|
||||||
|
|
||||||
impl MsRoots {
|
impl MsRoots {
|
||||||
@@ -97,10 +96,9 @@ impl MsRoots {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MsRoots::new(&data).map(Some).map_err(|e| {
|
MsRoots::new(&data)
|
||||||
error!("error parsing msroots: {:?}", e);
|
.map(Some)
|
||||||
e
|
.inspect_err(|e| error!("error parsing msroots: {:?}", e))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `msroots` file to YubiKey
|
/// Write `msroots` file to YubiKey
|
||||||
|
|||||||
@@ -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];
|
||||||
+594
-159
@@ -6,10 +6,10 @@
|
|||||||
//! Supported algorithms:
|
//! Supported algorithms:
|
||||||
//!
|
//!
|
||||||
//! - **Encryption**:
|
//! - **Encryption**:
|
||||||
//! - RSA: `RSA1024`, `RSA2048`
|
//! - RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
|
||||||
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
||||||
//! - **Signatures**:
|
//! - **Signatures**:
|
||||||
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
|
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
|
||||||
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
|
||||||
|
|
||||||
// Adapted from yubico-piv-tool:
|
// Adapted from yubico-piv-tool:
|
||||||
@@ -44,35 +44,44 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apdu::{Ins, StatusWords},
|
apdu::{Ins, StatusWords},
|
||||||
certificate::{self, Certificate, PublicKeyInfo},
|
certificate::{self, Certificate},
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
|
mgm::MgmAlgorithmId,
|
||||||
policy::{PinPolicy, TouchPolicy},
|
policy::{PinPolicy, TouchPolicy},
|
||||||
serialization::*,
|
serialization::*,
|
||||||
setting,
|
setting,
|
||||||
yubikey::YubiKey,
|
yubikey::YubiKey,
|
||||||
Buffer, ObjectId,
|
Buffer, ObjectId,
|
||||||
};
|
};
|
||||||
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
|
use elliptic_curve::{sec1::Sec1Point as EcPublicKey, PublicKey};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use p256::NistP256;
|
use p256::NistP256;
|
||||||
use p384::NistP384;
|
use p384::NistP384;
|
||||||
use rsa::{BigUint, RsaPublicKey};
|
use rsa::{pkcs8::EncodePublicKey, BoxedUint, RsaPublicKey};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
use x509_cert::{
|
||||||
#[cfg(feature = "untested")]
|
der::{asn1::BitString, Decode},
|
||||||
use {
|
spki::{AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfoOwned},
|
||||||
crate::consts::CB_OBJ_MAX,
|
|
||||||
num_bigint_dig::traits::ModInverse,
|
|
||||||
num_integer::Integer,
|
|
||||||
num_traits::{FromPrimitive, One},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use crate::consts::CB_OBJ_MAX;
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
use rsa::{traits::PrivateKeyParts, RsaPrivateKey};
|
||||||
|
|
||||||
|
/// PIV Applet Name
|
||||||
|
pub(crate) const APPLET_NAME: &str = "PIV";
|
||||||
|
|
||||||
|
/// PIV Applet ID
|
||||||
|
pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x03, 0x08];
|
||||||
|
|
||||||
const CB_ECC_POINTP256: usize = 65;
|
const CB_ECC_POINTP256: usize = 65;
|
||||||
const CB_ECC_POINTP384: usize = 97;
|
const CB_ECC_POINTP384: usize = 97;
|
||||||
|
|
||||||
@@ -80,6 +89,9 @@ const TAG_RSA_MODULUS: u8 = 0x81;
|
|||||||
const TAG_RSA_EXP: u8 = 0x82;
|
const TAG_RSA_EXP: u8 = 0x82;
|
||||||
const TAG_ECC_POINT: u8 = 0x86;
|
const TAG_ECC_POINT: u8 = 0x86;
|
||||||
|
|
||||||
|
/// OID for ed25519 and x25519 algorithms
|
||||||
|
pub const OID_X25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.110");
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
const KEYDATA_LEN: usize = 1024;
|
const KEYDATA_LEN: usize = 1024;
|
||||||
|
|
||||||
@@ -126,6 +138,9 @@ pub enum SlotId {
|
|||||||
/// attestation of other keys generated on device with instruction `f9`. This slot is
|
/// attestation of other keys generated on device with instruction `f9`. This slot is
|
||||||
/// not cleared on reset, but can be overwritten.
|
/// not cleared on reset, but can be overwritten.
|
||||||
Attestation,
|
Attestation,
|
||||||
|
|
||||||
|
/// Thse slots are used for management. PIN PUK and Management Key.
|
||||||
|
Management(ManagementSlotId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for SlotId {
|
impl TryFrom<u8> for SlotId {
|
||||||
@@ -138,7 +153,9 @@ impl TryFrom<u8> for SlotId {
|
|||||||
0x9d => Ok(SlotId::KeyManagement),
|
0x9d => Ok(SlotId::KeyManagement),
|
||||||
0x9e => Ok(SlotId::CardAuthentication),
|
0x9e => Ok(SlotId::CardAuthentication),
|
||||||
0xf9 => Ok(SlotId::Attestation),
|
0xf9 => Ok(SlotId::Attestation),
|
||||||
_ => RetiredSlotId::try_from(value).map(SlotId::Retired),
|
_ => RetiredSlotId::try_from(value)
|
||||||
|
.map(SlotId::Retired)
|
||||||
|
.or_else(|_| ManagementSlotId::try_from(value).map(SlotId::Management)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +169,7 @@ impl From<SlotId> for u8 {
|
|||||||
SlotId::CardAuthentication => 0x9e,
|
SlotId::CardAuthentication => 0x9e,
|
||||||
SlotId::Retired(retired) => retired.into(),
|
SlotId::Retired(retired) => retired.into(),
|
||||||
SlotId::Attestation => 0xf9,
|
SlotId::Attestation => 0xf9,
|
||||||
|
SlotId::Management(mgmt) => mgmt.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,8 +177,9 @@ impl From<SlotId> for u8 {
|
|||||||
impl Display for SlotId {
|
impl Display for SlotId {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
SlotId::Retired(r) => write!(f, "{:?}", r),
|
SlotId::Management(r) => write!(f, "{r:?}"),
|
||||||
_ => write!(f, "{:?}", self),
|
SlotId::Retired(r) => write!(f, "{r:?}"),
|
||||||
|
_ => write!(f, "{self:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +194,10 @@ impl FromStr for SlotId {
|
|||||||
"9d" => Ok(SlotId::KeyManagement),
|
"9d" => Ok(SlotId::KeyManagement),
|
||||||
"9e" => Ok(SlotId::CardAuthentication),
|
"9e" => Ok(SlotId::CardAuthentication),
|
||||||
"f9" => Ok(SlotId::Attestation),
|
"f9" => Ok(SlotId::Attestation),
|
||||||
_ => s.parse().map(SlotId::Retired),
|
_ => s
|
||||||
|
.parse()
|
||||||
|
.map(SlotId::Management)
|
||||||
|
.or_else(|_| s.parse().map(SlotId::Retired)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,6 +211,7 @@ impl SlotId {
|
|||||||
SlotId::KeyManagement => 0x005f_c10b,
|
SlotId::KeyManagement => 0x005f_c10b,
|
||||||
SlotId::CardAuthentication => 0x005f_c101,
|
SlotId::CardAuthentication => 0x005f_c101,
|
||||||
SlotId::Retired(retired) => retired.object_id(),
|
SlotId::Retired(retired) => retired.object_id(),
|
||||||
|
SlotId::Management(mgmt) => mgmt.object_id(),
|
||||||
SlotId::Attestation => 0x005f_ff01,
|
SlotId::Attestation => 0x005f_ff01,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +332,7 @@ impl From<RetiredSlotId> for u8 {
|
|||||||
|
|
||||||
impl Display for RetiredSlotId {
|
impl Display for RetiredSlotId {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{self:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,11 +364,94 @@ impl RetiredSlotId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Management slot IDs.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum ManagementSlotId {
|
||||||
|
/// Personal Identification Number (PIN).
|
||||||
|
Pin,
|
||||||
|
|
||||||
|
/// PIN Unblocking Key (PUK).
|
||||||
|
Puk,
|
||||||
|
|
||||||
|
/// Management Key.
|
||||||
|
Management,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for ManagementSlotId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0x80 => Ok(ManagementSlotId::Pin),
|
||||||
|
0x81 => Ok(ManagementSlotId::Puk),
|
||||||
|
0x9b => Ok(ManagementSlotId::Management),
|
||||||
|
_ => Err(Error::InvalidObject),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ManagementSlotId {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
"80" => Ok(ManagementSlotId::Pin),
|
||||||
|
"81" => Ok(ManagementSlotId::Puk),
|
||||||
|
"9b" => Ok(ManagementSlotId::Management),
|
||||||
|
_ => Err(Error::InvalidObject),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ManagementSlotId> for u8 {
|
||||||
|
fn from(slot: ManagementSlotId) -> u8 {
|
||||||
|
match slot {
|
||||||
|
ManagementSlotId::Pin => 0x80,
|
||||||
|
ManagementSlotId::Puk => 0x81,
|
||||||
|
ManagementSlotId::Management => 0x9b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ManagementSlotId {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManagementSlotId {
|
||||||
|
/// Returns the [`ObjectId`] that corresponds to a given [`ManagementSlotId`].
|
||||||
|
///
|
||||||
|
/// These correspond to the "BER-TLV Tag" values from Table 3 of [NIST SP 800-73-4]:
|
||||||
|
/// "Object Identifiers of the PIV Data Objects for Interoperable Use".
|
||||||
|
///
|
||||||
|
/// [NIST SP 800-73-4]: https://csrc.nist.gov/publications/detail/sp/800-73/4/final
|
||||||
|
pub(crate) fn object_id(self) -> ObjectId {
|
||||||
|
match self {
|
||||||
|
// Data Object: "X.509 Certificate for Key Management"
|
||||||
|
// OID: 2.16.840.1.101.3.7.2.1.2
|
||||||
|
// BER-TLV Tag: '5FC10B'
|
||||||
|
ManagementSlotId::Pin => 0x005f_c10b,
|
||||||
|
|
||||||
|
// Data Object: "Key History Object"
|
||||||
|
// OID: 2.16.840.1.101.3.7.2.96.96
|
||||||
|
// BER-TLV Tag: '5FC10C'
|
||||||
|
ManagementSlotId::Puk => 0x005f_c10c,
|
||||||
|
|
||||||
|
// Data Object: "Retired X.509 Certificate for Key Management 3"
|
||||||
|
// OID: 2.16.840.1.101.3.7.2.16.3
|
||||||
|
// BER-TLV Tag: '5FC10F'
|
||||||
|
ManagementSlotId::Management => 0x005f_c10f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Personal Identity Verification (PIV) key slots
|
/// Personal Identity Verification (PIV) key slots
|
||||||
pub const SLOTS: [SlotId; 24] = [
|
pub const SLOTS: [SlotId; 28] = [
|
||||||
SlotId::Authentication,
|
SlotId::Authentication,
|
||||||
SlotId::Signature,
|
SlotId::Signature,
|
||||||
SlotId::KeyManagement,
|
SlotId::KeyManagement,
|
||||||
|
SlotId::Attestation,
|
||||||
SlotId::Retired(RetiredSlotId::R1),
|
SlotId::Retired(RetiredSlotId::R1),
|
||||||
SlotId::Retired(RetiredSlotId::R2),
|
SlotId::Retired(RetiredSlotId::R2),
|
||||||
SlotId::Retired(RetiredSlotId::R3),
|
SlotId::Retired(RetiredSlotId::R3),
|
||||||
@@ -367,6 +473,9 @@ pub const SLOTS: [SlotId; 24] = [
|
|||||||
SlotId::Retired(RetiredSlotId::R19),
|
SlotId::Retired(RetiredSlotId::R19),
|
||||||
SlotId::Retired(RetiredSlotId::R20),
|
SlotId::Retired(RetiredSlotId::R20),
|
||||||
SlotId::CardAuthentication,
|
SlotId::CardAuthentication,
|
||||||
|
SlotId::Management(ManagementSlotId::Pin),
|
||||||
|
SlotId::Management(ManagementSlotId::Puk),
|
||||||
|
SlotId::Management(ManagementSlotId::Management),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Algorithm identifiers
|
/// Algorithm identifiers
|
||||||
@@ -378,11 +487,23 @@ pub enum AlgorithmId {
|
|||||||
/// 2048-bit RSA.
|
/// 2048-bit RSA.
|
||||||
Rsa2048,
|
Rsa2048,
|
||||||
|
|
||||||
|
/// 3072-bit RSA. Requires firmware 5.7 or newer
|
||||||
|
Rsa3072,
|
||||||
|
|
||||||
|
/// 4096-bit RSA. Requires firmware 5.7 or newer
|
||||||
|
Rsa4096,
|
||||||
|
|
||||||
/// ECDSA with the NIST P256 curve.
|
/// ECDSA with the NIST P256 curve.
|
||||||
EccP256,
|
EccP256,
|
||||||
|
|
||||||
/// ECDSA with the NIST P384 curve.
|
/// ECDSA with the NIST P384 curve.
|
||||||
EccP384,
|
EccP384,
|
||||||
|
|
||||||
|
/// ED25519
|
||||||
|
Ed25519,
|
||||||
|
|
||||||
|
/// X25519
|
||||||
|
X25519,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for AlgorithmId {
|
impl TryFrom<u8> for AlgorithmId {
|
||||||
@@ -392,8 +513,12 @@ impl TryFrom<u8> for AlgorithmId {
|
|||||||
match value {
|
match value {
|
||||||
0x06 => Ok(AlgorithmId::Rsa1024),
|
0x06 => Ok(AlgorithmId::Rsa1024),
|
||||||
0x07 => Ok(AlgorithmId::Rsa2048),
|
0x07 => Ok(AlgorithmId::Rsa2048),
|
||||||
|
0x05 => Ok(AlgorithmId::Rsa3072),
|
||||||
|
0x16 => Ok(AlgorithmId::Rsa4096),
|
||||||
0x11 => Ok(AlgorithmId::EccP256),
|
0x11 => Ok(AlgorithmId::EccP256),
|
||||||
0x14 => Ok(AlgorithmId::EccP384),
|
0x14 => Ok(AlgorithmId::EccP384),
|
||||||
|
0xE0 => Ok(AlgorithmId::Ed25519),
|
||||||
|
0xE1 => Ok(AlgorithmId::X25519),
|
||||||
_ => Err(Error::AlgorithmError),
|
_ => Err(Error::AlgorithmError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,8 +529,12 @@ impl From<AlgorithmId> for u8 {
|
|||||||
match id {
|
match id {
|
||||||
AlgorithmId::Rsa1024 => 0x06,
|
AlgorithmId::Rsa1024 => 0x06,
|
||||||
AlgorithmId::Rsa2048 => 0x07,
|
AlgorithmId::Rsa2048 => 0x07,
|
||||||
|
AlgorithmId::Rsa3072 => 0x05,
|
||||||
|
AlgorithmId::Rsa4096 => 0x16,
|
||||||
AlgorithmId::EccP256 => 0x11,
|
AlgorithmId::EccP256 => 0x11,
|
||||||
AlgorithmId::EccP384 => 0x14,
|
AlgorithmId::EccP384 => 0x14,
|
||||||
|
AlgorithmId::Ed25519 => 0xE0,
|
||||||
|
AlgorithmId::X25519 => 0xE1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,22 +546,29 @@ impl AlgorithmId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
fn get_elem_len(self) -> usize {
|
fn get_elem_len(self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
AlgorithmId::Rsa1024 => 64,
|
AlgorithmId::Rsa1024 => 64,
|
||||||
AlgorithmId::Rsa2048 => 128,
|
AlgorithmId::Rsa2048 => 128,
|
||||||
|
AlgorithmId::Rsa3072 => 192,
|
||||||
|
AlgorithmId::Rsa4096 => 256,
|
||||||
AlgorithmId::EccP256 => 32,
|
AlgorithmId::EccP256 => 32,
|
||||||
AlgorithmId::EccP384 => 48,
|
AlgorithmId::EccP384 => 48,
|
||||||
|
AlgorithmId::Ed25519 => 32,
|
||||||
|
AlgorithmId::X25519 => 32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
fn get_param_tag(self) -> u8 {
|
fn get_param_tag(self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => 0x01,
|
AlgorithmId::Rsa1024
|
||||||
|
| AlgorithmId::Rsa2048
|
||||||
|
| AlgorithmId::Rsa3072
|
||||||
|
| AlgorithmId::Rsa4096 => 0x01,
|
||||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6,
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6,
|
||||||
|
AlgorithmId::Ed25519 => 0x07,
|
||||||
|
AlgorithmId::X25519 => 0x08,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -463,7 +599,13 @@ impl Key {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !buf.is_empty() {
|
if !buf.is_empty() {
|
||||||
let cert = Certificate::from_bytes(buf)?;
|
let cert = match Certificate::from_bytes(buf) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("error parsing certificate in slot {:?}: {}", slot, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
keys.push(Key { slot, cert });
|
keys.push(Key { slot, cert });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,7 +631,7 @@ pub fn generate(
|
|||||||
algorithm: AlgorithmId,
|
algorithm: AlgorithmId,
|
||||||
pin_policy: PinPolicy,
|
pin_policy: PinPolicy,
|
||||||
touch_policy: TouchPolicy,
|
touch_policy: TouchPolicy,
|
||||||
) -> Result<PublicKeyInfo> {
|
) -> Result<SubjectPublicKeyInfoOwned> {
|
||||||
// Keygen messages
|
// Keygen messages
|
||||||
// TODO(tarcieri): extract these into an I18N-handling type?
|
// TODO(tarcieri): extract these into an I18N-handling type?
|
||||||
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
|
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
|
||||||
@@ -504,7 +646,10 @@ pub fn generate(
|
|||||||
let setting_roca: setting::Setting;
|
let setting_roca: setting::Setting;
|
||||||
|
|
||||||
match algorithm {
|
match algorithm {
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
AlgorithmId::Rsa1024
|
||||||
|
| AlgorithmId::Rsa2048
|
||||||
|
| AlgorithmId::Rsa3072
|
||||||
|
| AlgorithmId::Rsa4096 => {
|
||||||
if yubikey.version.major == 4
|
if yubikey.version.major == 4
|
||||||
&& (yubikey.version.minor < 3
|
&& (yubikey.version.minor < 3
|
||||||
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
|
||||||
@@ -594,105 +739,11 @@ pub fn generate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(str4d): Response is wrapped in an ASN.1 TLV:
|
let value = response.data();
|
||||||
//
|
read_public_key(algorithm, value, true)
|
||||||
// 0x7f 0x49 -> Application | Constructed | 0x49
|
|
||||||
match algorithm {
|
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
|
||||||
// It appears that the inner application-specific value returned by the
|
|
||||||
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
|
|
||||||
//
|
|
||||||
// - Use a full ASN.1 parser on the entire datastructure:
|
|
||||||
//
|
|
||||||
// RSA 1024:
|
|
||||||
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
|
||||||
// | tag | len:136 |0x81| len:128 | modulus |0x82|len3| exp |
|
|
||||||
//
|
|
||||||
// RSA 2048:
|
|
||||||
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
|
||||||
// | tag | len:265 |0x81| len:256 | modulus |0x82|len3| exp |
|
|
||||||
//
|
|
||||||
// - Skip the first 5 bytes and use crate::serialize::get_length during TLV
|
|
||||||
// parsing (which treats 128 as a single-byte definite length instead of an
|
|
||||||
// indefinite length):
|
|
||||||
//
|
|
||||||
// RSA 1024:
|
|
||||||
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
|
||||||
// | |0x81|len128| modulus |0x82|len3| exp |
|
|
||||||
//
|
|
||||||
// RSA 2048:
|
|
||||||
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
|
||||||
// | |0x81| len:256 | modulus |0x82|len3| exp |
|
|
||||||
//
|
|
||||||
// Because of the above, treat this for now as a 2-byte ASN.1 tag with a
|
|
||||||
// 3-byte length.
|
|
||||||
let data = &response.data()[5..];
|
|
||||||
|
|
||||||
let (data, modulus_tlv) = Tlv::parse(data)?;
|
|
||||||
if modulus_tlv.tag != TAG_RSA_MODULUS {
|
|
||||||
error!("Failed to parse public key structure (modulus)");
|
|
||||||
return Err(Error::ParseError);
|
|
||||||
}
|
|
||||||
let modulus = modulus_tlv.value.to_vec();
|
|
||||||
|
|
||||||
let (_, exp_tlv) = Tlv::parse(data)?;
|
|
||||||
if exp_tlv.tag != TAG_RSA_EXP {
|
|
||||||
error!("failed to parse public key structure (public exponent)");
|
|
||||||
return Err(Error::ParseError);
|
|
||||||
}
|
|
||||||
let exp = exp_tlv.value.to_vec();
|
|
||||||
|
|
||||||
Ok(PublicKeyInfo::Rsa {
|
|
||||||
algorithm,
|
|
||||||
pubkey: RsaPublicKey::new(
|
|
||||||
BigUint::from_bytes_be(&modulus),
|
|
||||||
BigUint::from_bytes_be(&exp),
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::InvalidObject)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
|
||||||
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
|
|
||||||
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
|
|
||||||
let data = &response.data()[3..];
|
|
||||||
|
|
||||||
let len = if let AlgorithmId::EccP256 = algorithm {
|
|
||||||
CB_ECC_POINTP256
|
|
||||||
} else {
|
|
||||||
CB_ECC_POINTP384
|
|
||||||
};
|
|
||||||
|
|
||||||
let (_, tlv) = Tlv::parse(data)?;
|
|
||||||
|
|
||||||
if tlv.tag != TAG_ECC_POINT {
|
|
||||||
error!("failed to parse public key structure");
|
|
||||||
return Err(Error::ParseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// the curve point should always be determined by the curve
|
|
||||||
if tlv.value.len() != len {
|
|
||||||
error!("unexpected length");
|
|
||||||
return Err(Error::AlgorithmError);
|
|
||||||
}
|
|
||||||
|
|
||||||
let point = tlv.value.to_vec();
|
|
||||||
|
|
||||||
match algorithm {
|
|
||||||
AlgorithmId::EccP256 => {
|
|
||||||
EcPublicKey::<NistP256>::from_bytes(point).map(PublicKeyInfo::EcP256)
|
|
||||||
}
|
|
||||||
AlgorithmId::EccP384 => {
|
|
||||||
EcPublicKey::<NistP384>::from_bytes(point).map(PublicKeyInfo::EcP384)
|
|
||||||
}
|
|
||||||
_ => return Err(Error::AlgorithmError),
|
|
||||||
}
|
|
||||||
.map_err(|_| Error::InvalidObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
fn write_key(
|
fn write_key(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
slot: SlotId,
|
slot: SlotId,
|
||||||
@@ -741,7 +792,6 @@ fn write_key(
|
|||||||
|
|
||||||
/// The key data that makes up an RSA key.
|
/// The key data that makes up an RSA key.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub struct RsaKeyData {
|
pub struct RsaKeyData {
|
||||||
/// The secret prime `p`.
|
/// The secret prime `p`.
|
||||||
p: Buffer,
|
p: Buffer,
|
||||||
@@ -757,41 +807,51 @@ pub struct RsaKeyData {
|
|||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
impl RsaKeyData {
|
impl RsaKeyData {
|
||||||
/// Generates a new RSA key data set from two randomly generated, secret, primes.
|
/// Generates a new RSA key data set from two (randomly generated) secret primes.
|
||||||
///
|
///
|
||||||
/// Panics if `secret_p` or `secret_q` are invalid primes.
|
/// # Returns
|
||||||
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Self {
|
/// - `Ok(key_data)` if `secret_p` and `secret_q` are valid primes.
|
||||||
let p = BigUint::from_bytes_be(secret_p);
|
/// - `Err(Error::AlgorithmError)` if `secret_p`/`secret_q` are invalid primes.
|
||||||
let q = BigUint::from_bytes_be(secret_q);
|
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Result<Self> {
|
||||||
|
let p = BoxedUint::from_be_slice_vartime(secret_p);
|
||||||
|
let q = BoxedUint::from_be_slice_vartime(secret_q);
|
||||||
|
let exp = BoxedUint::from(KEYDATA_RSA_EXP);
|
||||||
|
|
||||||
let totient = {
|
let mut private_key = RsaPrivateKey::from_p_q(p.clone(), q.clone(), exp)
|
||||||
let p_t = &p - BigUint::one();
|
.map_err(|_| Error::AlgorithmError)?;
|
||||||
let q_t = &p - BigUint::one();
|
private_key
|
||||||
|
.precompute()
|
||||||
|
.map_err(|_| Error::AlgorithmError)?;
|
||||||
|
|
||||||
p_t.lcm(&q_t)
|
Ok(RsaKeyData {
|
||||||
};
|
p: Zeroizing::new(p.to_be_bytes().to_vec()),
|
||||||
|
q: Zeroizing::new(q.to_be_bytes().to_vec()),
|
||||||
let exp = BigUint::from_u64(KEYDATA_RSA_EXP).unwrap();
|
dp: Zeroizing::new(
|
||||||
|
private_key
|
||||||
let d = exp.mod_inverse(&totient).unwrap();
|
.dp()
|
||||||
let d = d.to_biguint().unwrap();
|
.expect("invariant violation: precompute should fill the field")
|
||||||
|
.clone()
|
||||||
// We calculate the optimization values ahead of time, instead of making the user
|
.to_be_bytes()
|
||||||
// do so.
|
.to_vec(),
|
||||||
|
),
|
||||||
let dp = &d % (&p - BigUint::one());
|
dq: Zeroizing::new(
|
||||||
let dq = &d % (&q - BigUint::one());
|
private_key
|
||||||
|
.dq()
|
||||||
let qinv = q.clone().mod_inverse(&p).unwrap();
|
.expect("invariant violation: precompute should fill the field")
|
||||||
let (_, qinv) = qinv.to_bytes_be();
|
.clone()
|
||||||
|
.to_be_bytes()
|
||||||
RsaKeyData {
|
.to_vec(),
|
||||||
p: Zeroizing::new(p.to_bytes_be()),
|
),
|
||||||
q: Zeroizing::new(q.to_bytes_be()),
|
qinv: Zeroizing::new(
|
||||||
dp: Zeroizing::new(dp.to_bytes_be()),
|
private_key
|
||||||
dq: Zeroizing::new(dq.to_bytes_be()),
|
.qinv()
|
||||||
qinv: Zeroizing::new(qinv),
|
.expect("invariant violation: precompute should fill the field")
|
||||||
}
|
.clone()
|
||||||
|
.retrieve()
|
||||||
|
.to_be_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn total_len(&self) -> usize {
|
fn total_len(&self) -> usize {
|
||||||
@@ -801,9 +861,8 @@ impl RsaKeyData {
|
|||||||
|
|
||||||
/// Imports a private RSA encryption or signing key into the YubiKey.
|
/// Imports a private RSA encryption or signing key into the YubiKey.
|
||||||
///
|
///
|
||||||
/// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048`.
|
/// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048` or `AlgorithmId::Rsa3072` or `AlgorithmId::Rsa4096`.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn import_rsa_key(
|
pub fn import_rsa_key(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
slot: SlotId,
|
slot: SlotId,
|
||||||
@@ -813,7 +872,10 @@ pub fn import_rsa_key(
|
|||||||
pin_policy: PinPolicy,
|
pin_policy: PinPolicy,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match algorithm {
|
match algorithm {
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => (),
|
AlgorithmId::Rsa1024
|
||||||
|
| AlgorithmId::Rsa2048
|
||||||
|
| AlgorithmId::Rsa3072
|
||||||
|
| AlgorithmId::Rsa4096 => (),
|
||||||
_ => return Err(Error::AlgorithmError),
|
_ => return Err(Error::AlgorithmError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +900,6 @@ pub fn import_rsa_key(
|
|||||||
///
|
///
|
||||||
/// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`.
|
/// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn import_ecc_key(
|
pub fn import_ecc_key(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
slot: SlotId,
|
slot: SlotId,
|
||||||
@@ -863,11 +924,38 @@ pub fn import_ecc_key(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Imports a private ECDH/EdDSA encryption or signing key into the YubiKey.
|
||||||
|
///
|
||||||
|
/// Errors if `algorithm` isn't `AlgorithmId::Ed25519` or ` AlgorithmId::X25519`.
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub fn import_cv_key(
|
||||||
|
yubikey: &mut YubiKey,
|
||||||
|
slot: SlotId,
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
key_data: &[u8],
|
||||||
|
touch_policy: TouchPolicy,
|
||||||
|
pin_policy: PinPolicy,
|
||||||
|
) -> Result<()> {
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::Ed25519 | AlgorithmId::X25519 => (),
|
||||||
|
_ => return Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_data.len() > KEYDATA_LEN {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = vec![key_data];
|
||||||
|
|
||||||
|
write_key(yubikey, slot, params, pin_policy, touch_policy, algorithm)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate an attestation certificate for a stored key.
|
/// Generate an attestation certificate for a stored key.
|
||||||
///
|
///
|
||||||
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer> {
|
pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer> {
|
||||||
let templ = [0, Ins::Attest.code(), key.into(), 0];
|
let templ = [0, Ins::Attest.code(), key.into(), 0];
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
@@ -903,7 +991,6 @@ pub fn sign_data(
|
|||||||
|
|
||||||
/// Decrypt data using a PIV key.
|
/// Decrypt data using a PIV key.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn decrypt_data(
|
pub fn decrypt_data(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
@@ -915,3 +1002,351 @@ pub fn decrypt_data(
|
|||||||
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
|
||||||
txn.authenticated_command(input, algorithm, key, true)
|
txn.authenticated_command(input, algorithm, key, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read metadata
|
||||||
|
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
|
||||||
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
|
txn.get_metadata(slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata from a slot
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SlotMetadata {
|
||||||
|
/// Algorithm / Type of key
|
||||||
|
pub algorithm: SlotAlgorithmId,
|
||||||
|
/// PIN and touch policy
|
||||||
|
pub policy: Option<(PinPolicy, TouchPolicy)>,
|
||||||
|
/// Imported or generated key
|
||||||
|
pub origin: Option<Origin>,
|
||||||
|
/// Pub key of the key
|
||||||
|
pub public: Option<SubjectPublicKeyInfoOwned>,
|
||||||
|
/// Whether PIN PUK and management key are default
|
||||||
|
pub default: Option<bool>,
|
||||||
|
/// Number of retries left
|
||||||
|
pub retries: Option<Retries>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Buffer> for SlotMetadata {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(buf: Buffer) -> Result<Self> {
|
||||||
|
use nom::{
|
||||||
|
combinator::{eof, map_res},
|
||||||
|
multi::fold_many1,
|
||||||
|
number::complete::u8,
|
||||||
|
Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = fold_many1(
|
||||||
|
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|
||||||
|
|| {
|
||||||
|
Ok(SlotMetadata {
|
||||||
|
algorithm: SlotAlgorithmId::PinPuk,
|
||||||
|
policy: None,
|
||||||
|
origin: None,
|
||||||
|
public: None,
|
||||||
|
default: None,
|
||||||
|
retries: None,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|acc: Result<SlotMetadata>, tlv| match acc {
|
||||||
|
Ok(mut metadata) => match tlv.tag {
|
||||||
|
1 => {
|
||||||
|
metadata.algorithm = SlotAlgorithmId::try_from(tlv.value[0])?;
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
fn policy_parser(
|
||||||
|
i: &[u8],
|
||||||
|
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
|
||||||
|
let (i, pin) = map_res(u8, PinPolicy::try_from).parse(i)?;
|
||||||
|
let (i, touch) = map_res(u8, TouchPolicy::try_from).parse(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, (pin, touch)))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.policy =
|
||||||
|
Some(policy_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> {
|
||||||
|
let (i, origin) = map_res(u8, Origin::try_from).parse(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.origin =
|
||||||
|
Some(origin_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
match metadata.algorithm {
|
||||||
|
SlotAlgorithmId::Asymmetric(alg) => {
|
||||||
|
metadata.public = Some(read_public_key(alg, tlv.value, false)?);
|
||||||
|
}
|
||||||
|
_ => Err(Error::ParseError)?,
|
||||||
|
}
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
fn default_parser(i: &[u8]) -> nom::IResult<&[u8], bool> {
|
||||||
|
let (i, default) = u8(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((i, default == 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.default =
|
||||||
|
Some(default_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
6 => {
|
||||||
|
fn retries_parser(i: &[u8]) -> nom::IResult<&[u8], Retries> {
|
||||||
|
let (i, retry_count) = u8(i)?;
|
||||||
|
let (i, remaining_count) = u8(i)?;
|
||||||
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
i,
|
||||||
|
Retries {
|
||||||
|
retry_count,
|
||||||
|
remaining_count,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.retries =
|
||||||
|
Some(retries_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
_unsupported => {
|
||||||
|
// New unsupported tags
|
||||||
|
// https://docs.yubico.com/yesdk/users-manual/application-piv/apdu/metadata.html
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => err,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.parse(buf.as_ref());
|
||||||
|
|
||||||
|
match out {
|
||||||
|
Ok((_, res)) => res,
|
||||||
|
_ => Err(Error::ParseError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of retries used and remaining.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct Retries {
|
||||||
|
/// TODO
|
||||||
|
pub retry_count: u8,
|
||||||
|
/// Remaining attempts
|
||||||
|
pub remaining_count: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Origin of a slot
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum Origin {
|
||||||
|
/// The key has been imported
|
||||||
|
Imported,
|
||||||
|
/// The key has been generated on the YubiKey
|
||||||
|
Generated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for Origin {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Origin::Generated),
|
||||||
|
2 => Ok(Origin::Imported),
|
||||||
|
_ => Err(Error::GenericError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_public_key(
|
||||||
|
algorithm: AlgorithmId,
|
||||||
|
input: &[u8],
|
||||||
|
skip_asn1_tag: bool,
|
||||||
|
) -> Result<SubjectPublicKeyInfoOwned> {
|
||||||
|
// TODO(str4d): Response is wrapped in an ASN.1 TLV:
|
||||||
|
//
|
||||||
|
// 0x7f 0x49 -> Application | Constructed | 0x49
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::X25519 | AlgorithmId::Ed25519 => {
|
||||||
|
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
|
||||||
|
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
|
||||||
|
let data = if skip_asn1_tag { &input[3..] } else { input };
|
||||||
|
|
||||||
|
let (_, tlv) = Tlv::parse(data)?;
|
||||||
|
let pk_data: [u8; 32] = tlv.value.try_into().map_err(|_| Error::InvalidObject)?;
|
||||||
|
|
||||||
|
match algorithm {
|
||||||
|
AlgorithmId::Ed25519 => SubjectPublicKeyInfoOwned::from_der(
|
||||||
|
ed25519_dalek::VerifyingKey::from_bytes(&pk_data)
|
||||||
|
.map_err(|_| Error::InvalidObject)?
|
||||||
|
.to_public_key_der()
|
||||||
|
.map_err(|_| Error::InvalidObject)?
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject),
|
||||||
|
AlgorithmId::X25519 => Ok(SubjectPublicKeyInfoOwned {
|
||||||
|
algorithm: AlgorithmIdentifier {
|
||||||
|
oid: OID_X25519,
|
||||||
|
parameters: None,
|
||||||
|
},
|
||||||
|
subject_public_key: BitString::from_bytes(
|
||||||
|
x25519_dalek::PublicKey::from(pk_data).as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject)?,
|
||||||
|
}),
|
||||||
|
_ => Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa1024
|
||||||
|
| AlgorithmId::Rsa2048
|
||||||
|
| AlgorithmId::Rsa3072
|
||||||
|
| AlgorithmId::Rsa4096 => {
|
||||||
|
// It appears that the inner application-specific value returned by the
|
||||||
|
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
|
||||||
|
//
|
||||||
|
// - Use a full ASN.1 parser on the entire datastructure:
|
||||||
|
//
|
||||||
|
// RSA 1024:
|
||||||
|
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | tag | len:136 |0x81| len:128 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// RSA 2048:
|
||||||
|
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | tag | len:265 |0x81| len:256 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// - Skip the first 5 bytes and use crate::serialize::get_length during TLV
|
||||||
|
// parsing (which treats 128 as a single-byte definite length instead of an
|
||||||
|
// indefinite length):
|
||||||
|
//
|
||||||
|
// RSA 1024:
|
||||||
|
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | |0x81|len128| modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// RSA 2048:
|
||||||
|
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
|
||||||
|
// | |0x81| len:256 | modulus |0x82|len3| exp |
|
||||||
|
//
|
||||||
|
// Because of the above, treat this for now as a 2-byte ASN.1 tag with a
|
||||||
|
// 3-byte length.
|
||||||
|
let data = if skip_asn1_tag { &input[5..] } else { input };
|
||||||
|
|
||||||
|
let (data, modulus_tlv) = Tlv::parse(data)?;
|
||||||
|
if modulus_tlv.tag != TAG_RSA_MODULUS {
|
||||||
|
error!("Failed to parse public key structure (modulus)");
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
let modulus = modulus_tlv.value.to_vec();
|
||||||
|
|
||||||
|
let (_, exp_tlv) = Tlv::parse(data)?;
|
||||||
|
if exp_tlv.tag != TAG_RSA_EXP {
|
||||||
|
error!("failed to parse public key structure (public exponent)");
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
let exp = exp_tlv.value.to_vec();
|
||||||
|
|
||||||
|
let pubkey = RsaPublicKey::new(
|
||||||
|
BoxedUint::from_be_slice_vartime(&modulus),
|
||||||
|
BoxedUint::from_be_slice_vartime(&exp),
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject)?;
|
||||||
|
Ok(SubjectPublicKeyInfoOwned::from_der(
|
||||||
|
pubkey
|
||||||
|
.to_public_key_der()
|
||||||
|
.map_err(|_| Error::ParseError)?
|
||||||
|
.as_bytes(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
|
||||||
|
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
|
||||||
|
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
|
||||||
|
let data = if skip_asn1_tag { &input[3..] } else { input };
|
||||||
|
|
||||||
|
let len = if let AlgorithmId::EccP256 = algorithm {
|
||||||
|
CB_ECC_POINTP256
|
||||||
|
} else {
|
||||||
|
CB_ECC_POINTP384
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, tlv) = Tlv::parse(data)?;
|
||||||
|
|
||||||
|
if tlv.tag != TAG_ECC_POINT {
|
||||||
|
error!("failed to parse public key structure");
|
||||||
|
return Err(Error::ParseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the curve point should always be determined by the curve
|
||||||
|
if tlv.value.len() != len {
|
||||||
|
error!("unexpected length");
|
||||||
|
return Err(Error::AlgorithmError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = tlv.value.to_vec();
|
||||||
|
|
||||||
|
let pubkey = match algorithm {
|
||||||
|
AlgorithmId::EccP256 => PublicKey::<NistP256>::try_from(
|
||||||
|
EcPublicKey::<NistP256>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject)?
|
||||||
|
.to_public_key_der(),
|
||||||
|
AlgorithmId::EccP384 => PublicKey::<NistP384>::try_from(
|
||||||
|
EcPublicKey::<NistP384>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::InvalidObject)?
|
||||||
|
.to_public_key_der(),
|
||||||
|
_ => return Err(Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
.map_err(|_| Error::InvalidObject)?;
|
||||||
|
|
||||||
|
Ok(SubjectPublicKeyInfoOwned::from_der(pubkey.as_bytes())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
/// Algorithms as reported by the metadata command.
|
||||||
|
pub enum SlotAlgorithmId {
|
||||||
|
/// Used on PIN and PUK slots.
|
||||||
|
PinPuk,
|
||||||
|
/// Used on the key management slot.
|
||||||
|
Management(MgmAlgorithmId),
|
||||||
|
/// Used on all other slots.
|
||||||
|
Asymmetric(AlgorithmId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for SlotAlgorithmId {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0xff => Ok(SlotAlgorithmId::PinPuk),
|
||||||
|
oth => MgmAlgorithmId::try_from(oth)
|
||||||
|
.map(SlotAlgorithmId::Management)
|
||||||
|
.or_else(|_| AlgorithmId::try_from(oth).map(SlotAlgorithmId::Asymmetric)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SlotAlgorithmId> for u8 {
|
||||||
|
fn from(id: SlotAlgorithmId) -> u8 {
|
||||||
|
match id {
|
||||||
|
SlotAlgorithmId::PinPuk => 0xff,
|
||||||
|
SlotAlgorithmId::Management(oth) => oth.into(),
|
||||||
|
SlotAlgorithmId::Asymmetric(oth) => oth.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+31
-3
@@ -1,12 +1,12 @@
|
|||||||
//! Enums representing key policies.
|
//! Enums representing key policies.
|
||||||
|
|
||||||
use crate::{serialization::Tlv, Result};
|
use crate::{serialization::Tlv, Error, Result};
|
||||||
|
|
||||||
/// Specifies how often the PIN needs to be entered for access to the credential in a
|
/// Specifies how often the PIN needs to be entered for access to the credential in a
|
||||||
/// given slot.
|
/// given slot.
|
||||||
///
|
///
|
||||||
/// This policy must be set when keys are generated or imported, and cannot be changed later.
|
/// This policy must be set when keys are generated or imported, and cannot be changed later.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum PinPolicy {
|
pub enum PinPolicy {
|
||||||
/// Use the default PIN policy for the slot. See the slot's documentation for details.
|
/// Use the default PIN policy for the slot. See the slot's documentation for details.
|
||||||
Default,
|
Default,
|
||||||
@@ -35,6 +35,20 @@ impl From<PinPolicy> for u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl PinPolicy {
|
||||||
/// Writes the `PinPolicy` in the format the YubiKey expects during key generation or
|
/// Writes the `PinPolicy` in the format the YubiKey expects during key generation or
|
||||||
/// importation.
|
/// importation.
|
||||||
@@ -50,7 +64,7 @@ impl PinPolicy {
|
|||||||
/// addition to the [`PinPolicy`].
|
/// addition to the [`PinPolicy`].
|
||||||
///
|
///
|
||||||
/// This policy must be set when keys are generated or imported, and cannot be changed later.
|
/// This policy must be set when keys are generated or imported, and cannot be changed later.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum TouchPolicy {
|
pub enum TouchPolicy {
|
||||||
/// Use the default touch policy for the slot.
|
/// Use the default touch policy for the slot.
|
||||||
Default,
|
Default,
|
||||||
@@ -90,3 +104,17 @@ impl TouchPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-3
@@ -1,9 +1,10 @@
|
|||||||
//! Support for enumerating available PC/SC card readers.
|
//! Support for enumerating available PC/SC card readers.
|
||||||
|
|
||||||
use crate::{Result, YubiKey};
|
use crate::{Error, Result, YubiKey};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::CStr,
|
ffi::CStr,
|
||||||
|
fmt,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,6 +20,12 @@ pub struct Context {
|
|||||||
reader_names: Vec<u8>,
|
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 {
|
impl Context {
|
||||||
/// Open a PC/SC context, which can be used to enumerate available PC/SC
|
/// Open a PC/SC context, which can be used to enumerate available PC/SC
|
||||||
/// readers (which can be used to connect to YubiKeys).
|
/// readers (which can be used to connect to YubiKeys).
|
||||||
@@ -36,7 +43,8 @@ impl Context {
|
|||||||
let Self { ctx, reader_names } = self;
|
let Self { ctx, reader_names } = self;
|
||||||
|
|
||||||
let reader_cstrs: Vec<_> = {
|
let reader_cstrs: Vec<_> = {
|
||||||
let c = ctx.lock().unwrap();
|
// TODO(tarcieri): better error?
|
||||||
|
let c = ctx.lock().map_err(|_| Error::GenericError)?;
|
||||||
|
|
||||||
// ensure PC/SC context is valid
|
// ensure PC/SC context is valid
|
||||||
c.is_valid()?;
|
c.is_valid()?;
|
||||||
@@ -83,7 +91,8 @@ impl<'ctx> Reader<'ctx> {
|
|||||||
|
|
||||||
/// Connect to this reader, returning its `pcsc::Card`.
|
/// Connect to this reader, returning its `pcsc::Card`.
|
||||||
pub(crate) fn connect(&self) -> Result<pcsc::Card> {
|
pub(crate) fn connect(&self) -> Result<pcsc::Card> {
|
||||||
let ctx = self.ctx.lock().unwrap();
|
// TODO(tarcieri): better error?
|
||||||
|
let ctx = self.ctx.lock().map_err(|_| Error::GenericError)?;
|
||||||
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
|
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -41,7 +41,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Source of how a setting was configured.
|
/// Source of how a setting was configured.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
pub enum SettingSource {
|
pub enum SettingSource {
|
||||||
/// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars.
|
/// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars.
|
||||||
User,
|
User,
|
||||||
@@ -51,15 +51,10 @@ pub enum SettingSource {
|
|||||||
Admin,
|
Admin,
|
||||||
|
|
||||||
/// Default setting.
|
/// Default setting.
|
||||||
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SettingSource {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setting booleans: configuration values sourced from a file or the environment.
|
/// Setting booleans: configuration values sourced from a file or the environment.
|
||||||
///
|
///
|
||||||
/// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a
|
/// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a
|
||||||
@@ -122,7 +117,7 @@ impl Setting {
|
|||||||
|
|
||||||
/// Get a setting boolean from an environment variable
|
/// Get a setting boolean from an environment variable
|
||||||
fn from_env(key: &str) -> Option<Self> {
|
fn from_env(key: &str) -> Option<Self> {
|
||||||
env::var(format!("YUBIKEY_PIV_{}", key))
|
env::var(format!("YUBIKEY_PIV_{key}"))
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| Setting {
|
.map(|value| Setting {
|
||||||
source: SettingSource::User,
|
source: SettingSource::User,
|
||||||
|
|||||||
+187
-92
@@ -5,7 +5,9 @@ use crate::{
|
|||||||
apdu::{Apdu, Ins, StatusWords},
|
apdu::{Apdu, Ins, StatusWords},
|
||||||
consts::{CB_BUF_MAX, CB_OBJ_MAX},
|
consts::{CB_BUF_MAX, CB_OBJ_MAX},
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
piv::{AlgorithmId, SlotId},
|
mgm::MgmKey,
|
||||||
|
otp,
|
||||||
|
piv::{self, AlgorithmId, SlotId},
|
||||||
serialization::*,
|
serialization::*,
|
||||||
yubikey::*,
|
yubikey::*,
|
||||||
Buffer, ObjectId,
|
Buffer, ObjectId,
|
||||||
@@ -14,13 +16,7 @@ use log::{error, trace};
|
|||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
use crate::mgm::{MgmKey, DES_LEN_3DES};
|
use crate::mgm::{DeviceConfig, DeviceInfo, Lock};
|
||||||
|
|
||||||
/// PIV Applet ID
|
|
||||||
const PIV_AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
|
|
||||||
|
|
||||||
/// YubiKey OTP Applet ID. Needed to query serial on YK4.
|
|
||||||
const YK_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
|
|
||||||
|
|
||||||
const CB_PIN_MAX: usize = 8;
|
const CB_PIN_MAX: usize = 8;
|
||||||
|
|
||||||
@@ -65,23 +61,34 @@ impl<'tx> Transaction<'tx> {
|
|||||||
Ok(recv_buffer)
|
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.
|
/// Select application.
|
||||||
pub fn select_application(&self) -> Result<()> {
|
pub fn select_application(
|
||||||
|
&self,
|
||||||
|
applet: &[u8],
|
||||||
|
applet_name: &'static str,
|
||||||
|
error: &'static str,
|
||||||
|
) -> Result<()> {
|
||||||
let response = Apdu::new(Ins::SelectApplication)
|
let response = Apdu::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(&PIV_AID)
|
.data(applet)
|
||||||
.transmit(self, 0xFF)
|
.transmit(self, 0xFF)
|
||||||
.map_err(|e| {
|
.inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
|
||||||
error!("failed communicating with card: '{}'", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
error!(
|
error!("{}: {:04x}", error, response.status_words().code());
|
||||||
"failed selecting application: {:04x}",
|
return Err(match response.status_words() {
|
||||||
response.status_words().code()
|
StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
|
||||||
);
|
_ => Error::GenericError,
|
||||||
return Err(Error::GenericError);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -92,74 +99,81 @@ impl<'tx> Transaction<'tx> {
|
|||||||
// get version from device
|
// get version from device
|
||||||
let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?;
|
let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?;
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() || response.data().is_empty() {
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.data().len() < 3 {
|
Ok(response.data()[..3].try_into().map(Version::new)?)
|
||||||
return Err(Error::SizeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Version::new(response.data()[..3].try_into().unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get YubiKey device serial number.
|
/// Get YubiKey device serial number.
|
||||||
pub fn get_serial(&self, version: Version) -> Result<Serial> {
|
pub fn get_serial(&self, version: Version) -> Result<Serial> {
|
||||||
let response = if version.major < 5 {
|
match version.major {
|
||||||
// YK4 requires switching to the yk applet to retrieve the serial
|
// YK4 requires switching to the YK applet to retrieve the serial
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
4 => {
|
||||||
.p1(0x04)
|
self.select_application(
|
||||||
.data(&YK_AID)
|
otp::APPLET_ID,
|
||||||
.transmit(self, 0xFF)?
|
otp::APPLET_NAME,
|
||||||
.status_words();
|
"failed selecting yk application",
|
||||||
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
||||||
error!("failed selecting yk application: {:04x}", sw.code());
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
if !response.is_success() {
|
||||||
|
// TODO(tarcieri): still reselect the PIV applet in this case?
|
||||||
if !resp.is_success() {
|
|
||||||
error!(
|
error!(
|
||||||
"failed retrieving serial number: {:04x}",
|
"failed retrieving serial number: {:04x}",
|
||||||
resp.status_words().code()
|
response.status_words().code()
|
||||||
);
|
);
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// reselect the PIV applet
|
// reselect the PIV applet
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
self.select_application(
|
||||||
.p1(0x04)
|
piv::APPLET_ID,
|
||||||
.data(&PIV_AID)
|
piv::APPLET_NAME,
|
||||||
.transmit(self, 0xFF)?
|
"failed selecting application",
|
||||||
.status_words();
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
response.data().try_into()
|
||||||
error!("failed selecting application: {:04x}", sw.code());
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp
|
|
||||||
} else {
|
|
||||||
// YK5 implements getting the serial as a PIV applet command (0xf8)
|
// YK5 implements getting the serial as a PIV applet command (0xf8)
|
||||||
let resp = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
|
5 => {
|
||||||
|
let response = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
if !resp.is_success() {
|
if !response.is_success() {
|
||||||
error!(
|
error!(
|
||||||
"failed retrieving serial number: {:04x}",
|
"failed retrieving serial number: {:04x}",
|
||||||
resp.status_words().code()
|
response.status_words().code()
|
||||||
);
|
);
|
||||||
return Err(Error::GenericError);
|
return Err(Error::GenericError);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp
|
response.data().try_into()
|
||||||
};
|
}
|
||||||
|
|
||||||
response.data()[..4]
|
// Other versions unsupported
|
||||||
.try_into()
|
_ => Err(Error::NotSupported),
|
||||||
.map(|serial| Serial::from(u32::from_be_bytes(serial)))
|
}
|
||||||
.map_err(|_| Error::SizeError)
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// Verify device PIN.
|
||||||
@@ -177,7 +191,7 @@ impl<'tx> Transaction<'tx> {
|
|||||||
if !pin.is_empty() {
|
if !pin.is_empty() {
|
||||||
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
|
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
|
||||||
data[0..pin.len()].copy_from_slice(pin);
|
data[0..pin.len()].copy_from_slice(pin);
|
||||||
query.data(data.as_ref());
|
query.data(data.as_slice());
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = query.transmit(self, 261)?;
|
let response = query.transmit(self, 261)?;
|
||||||
@@ -234,19 +248,18 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the management key (MGM).
|
/// Set the management key (MGM).
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
|
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
|
||||||
let p2 = if require_touch { 0xfe } else { 0xff };
|
let p2 = if require_touch { 0xfe } else { 0xff };
|
||||||
|
|
||||||
let mut data = [0u8; DES_LEN_3DES + 3];
|
let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
|
||||||
data[0] = ALGO_3DES;
|
data.push(new_key.algorithm_id().into());
|
||||||
data[1] = KEY_CARDMGM;
|
data.push(KEY_CARDMGM);
|
||||||
data[2] = DES_LEN_3DES as u8;
|
data.push(new_key.key_size());
|
||||||
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
|
data.extend_from_slice(new_key.as_ref());
|
||||||
|
|
||||||
let status_words = Apdu::new(Ins::SetMgmKey)
|
let status_words = Apdu::new(Ins::SetMgmKey)
|
||||||
.params(0xff, p2)
|
.params(0xff, p2)
|
||||||
.data(&data)
|
.data(data)
|
||||||
.transmit(self, 261)?
|
.transmit(self, 261)?
|
||||||
.status_words();
|
.status_words();
|
||||||
|
|
||||||
@@ -275,14 +288,23 @@ impl<'tx> Transaction<'tx> {
|
|||||||
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
|
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
|
||||||
|
|
||||||
match algorithm {
|
match algorithm {
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
AlgorithmId::Rsa1024 => {
|
||||||
let key_len = if let AlgorithmId::Rsa1024 = algorithm {
|
if in_len != 128 {
|
||||||
128
|
return Err(Error::SizeError);
|
||||||
} else {
|
}
|
||||||
256
|
}
|
||||||
};
|
AlgorithmId::Rsa2048 => {
|
||||||
|
if in_len != 256 {
|
||||||
if in_len != key_len {
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa3072 => {
|
||||||
|
if in_len != 384 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlgorithmId::Rsa4096 => {
|
||||||
|
if in_len != 512 {
|
||||||
return Err(Error::SizeError);
|
return Err(Error::SizeError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,6 +320,19 @@ impl<'tx> Transaction<'tx> {
|
|||||||
return Err(Error::SizeError);
|
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 {
|
let bytes = if in_len < 0x80 {
|
||||||
@@ -314,7 +349,9 @@ impl<'tx> Transaction<'tx> {
|
|||||||
Tlv::write(
|
Tlv::write(
|
||||||
&mut buf[2..],
|
&mut buf[2..],
|
||||||
match (algorithm, decipher) {
|
match (algorithm, decipher) {
|
||||||
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
|
(AlgorithmId::EccP256, true)
|
||||||
|
| (AlgorithmId::EccP384, true)
|
||||||
|
| (AlgorithmId::X25519, true) => 0x85,
|
||||||
_ => 0x81,
|
_ => 0x81,
|
||||||
},
|
},
|
||||||
sign_in
|
sign_in
|
||||||
@@ -326,10 +363,7 @@ impl<'tx> Transaction<'tx> {
|
|||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.transfer_data(&templ, &indata[..offset], 1024)
|
.transfer_data(&templ, &indata[..offset], 1024)
|
||||||
.map_err(|e| {
|
.inspect_err(|e| error!("sign command failed to communicate: {}", e))?;
|
||||||
error!("sign command failed to communicate: {}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
error!("failed sign command with code {:x}", response.code());
|
error!("failed sign command with code {:x}", response.code());
|
||||||
@@ -387,11 +421,12 @@ impl<'tx> Transaction<'tx> {
|
|||||||
.data(&in_data[in_offset..(in_offset + this_size)])
|
.data(&in_data[in_offset..(in_offset + this_size)])
|
||||||
.transmit(self, 261)?;
|
.transmit(self, 261)?;
|
||||||
|
|
||||||
sw = response.status_words().code();
|
sw = response.status_words();
|
||||||
|
|
||||||
if !response.is_success() && (sw >> 8 != 0x61) {
|
match sw {
|
||||||
|
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
|
||||||
// TODO(tarcieri): is this really OK?
|
// TODO(tarcieri): is this really OK?
|
||||||
return Ok(Response::new(sw.into(), out_data));
|
_ => return Ok(Response::new(sw, out_data)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
|
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
|
||||||
@@ -412,17 +447,15 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while sw >> 8 == 0x61 {
|
while let StatusWords::BytesRemaining { len } = sw {
|
||||||
trace!(
|
trace!("The card indicates there is {} bytes more data for us", len);
|
||||||
"The card indicates there is {} bytes more data for us",
|
|
||||||
sw & 0xff
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?;
|
let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?;
|
||||||
sw = response.status_words().code();
|
sw = response.status_words();
|
||||||
|
|
||||||
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) {
|
match sw {
|
||||||
return Ok(Response::new(sw.into(), vec![]));
|
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
|
||||||
|
_ => return Ok(Response::new(sw, vec![])),
|
||||||
}
|
}
|
||||||
|
|
||||||
if out_data.len() + response.data().len() > max_out {
|
if out_data.len() + response.data().len() > max_out {
|
||||||
@@ -438,7 +471,7 @@ impl<'tx> Transaction<'tx> {
|
|||||||
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
out_data.extend_from_slice(&response.data()[..response.data().len()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Response::new(sw.into(), out_data))
|
Ok(Response::new(sw, out_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch an object.
|
/// Fetch an object.
|
||||||
@@ -503,4 +536,66 @@ impl<'tx> Transaction<'tx> {
|
|||||||
_ => Err(Error::GenericError),
|
_ => 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+242
-104
@@ -41,10 +41,12 @@ use crate::{
|
|||||||
reader::{Context, Reader},
|
reader::{Context, Reader},
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
};
|
};
|
||||||
|
use cipher::common::getrandom::SysRng;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use pcsc::Card;
|
use pcsc::Card;
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::TryRng;
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::{Ord, Ordering},
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
@@ -55,10 +57,10 @@ use {
|
|||||||
apdu::StatusWords,
|
apdu::StatusWords,
|
||||||
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
|
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
|
||||||
metadata::AdminData,
|
metadata::AdminData,
|
||||||
|
mgm,
|
||||||
transaction::ChangeRefAction,
|
transaction::ChangeRefAction,
|
||||||
Buffer, ObjectId,
|
Buffer, ObjectId,
|
||||||
},
|
},
|
||||||
secrecy::ExposeSecret,
|
|
||||||
std::time::{SystemTime, UNIX_EPOCH},
|
std::time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,24 +68,20 @@ use {
|
|||||||
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
|
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
|
||||||
|
|
||||||
/// 3DES authentication
|
/// 3DES authentication
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
pub(crate) const ALGO_3DES: u8 = 0x03;
|
pub(crate) const ALGO_3DES: u8 = 0x03;
|
||||||
|
|
||||||
/// Card management key
|
/// Card management key
|
||||||
pub(crate) const KEY_CARDMGM: u8 = 0x9b;
|
pub(crate) const KEY_CARDMGM: u8 = 0x9b;
|
||||||
|
|
||||||
/// MGMT Applet ID.
|
|
||||||
///
|
|
||||||
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
|
|
||||||
#[cfg(feature = "untested")]
|
|
||||||
const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
|
|
||||||
|
|
||||||
const TAG_DYN_AUTH: u8 = 0x7c;
|
const TAG_DYN_AUTH: u8 = 0x7c;
|
||||||
|
|
||||||
/// Cached YubiKey PIN.
|
/// Cached YubiKey PIN.
|
||||||
pub type CachedPin = secrecy::SecretVec<u8>;
|
// TODO(tarcieri): add a newtype for this with a zeroize impl
|
||||||
|
pub type CachedPin = Vec<u8>;
|
||||||
|
|
||||||
/// YubiKey serial number.
|
/// YubiKey serial number.
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
pub struct Serial(pub u32);
|
pub struct Serial(pub u32);
|
||||||
|
|
||||||
impl From<u32> for Serial {
|
impl From<u32> for Serial {
|
||||||
@@ -98,6 +96,20 @@ impl From<Serial> for u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for Serial {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(bytes: &[u8]) -> Result<Self> {
|
||||||
|
if bytes.len() > 4 {
|
||||||
|
return Err(Error::SizeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut arr = [0u8; 4];
|
||||||
|
arr[(4 - bytes.len())..].copy_from_slice(bytes);
|
||||||
|
Ok(Self(u32::from_be_bytes(arr)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for Serial {
|
impl FromStr for Serial {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
@@ -134,6 +146,22 @@ impl Version {
|
|||||||
patch: bytes[2],
|
patch: bytes[2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "untested")]
|
||||||
|
pub(crate) fn parse(input: &[u8]) -> Result<Self> {
|
||||||
|
use nom::{combinator::eof, number::complete::u8};
|
||||||
|
|
||||||
|
let (i, major) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (i, minor) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (i, patch) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Version {
|
impl Display for Version {
|
||||||
@@ -142,6 +170,39 @@ impl Display for Version {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ord for Version {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.major > other.major {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.major < other.major {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.minor > other.minor {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.minor < other.minor {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.patch > other.patch {
|
||||||
|
return Ordering::Greater;
|
||||||
|
}
|
||||||
|
if self.patch < other.patch {
|
||||||
|
return Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Version {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// YubiKey device: primary API for opening a session and performing various operations.
|
/// YubiKey device: primary API for opening a session and performing various operations.
|
||||||
///
|
///
|
||||||
/// Almost all functionality in this library will require an open session
|
/// Almost all functionality in this library will require an open session
|
||||||
@@ -156,64 +217,101 @@ pub struct YubiKey {
|
|||||||
pub(crate) serial: Serial,
|
pub(crate) serial: Serial,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for YubiKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("YubiKey")
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("serial", &self.serial)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl YubiKey {
|
impl YubiKey {
|
||||||
/// Open a connection to a YubiKey.
|
/// Open a connection to a YubiKey.
|
||||||
///
|
///
|
||||||
/// Returns an error if there is more than one YubiKey detected.
|
/// Returns an error if more than one YubiKey is detected (or none at all).
|
||||||
|
///
|
||||||
|
/// NOTE: If multiple YubiKeys are connected, but we are only able to
|
||||||
|
/// open one of them (e.g. because the other one is in use, and the
|
||||||
|
/// connection doesn't allow sharing), the YubiKey that we were able to
|
||||||
|
/// open is returned.
|
||||||
///
|
///
|
||||||
/// If you need to operate in environments with more than one YubiKey
|
/// If you need to operate in environments with more than one YubiKey
|
||||||
/// attached to the same system, use [`YubiKey::open_by_serial`] or
|
/// attached to the same system, use [`YubiKey::open_by_serial`] or
|
||||||
/// [`yubikey::reader::Context`][`Context`] to select from the available
|
/// [`yubikey::reader::Context`][`Context`] to select from the available
|
||||||
/// PC/SC readers.
|
/// PC/SC readers.
|
||||||
pub fn open() -> Result<Self> {
|
pub fn open() -> Result<Self> {
|
||||||
let mut readers = Context::open().map_err(|e| match e {
|
let mut yubikey: Option<Self> = None;
|
||||||
Error::PcscError {
|
|
||||||
inner: Some(pcsc::Error::NoReadersAvailable),
|
let mut readers = Context::open()?;
|
||||||
} => Error::NotFound,
|
for reader in readers.iter()? {
|
||||||
other => other,
|
if let Ok(yk_found) = reader.open() {
|
||||||
})?;
|
if let Some(yk_stored) = yubikey {
|
||||||
let mut reader_iter = readers.iter()?;
|
// We found two YubiKeys, so we won't use either.
|
||||||
|
// Don't reset them.
|
||||||
|
let _ = yk_stored.disconnect(pcsc::Disposition::LeaveCard);
|
||||||
|
let _ = yk_found.disconnect(pcsc::Disposition::LeaveCard);
|
||||||
|
|
||||||
if let Some(reader) = reader_iter.next() {
|
|
||||||
if reader_iter.next().is_some() {
|
|
||||||
error!("multiple YubiKeys detected!");
|
error!("multiple YubiKeys detected!");
|
||||||
return Err(Error::PcscError { inner: None });
|
return Err(Error::PcscError { inner: None });
|
||||||
|
} else {
|
||||||
|
yubikey = Some(yk_found);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader.open();
|
if let Some(yubikey) = yubikey {
|
||||||
}
|
// We found exactly one YubiKey that we could open, so we return it.
|
||||||
|
Ok(yubikey)
|
||||||
|
} else {
|
||||||
error!("no YubiKey detected!");
|
error!("no YubiKey detected!");
|
||||||
Err(Error::NotFound)
|
Err(Error::NotFound)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Open a YubiKey with a specific serial number.
|
/// Open a YubiKey with a specific serial number.
|
||||||
pub fn open_by_serial(serial: Serial) -> Result<Self> {
|
pub fn open_by_serial(serial: Serial) -> Result<Self> {
|
||||||
let mut readers = Context::open().map_err(|e| match e {
|
let mut readers = Context::open()?;
|
||||||
Error::PcscError {
|
|
||||||
inner: Some(pcsc::Error::NoReadersAvailable),
|
let mut open_error = None;
|
||||||
} => Error::NotFound,
|
|
||||||
other => other,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for reader in readers.iter()? {
|
for reader in readers.iter()? {
|
||||||
let yubikey = match reader.open() {
|
let yubikey = match reader.open() {
|
||||||
Ok(yk) => yk,
|
Ok(yk) => yk,
|
||||||
Err(_) => continue,
|
Err(e) => {
|
||||||
|
// Save the first error we see that indicates we might have been able
|
||||||
|
// to find a matching YubiKey.
|
||||||
|
if open_error.is_none() {
|
||||||
|
if let Error::PcscError {
|
||||||
|
inner: Some(pcsc::Error::SharingViolation),
|
||||||
|
} = e
|
||||||
|
{
|
||||||
|
open_error = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if serial == yubikey.serial() {
|
if serial == yubikey.serial() {
|
||||||
return Ok(yubikey);
|
return Ok(yubikey);
|
||||||
|
} else {
|
||||||
|
// We didn't want this YubiKey; don't reset it.
|
||||||
|
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Err(if let Some(e) = open_error {
|
||||||
|
e
|
||||||
|
} else {
|
||||||
error!("no YubiKey detected with serial: {}", serial);
|
error!("no YubiKey detected with serial: {}", serial);
|
||||||
Err(Error::NotFound)
|
Error::NotFound
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconnect to a YubiKey.
|
/// Reconnect to a YubiKey.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn reconnect(&mut self) -> Result<()> {
|
pub fn reconnect(&mut self) -> Result<()> {
|
||||||
info!("trying to reconnect to current reader");
|
info!("trying to reconnect to current reader");
|
||||||
|
|
||||||
@@ -223,13 +321,10 @@ impl YubiKey {
|
|||||||
pcsc::Disposition::ResetCard,
|
pcsc::Disposition::ResetCard,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pin = self
|
let pin = self.pin.as_ref().map(|p| Buffer::new(p.clone()));
|
||||||
.pin
|
|
||||||
.as_ref()
|
|
||||||
.map(|p| Buffer::new(p.expose_secret().clone()));
|
|
||||||
|
|
||||||
let txn = Transaction::new(&mut self.card)?;
|
let txn = Transaction::new(&mut self.card)?;
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
if let Some(p) = &pin {
|
if let Some(p) = &pin {
|
||||||
txn.verify_pin(p)?;
|
txn.verify_pin(p)?;
|
||||||
@@ -238,6 +333,41 @@ impl YubiKey {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disconnect from the YubiKey.
|
||||||
|
///
|
||||||
|
/// In case of error, ownership of the YubiKey is returned to the caller.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// `YubiKey` implements `Drop` which automatically disconnects the card using
|
||||||
|
/// `Disposition::ResetCard`; you only need to call this function if you want to
|
||||||
|
/// handle errors or use a different disposition method.
|
||||||
|
pub fn disconnect(
|
||||||
|
self,
|
||||||
|
disposition: pcsc::Disposition,
|
||||||
|
) -> core::result::Result<(), (Self, Error)> {
|
||||||
|
let Self {
|
||||||
|
card,
|
||||||
|
name,
|
||||||
|
pin,
|
||||||
|
version,
|
||||||
|
serial,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
card.disconnect(disposition).map_err(|(card, e)| {
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
card,
|
||||||
|
name,
|
||||||
|
pin,
|
||||||
|
version,
|
||||||
|
serial,
|
||||||
|
},
|
||||||
|
e.into(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Begin a transaction.
|
/// Begin a transaction.
|
||||||
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> {
|
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> {
|
||||||
// TODO(tarcieri): reconnect support
|
// TODO(tarcieri): reconnect support
|
||||||
@@ -279,38 +409,47 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate to the card using the provided management key (MGM).
|
/// Authenticate to the card using the provided management key (MGM).
|
||||||
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<()> {
|
pub fn authenticate(&mut self, mgm_key: &MgmKey) -> Result<()> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
// get a challenge from the card
|
// get a challenge from the card
|
||||||
let challenge = Apdu::new(Ins::Authenticate)
|
let card_response = Apdu::new(Ins::Authenticate)
|
||||||
.params(ALGO_3DES, KEY_CARDMGM)
|
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
|
||||||
.data(&[TAG_DYN_AUTH, 0x02, 0x80, 0x00])
|
.data([TAG_DYN_AUTH, 0x02, 0x80, 0x00])
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
if !challenge.is_success() || challenge.data().len() < 12 {
|
if !card_response.is_success() || card_response.data().len() < 5 {
|
||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// send a response to the cards challenge and a challenge of our own.
|
// send a response to the cards challenge and a challenge of our own.
|
||||||
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
|
let card_challenge = mgm_key.card_challenge(&card_response.data()[4..])?;
|
||||||
|
let challenge_len = card_challenge.len();
|
||||||
|
|
||||||
let mut data = [0u8; 22];
|
// If this exceeds a `u8` then the card is giving us unexpected data.
|
||||||
data[0] = TAG_DYN_AUTH;
|
let auth_len = (2 + challenge_len + 2 + challenge_len)
|
||||||
data[1] = 20; // 2 + 8 + 2 +8
|
.try_into()
|
||||||
data[2] = 0x80;
|
.map_err(|_| Error::AuthenticationError)?;
|
||||||
data[3] = 8;
|
|
||||||
data[4..12].copy_from_slice(&response);
|
|
||||||
data[12] = 0x81;
|
|
||||||
data[13] = 8;
|
|
||||||
OsRng.fill_bytes(&mut data[14..22]);
|
|
||||||
|
|
||||||
let mut challenge = [0u8; 8];
|
let mut data = Vec::with_capacity(4 + challenge_len + 2 + challenge_len);
|
||||||
challenge.copy_from_slice(&data[14..22]);
|
data.push(TAG_DYN_AUTH);
|
||||||
|
data.push(auth_len);
|
||||||
|
data.push(0x80);
|
||||||
|
data.push(challenge_len as u8);
|
||||||
|
data.extend_from_slice(&card_challenge);
|
||||||
|
data.push(0x81);
|
||||||
|
data.push(challenge_len as u8);
|
||||||
|
|
||||||
|
let mut host_challenge = vec![0u8; challenge_len];
|
||||||
|
SysRng
|
||||||
|
.try_fill_bytes(&mut host_challenge)
|
||||||
|
.map_err(|_| Error::GenericError)?;
|
||||||
|
|
||||||
|
data.extend_from_slice(&host_challenge);
|
||||||
|
|
||||||
let authentication = Apdu::new(Ins::Authenticate)
|
let authentication = Apdu::new(Ins::Authenticate)
|
||||||
.params(ALGO_3DES, KEY_CARDMGM)
|
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
|
||||||
.data(&data)
|
.data(data)
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
if !authentication.is_success() {
|
if !authentication.is_success() {
|
||||||
@@ -318,14 +457,7 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compare the response from the card with our challenge
|
// compare the response from the card with our challenge
|
||||||
let response = mgm_key.encrypt(&challenge);
|
mgm_key.check_challenge(&host_challenge, &authentication.data()[4..])
|
||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
|
||||||
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
|
|
||||||
return Err(Error::AuthenticationError);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the PIV keys contained in this YubiKey.
|
/// Get the PIV keys contained in this YubiKey.
|
||||||
@@ -335,13 +467,12 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Deauthenticate.
|
/// Deauthenticate.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn deauthenticate(&mut self) -> Result<()> {
|
pub fn deauthenticate(&mut self) -> Result<()> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
let status_words = Apdu::new(Ins::SelectApplication)
|
let status_words = Apdu::new(Ins::SelectApplication)
|
||||||
.p1(0x04)
|
.p1(0x04)
|
||||||
.data(MGMT_AID)
|
.data(mgm::APPLET_ID)
|
||||||
.transmit(&txn, 255)?
|
.transmit(&txn, 255)?
|
||||||
.status_words();
|
.status_words();
|
||||||
|
|
||||||
@@ -350,7 +481,12 @@ impl YubiKey {
|
|||||||
"Failed selecting mgmt application: {:04x}",
|
"Failed selecting mgmt application: {:04x}",
|
||||||
status_words.code()
|
status_words.code()
|
||||||
);
|
);
|
||||||
return Err(Error::GenericError);
|
return Err(match status_words {
|
||||||
|
StatusWords::NotFoundError => Error::AppletNotFound {
|
||||||
|
applet_name: mgm::APPLET_NAME,
|
||||||
|
},
|
||||||
|
_ => Error::GenericError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -364,7 +500,7 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !pin.is_empty() {
|
if !pin.is_empty() {
|
||||||
self.pin = Some(CachedPin::new(pin.into()))
|
self.pin = Some(pin.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -377,7 +513,7 @@ impl YubiKey {
|
|||||||
// Force a re-select to unverify, because once verified the spec dictates that
|
// Force a re-select to unverify, because once verified the spec dictates that
|
||||||
// subsequent verify calls will return a "verification not needed" instead of
|
// subsequent verify calls will return a "verification not needed" instead of
|
||||||
// the number of tries left...
|
// the number of tries left...
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
// WRONG_PIN is expected on successful query.
|
// WRONG_PIN is expected on successful query.
|
||||||
match txn.verify_pin(&[]) {
|
match txn.verify_pin(&[]) {
|
||||||
@@ -389,7 +525,6 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Set the number of PIN retries.
|
/// Set the number of PIN retries.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> {
|
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> {
|
||||||
// Special case: if either retry count is 0, it's a successful no-op
|
// Special case: if either retry count is 0, it's a successful no-op
|
||||||
if pin_tries == 0 || puk_tries == 0 {
|
if pin_tries == 0 || puk_tries == 0 {
|
||||||
@@ -414,7 +549,6 @@ impl YubiKey {
|
|||||||
///
|
///
|
||||||
/// The default PIN code is `123456`.
|
/// The default PIN code is `123456`.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> {
|
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> {
|
||||||
{
|
{
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
@@ -422,7 +556,7 @@ impl YubiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !new_pin.is_empty() {
|
if !new_pin.is_empty() {
|
||||||
self.pin = Some(CachedPin::new(new_pin.into()));
|
self.pin = Some(new_pin.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -430,7 +564,6 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Set PIN last changed.
|
/// Set PIN last changed.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> {
|
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
|
|
||||||
@@ -438,22 +571,17 @@ impl YubiKey {
|
|||||||
|
|
||||||
// TODO(tarcieri): double check this is little endian
|
// TODO(tarcieri): double check this is little endian
|
||||||
let tnow = SystemTime::now()
|
let tnow = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)?
|
||||||
.unwrap()
|
|
||||||
.as_secs()
|
.as_secs()
|
||||||
.to_le_bytes();
|
.to_le_bytes();
|
||||||
|
|
||||||
admin_data
|
admin_data
|
||||||
.set_item(TAG_ADMIN_TIMESTAMP, &tnow)
|
.set_item(TAG_ADMIN_TIMESTAMP, &tnow)
|
||||||
.map_err(|e| {
|
.inspect_err(|e| error!("could not set pin timestamp, err = {}", e))?;
|
||||||
error!("could not set pin timestamp, err = {}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
admin_data.write(&txn).map_err(|e| {
|
admin_data
|
||||||
error!("could not write admin data, err = {}", e);
|
.write(&txn)
|
||||||
e
|
.inspect_err(|e| error!("could not write admin data, err = {}", e))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -466,7 +594,6 @@ impl YubiKey {
|
|||||||
///
|
///
|
||||||
/// The default PUK code is `12345678`.
|
/// The default PUK code is `12345678`.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> {
|
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
|
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
|
||||||
@@ -474,7 +601,6 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Block PUK: permanently prevent the PIN from becoming unblocked.
|
/// Block PUK: permanently prevent the PIN from becoming unblocked.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn block_puk(&mut self) -> Result<()> {
|
pub fn block_puk(&mut self) -> Result<()> {
|
||||||
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
|
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
|
||||||
let mut tries_remaining: i32 = -1;
|
let mut tries_remaining: i32 = -1;
|
||||||
@@ -505,7 +631,7 @@ impl YubiKey {
|
|||||||
|
|
||||||
// Attempt to set the "PUK blocked" flag in admin data.
|
// Attempt to set the "PUK blocked" flag in admin data.
|
||||||
let mut admin_data = AdminData::read(&txn)
|
let mut admin_data = AdminData::read(&txn)
|
||||||
.map(|data| {
|
.inspect(|data| {
|
||||||
if let Ok(item) = data.get_item(TAG_ADMIN_FLAGS_1) {
|
if let Ok(item) = data.get_item(TAG_ADMIN_FLAGS_1) {
|
||||||
if item.len() == flags.len() {
|
if item.len() == flags.len() {
|
||||||
flags.copy_from_slice(item)
|
flags.copy_from_slice(item)
|
||||||
@@ -517,8 +643,6 @@ impl YubiKey {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -538,7 +662,6 @@ impl YubiKey {
|
|||||||
/// Unblock a Personal Identification Number (PIN) using a previously
|
/// Unblock a Personal Identification Number (PIN) using a previously
|
||||||
/// configured PIN Unblocking Key (PUK).
|
/// configured PIN Unblocking Key (PUK).
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> {
|
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
|
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
|
||||||
@@ -546,7 +669,6 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Fetch an object from the YubiKey.
|
/// Fetch an object from the YubiKey.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer> {
|
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.fetch_object(object_id)
|
txn.fetch_object(object_id)
|
||||||
@@ -554,7 +676,6 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Save an object.
|
/// Save an object.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> {
|
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
txn.save_object(object_id, indata)
|
txn.save_object(object_id, indata)
|
||||||
@@ -562,25 +683,27 @@ impl YubiKey {
|
|||||||
|
|
||||||
/// Get an auth challenge.
|
/// Get an auth challenge.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> {
|
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> {
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
|
|
||||||
let response = Apdu::new(Ins::Authenticate)
|
let response = Apdu::new(Ins::Authenticate)
|
||||||
.params(ALGO_3DES, KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&[0x7c, 0x02, 0x81, 0x00])
|
.data([0x7c, 0x02, 0x81, 0x00])
|
||||||
.transmit(&txn, 261)?;
|
.transmit(&txn, 261)?;
|
||||||
|
|
||||||
if !response.is_success() {
|
if !response.is_success() {
|
||||||
return Err(Error::AuthenticationError);
|
return Err(Error::AuthenticationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(response.data()[4..12].try_into().unwrap())
|
Ok(response
|
||||||
|
.data()
|
||||||
|
.get(4..12)
|
||||||
|
.ok_or(Error::SizeError)?
|
||||||
|
.try_into()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify an auth response.
|
/// Verify an auth response.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> {
|
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> {
|
||||||
let mut data = [0u8; 12];
|
let mut data = [0u8; 12];
|
||||||
data[0] = 0x7c;
|
data[0] = 0x7c;
|
||||||
@@ -594,7 +717,7 @@ impl YubiKey {
|
|||||||
// send the response to the card and a challenge of our own.
|
// send the response to the card and a challenge of our own.
|
||||||
let status_words = Apdu::new(Ins::Authenticate)
|
let status_words = Apdu::new(Ins::Authenticate)
|
||||||
.params(ALGO_3DES, KEY_CARDMGM)
|
.params(ALGO_3DES, KEY_CARDMGM)
|
||||||
.data(&data)
|
.data(data)
|
||||||
.transmit(&txn, 261)?
|
.transmit(&txn, 261)?
|
||||||
.status_words();
|
.status_words();
|
||||||
|
|
||||||
@@ -611,7 +734,6 @@ impl YubiKey {
|
|||||||
///
|
///
|
||||||
/// The reset function is only available when both pins are blocked.
|
/// The reset function is only available when both pins are blocked.
|
||||||
#[cfg(feature = "untested")]
|
#[cfg(feature = "untested")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
|
|
||||||
pub fn reset_device(&mut self) -> Result<()> {
|
pub fn reset_device(&mut self) -> Result<()> {
|
||||||
let templ = [0, Ins::Reset.code(), 0, 0];
|
let templ = [0, Ins::Reset.code(), 0, 0];
|
||||||
let txn = self.begin_transaction()?;
|
let txn = self.begin_transaction()?;
|
||||||
@@ -629,22 +751,36 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(reader: &'a Reader<'_>) -> Result<Self> {
|
fn try_from(reader: &'a Reader<'_>) -> Result<Self> {
|
||||||
let mut card = reader.connect().map_err(|e| {
|
let mut card = reader
|
||||||
error!("error connecting to reader '{}': {}", reader.name(), e);
|
.connect()
|
||||||
e
|
.inspect_err(|e| error!("error connecting to reader '{}': {}", reader.name(), e))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
info!("connected to reader: {}", reader.name());
|
info!("connected to reader: {}", reader.name());
|
||||||
|
|
||||||
let (version, serial) = {
|
let mut app_version_serial = || -> Result<(Version, Serial)> {
|
||||||
let txn = Transaction::new(&mut card)?;
|
let txn = Transaction::new(&mut card)?;
|
||||||
txn.select_application()?;
|
txn.select_piv_application()?;
|
||||||
|
|
||||||
let v = txn.get_version()?;
|
let v = txn.get_version()?;
|
||||||
let s = txn.get_serial(v)?;
|
let s = txn.get_serial(v)?;
|
||||||
(v, s)
|
Ok((v, s))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
match app_version_serial() {
|
||||||
|
Err(e) => {
|
||||||
|
error!("Could not use reader: {}", e);
|
||||||
|
|
||||||
|
// We were unable to use the card, so we've effectively only connected as
|
||||||
|
// a side-effect of determining this. Avoid disrupting its internal state
|
||||||
|
// any further (e.g. preserve the PIN cache of whatever applet is selected
|
||||||
|
// currently).
|
||||||
|
if let Err((_, e)) = card.disconnect(pcsc::Disposition::LeaveCard) {
|
||||||
|
error!("Failed to disconnect gracefully from card: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
Ok((version, serial)) => {
|
||||||
let yubikey = YubiKey {
|
let yubikey = YubiKey {
|
||||||
card,
|
card,
|
||||||
name: String::from(reader.name()),
|
name: String::from(reader.name()),
|
||||||
@@ -655,4 +791,6 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
|
|||||||
|
|
||||||
Ok(yubikey)
|
Ok(yubikey)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-----
|
||||||
+254
-70
@@ -3,37 +3,38 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use cipher::common::{getrandom::SysRng, Generate};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use rand_core::{OsRng, RngCore};
|
use once_cell::sync::Lazy;
|
||||||
use rsa::{hash::Hash::SHA2_256, PaddingScheme, PublicKey};
|
use rsa::{pkcs1v15, RsaPublicKey};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{env, sync::Mutex};
|
use signature::hazmat::PrehashVerifier;
|
||||||
use x509::RelativeDistinguishedName;
|
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||||
|
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
|
||||||
use yubikey::{
|
use yubikey::{
|
||||||
certificate::{Certificate, PublicKeyInfo},
|
certificate::{yubikey_signer, Certificate},
|
||||||
piv::{self, AlgorithmId, Key, RetiredSlotId, SlotId},
|
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
|
||||||
Error, MgmKey, PinPolicy, TouchPolicy, YubiKey,
|
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
|
||||||
/// Provide thread-safe access to a YubiKey
|
|
||||||
static ref YUBIKEY: Mutex<YubiKey> = init_yubikey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One-time test initialization and setup
|
|
||||||
fn init_yubikey() -> Mutex<YubiKey> {
|
|
||||||
// Only show logs if `RUST_LOG` is set
|
// Only show logs if `RUST_LOG` is set
|
||||||
if env::var("RUST_LOG").is_ok() {
|
if env::var("RUST_LOG").is_ok() {
|
||||||
env_logger::builder().format_timestamp(None).init();
|
env_logger::builder().format_timestamp(None).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey = YubiKey::open().unwrap();
|
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!("serial: {}", yubikey.serial());
|
||||||
trace!("version: {}", yubikey.version());
|
trace!("version: {}", yubikey.version());
|
||||||
|
|
||||||
Mutex::new(yubikey)
|
Mutex::new(yubikey)
|
||||||
}
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// CCCID support
|
// CCCID support
|
||||||
@@ -42,12 +43,15 @@ fn init_yubikey() -> Mutex<YubiKey> {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_get_cccid() {
|
fn test_get_cccid() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
match yubikey.cccid() {
|
match yubikey.cccid() {
|
||||||
Ok(cccid) => trace!("CCCID: {:?}", cccid),
|
Ok(cccid) => trace!("CCCID: {:?}", cccid),
|
||||||
Err(Error::NotFound) => trace!("CCCID not found"),
|
Err(Error::NotFound) => trace!("CCCID not found"),
|
||||||
Err(err) => panic!("error getting CCCID: {:?}", err),
|
Err(err) => panic!("error getting CCCID: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +62,15 @@ fn test_get_cccid() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_get_chuid() {
|
fn test_get_chuid() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
match yubikey.chuid() {
|
match yubikey.chuid() {
|
||||||
Ok(chuid) => trace!("CHUID: {:?}", chuid),
|
Ok(chuid) => trace!("CHUID: {:?}", chuid),
|
||||||
Err(Error::NotFound) => trace!("CHUID not found"),
|
Err(Error::NotFound) => trace!("CHUID not found"),
|
||||||
Err(err) => panic!("error getting CHUID: {:?}", err),
|
Err(err) => panic!("error getting CHUID: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +81,10 @@ fn test_get_chuid() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_get_config() {
|
fn test_get_config() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
let config_result = yubikey.config();
|
let config_result = yubikey.config();
|
||||||
assert!(config_result.is_ok());
|
assert!(config_result.is_ok());
|
||||||
trace!("config: {:?}", config_result.unwrap());
|
trace!("config: {:?}", config_result.unwrap());
|
||||||
@@ -87,7 +97,10 @@ fn test_get_config() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_list_keys() {
|
fn test_list_keys() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
let mut yubikey = match YUBIKEY.lock() {
|
||||||
|
Ok(yubikey) => yubikey,
|
||||||
|
Err(poison) => poison.into_inner(),
|
||||||
|
};
|
||||||
let keys_result = Key::list(&mut yubikey);
|
let keys_result = Key::list(&mut yubikey);
|
||||||
assert!(keys_result.is_ok());
|
assert!(keys_result.is_ok());
|
||||||
trace!("keys: {:?}", keys_result.unwrap());
|
trace!("keys: {:?}", keys_result.unwrap());
|
||||||
@@ -100,7 +113,10 @@ fn test_list_keys() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_verify_pin() {
|
fn test_verify_pin() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
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"000000").is_err());
|
||||||
assert!(yubikey.verify_pin(b"123456").is_ok());
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
}
|
}
|
||||||
@@ -113,43 +129,52 @@ fn test_verify_pin() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_set_mgmkey() {
|
fn test_set_mgmkey() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
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!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
// Set a protected management key.
|
// Set a protected management key.
|
||||||
assert!(MgmKey::generate().set_protected(&mut yubikey).is_ok());
|
assert!(MgmKey::generate_for(&yubikey, &mut rng)
|
||||||
|
.unwrap()
|
||||||
|
.set_protected(&mut yubikey)
|
||||||
|
.is_ok());
|
||||||
let protected = MgmKey::get_protected(&mut yubikey).unwrap();
|
let protected = MgmKey::get_protected(&mut yubikey).unwrap();
|
||||||
assert!(yubikey.authenticate(MgmKey::default()).is_err());
|
assert!(yubikey.authenticate(&default_key).is_err());
|
||||||
assert!(yubikey.authenticate(protected.clone()).is_ok());
|
assert!(yubikey.authenticate(&protected).is_ok());
|
||||||
|
|
||||||
// Set a manual management key.
|
// Set a manual management key.
|
||||||
let manual = MgmKey::generate();
|
let manual = MgmKey::generate_for(&yubikey, &mut rng).unwrap();
|
||||||
assert!(manual.set_manual(&mut yubikey, false).is_ok());
|
assert!(manual.set_manual(&mut yubikey, false).is_ok());
|
||||||
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
assert!(yubikey.authenticate(MgmKey::default()).is_err());
|
assert!(yubikey.authenticate(&default_key).is_err());
|
||||||
assert!(yubikey.authenticate(protected.clone()).is_err());
|
assert!(yubikey.authenticate(&protected).is_err());
|
||||||
assert!(yubikey.authenticate(manual.clone()).is_ok());
|
assert!(yubikey.authenticate(&manual).is_ok());
|
||||||
|
|
||||||
// Set back to the default management key.
|
// Set back to the default management key.
|
||||||
assert!(MgmKey::set_default(&mut yubikey).is_ok());
|
assert!(MgmKey::set_default(&mut yubikey).is_ok());
|
||||||
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
assert!(MgmKey::get_protected(&mut yubikey).is_err());
|
||||||
assert!(yubikey.authenticate(protected).is_err());
|
assert!(yubikey.authenticate(&protected).is_err());
|
||||||
assert!(yubikey.authenticate(manual).is_err());
|
assert!(yubikey.authenticate(&manual).is_err());
|
||||||
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Certificate support
|
// Certificate support
|
||||||
//
|
//
|
||||||
|
|
||||||
fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
|
fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
assert!(yubikey.verify_pin(b"123456").is_ok());
|
assert!(yubikey.verify_pin(b"123456").is_ok());
|
||||||
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
let slot = SlotId::Retired(RetiredSlotId::R1);
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
@@ -157,25 +182,27 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
|
|||||||
let generated = piv::generate(
|
let generated = piv::generate(
|
||||||
&mut yubikey,
|
&mut yubikey,
|
||||||
slot,
|
slot,
|
||||||
algorithm,
|
KT::ALGORITHM,
|
||||||
PinPolicy::Default,
|
PinPolicy::Default,
|
||||||
TouchPolicy::Default,
|
TouchPolicy::Default,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut serial = [0u8; 20];
|
// 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
|
||||||
OsRng.fill_bytes(&mut serial);
|
// 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.
|
// Generate a self-signed certificate for the new key.
|
||||||
let extensions: &[x509::Extension<'_, &[u64]>] = &[];
|
let cert_result = Certificate::generate_self_signed::<_, KT>(
|
||||||
let cert_result = Certificate::generate_self_signed(
|
|
||||||
&mut yubikey,
|
&mut yubikey,
|
||||||
slot,
|
slot,
|
||||||
serial,
|
serial,
|
||||||
None,
|
validity,
|
||||||
&[RelativeDistinguishedName::common_name("testSubject")],
|
Name::from_str("CN=testSubject").expect("parse name"),
|
||||||
generated,
|
generated,
|
||||||
extensions,
|
|_builder| Ok(()),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(cert_result.is_ok());
|
assert!(cert_result.is_ok());
|
||||||
@@ -187,61 +214,100 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn generate_self_signed_rsa_cert() {
|
fn generate_self_signed_rsa_cert() {
|
||||||
let cert = generate_self_signed_cert(AlgorithmId::Rsa1024);
|
let cert = generate_self_signed_cert::<yubikey_signer::YubiRsa<yubikey_signer::Rsa1024>>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verify that the certificate is signed correctly
|
// Verify that the certificate is signed correctly
|
||||||
//
|
//
|
||||||
|
|
||||||
let pubkey = match cert.subject_pki() {
|
let pubkey = RsaPublicKey::try_from(cert.subject_pki()).expect("valid rsa key");
|
||||||
PublicKeyInfo::Rsa { pubkey, .. } => pubkey,
|
let pubkey = pkcs1v15::VerifyingKey::<Sha256>::new(pubkey);
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = cert.as_ref();
|
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 tbs_cert_len = u16::from_be_bytes(data[6..8].try_into().unwrap()) as usize;
|
||||||
let msg = &data[4..8 + tbs_cert_len];
|
let msg = &data[4..8 + tbs_cert_len];
|
||||||
let sig = &data[data.len() - 128..];
|
let sig = pkcs1v15::Signature::try_from(&data[data.len() - 128..]).unwrap();
|
||||||
|
|
||||||
let hash = Sha256::digest(msg);
|
let hash = Sha256::digest(msg);
|
||||||
|
|
||||||
assert!(pubkey
|
assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
|
||||||
.verify(
|
}
|
||||||
PaddingScheme::PKCS1v15Sign {
|
|
||||||
hash: Some(SHA2_256)
|
#[test]
|
||||||
},
|
#[ignore]
|
||||||
&hash,
|
fn generate_rsa3072() {
|
||||||
sig
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
)
|
let version = yubikey.version();
|
||||||
.is_ok());
|
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]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn generate_self_signed_ec_cert() {
|
fn generate_self_signed_ec_cert() {
|
||||||
let cert = generate_self_signed_cert(AlgorithmId::EccP256);
|
let cert = generate_self_signed_cert::<p256::NistP256>();
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verify that the certificate is signed correctly
|
// Verify that the certificate is signed correctly
|
||||||
//
|
//
|
||||||
|
|
||||||
let pubkey = match cert.subject_pki() {
|
let vk = p256::ecdsa::VerifyingKey::try_from(cert.subject_pki()).expect("ecdsa key expected");
|
||||||
PublicKeyInfo::EcP256(pubkey) => pubkey,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = cert.as_ref();
|
let data = cert.cert.to_der().expect("serialize certificate");
|
||||||
let tbs_cert_len = data[6] as usize;
|
let tbs_cert_len = data[6] as usize;
|
||||||
let sig_algo_len = data[7 + tbs_cert_len + 1] 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 sig_start = 7 + tbs_cert_len + 2 + sig_algo_len + 3;
|
||||||
let msg = &data[4..7 + tbs_cert_len];
|
let msg = &data[4..7 + tbs_cert_len];
|
||||||
let sig = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap();
|
let sig = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap();
|
||||||
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(pubkey.as_bytes()).unwrap();
|
|
||||||
|
|
||||||
use p256::ecdsa::signature::Verifier;
|
use p256::ecdsa::signature::Verifier;
|
||||||
assert!(vk.verify(msg, &sig).is_ok());
|
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]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_slot_id_display() {
|
fn test_slot_id_display() {
|
||||||
@@ -273,4 +339,122 @@ fn test_slot_id_display() {
|
|||||||
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R18)), "R18");
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R18)), "R18");
|
||||||
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R19)), "R19");
|
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R19)), "R19");
|
||||||
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R20)), "R20");
|
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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user