Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -2,4 +2,5 @@
|
|||||||
ignore = [
|
ignore = [
|
||||||
"RUSTSEC-2020-0071", # chrono
|
"RUSTSEC-2020-0071", # chrono
|
||||||
"RUSTSEC-2021-0145", # atty
|
"RUSTSEC-2021-0145", # atty
|
||||||
|
"RUSTSEC-2023-0071", # rsa: Marvin Attack: potential key recovery
|
||||||
] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
|
] # 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.65.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.65.0 # MSRV
|
toolchain: 1.85.0 # MSRV
|
||||||
deps: true
|
deps: true
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
toolchain: 1.65.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.71.0
|
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') }}
|
||||||
|
|||||||
@@ -4,6 +4,49 @@ 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)
|
## 0.8.0 (2023-08-15)
|
||||||
### Added
|
### Added
|
||||||
- `impl Debug for {Context, YubiKey}` ([#457])
|
- `impl Debug for {Context, YubiKey}` ([#457])
|
||||||
|
|||||||
Generated
+814
-399
File diff suppressed because it is too large
Load Diff
+30
-24
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey"
|
name = "yubikey"
|
||||||
version = "0.8.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"]
|
||||||
@@ -14,49 +14,55 @@ readme = "README.md"
|
|||||||
categories = ["api-bindings", "authentication", "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.65"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "cli"]
|
members = [".", "cli"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
x509-cert = { version = "0.2.3", features = [ "builder", "hazmat" ] }
|
sha2 = "0.11"
|
||||||
|
x509-cert = { version = "0.3.0-rc.4", features = ["builder", "hazmat"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
der = "0.7.1"
|
aes = { version = "0.9.0-rc.4", features = ["zeroize"] }
|
||||||
des = "0.8"
|
bitflags = "2.5.0"
|
||||||
elliptic-curve = "0.13"
|
cipher = { version = "0.5", features = ["getrandom", "rand_core"] }
|
||||||
|
curve25519-dalek = "5.0.0-pre.6"
|
||||||
|
der = "0.8"
|
||||||
|
des = "0.9.0-rc.3"
|
||||||
|
ecdsa = { version = "0.17.0-rc.16", features = ["digest", "pem"] }
|
||||||
|
ed25519-dalek = { version = "3.0.0-pre.6", features = ["alloc", "pkcs8"] }
|
||||||
|
elliptic-curve = "0.14.0-rc.29"
|
||||||
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
||||||
hmac = "0.12"
|
|
||||||
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"] }
|
||||||
ecdsa = { version = "0.16.7", features = ["digest", "pem"] }
|
|
||||||
p256 = "0.13"
|
|
||||||
p384 = "0.13"
|
|
||||||
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
|
|
||||||
pcsc = "2.3.1"
|
pcsc = "2.3.1"
|
||||||
rand_core = { version = "0.6", features = ["std"] }
|
rand = "0.10"
|
||||||
rsa = { version = "0.9.2", features = ["sha2"] }
|
rand_core = "0.10"
|
||||||
secrecy = "0.8"
|
rsa = { version = "0.10.0-rc.17", features = ["sha2"] }
|
||||||
sha1 = { version = "0.10", features = ["oid"] }
|
sha1 = { version = "0.11", features = ["oid"] }
|
||||||
sha2 = { version = "0.10", features = ["oid"] }
|
sha2 = { workspace = true, features = ["oid"] }
|
||||||
signature = "2"
|
signature = "3.0.0-rc.10"
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
uuid = { version = "1.2", features = ["v4"] }
|
uuid = { version = "1.2", features = ["v4"] }
|
||||||
|
x25519-dalek = "3.0.0-pre.6"
|
||||||
x509-cert.workspace = true
|
x509-cert.workspace = true
|
||||||
zeroize = "1"
|
zeroize = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
signature = "2"
|
|
||||||
|
|
||||||
[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"]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ access provided by the [`pcsc` crate].
|
|||||||
## 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.
|
||||||
|
|
||||||
@@ -54,15 +54,19 @@ See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
|
|||||||
on which devices support PIV and the available functionality.
|
on which devices support PIV and the available functionality.
|
||||||
|
|
||||||
### Supported Algorithms
|
### Supported Algorithms
|
||||||
|
|
||||||
- **Authentication**: `3DES`
|
- **Authentication**: `3DES`
|
||||||
- **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)
|
||||||
|
|
||||||
NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
|
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
|
||||||
|
|
||||||
@@ -73,8 +77,7 @@ Rust **1.60** or newer.
|
|||||||
- [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
|
||||||
|
|
||||||
@@ -102,8 +105,8 @@ We would appreciate any help testing this functionality and removing the
|
|||||||
|
|
||||||
## 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
|
||||||
@@ -172,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-2023 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
|
||||||
@@ -208,16 +211,16 @@ or conditions.
|
|||||||
|
|
||||||
[//]: # (badges)
|
[//]: # (badges)
|
||||||
|
|
||||||
[crate-image]: https://buildstats.info/crate/yubikey
|
[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.65+-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
|
||||||
@@ -231,10 +234,10 @@ or conditions.
|
|||||||
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC
|
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC
|
||||||
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
|
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
|
||||||
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
|
[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 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
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ 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)
|
## 0.7.0 (2022-11-14)
|
||||||
### Changed
|
### Changed
|
||||||
- Bump `clap` to v4.0 ([#438])
|
- Bump `clap` to v4.0 ([#438])
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "yubikey-cli"
|
name = "yubikey-cli"
|
||||||
version = "0.8.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 = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
sha2 = "0.10"
|
sha2.workspace = true
|
||||||
termcolor = "1"
|
termcolor = "1"
|
||||||
x509-cert.workspace = true
|
x509-cert.workspace = true
|
||||||
yubikey = { version = "0.8", path = ".." }
|
yubikey = { version = "=0.9.0-pre.0", path = ".." }
|
||||||
|
|||||||
+2
-4
@@ -25,8 +25,7 @@ Rust **1.60** or newer.
|
|||||||
- [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
|
||||||
|
|
||||||
@@ -84,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.60+-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/
|
||||||
@@ -98,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/
|
||||||
|
|||||||
+13
-6
@@ -188,22 +188,29 @@ pub fn print_cert_info(
|
|||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Algorithm",
|
"Algorithm",
|
||||||
cert.tbs_certificate.subject_public_key_info.algorithm.oid,
|
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",
|
||||||
&hex::upper::encode_string(&fingerprint),
|
hex::upper::encode_string(&fingerprint),
|
||||||
)?;
|
)?;
|
||||||
print_cert_attr(
|
print_cert_attr(
|
||||||
stream,
|
stream,
|
||||||
"Not Before",
|
"Not Before",
|
||||||
cert.tbs_certificate.validity.not_before,
|
cert.tbs_certificate().validity().not_before,
|
||||||
|
)?;
|
||||||
|
print_cert_attr(
|
||||||
|
stream,
|
||||||
|
"Not After",
|
||||||
|
cert.tbs_certificate().validity().not_after,
|
||||||
)?;
|
)?;
|
||||||
print_cert_attr(stream, "Not After", cert.tbs_certificate.validity.not_after)?;
|
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
+32
-1
@@ -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;
|
||||||
@@ -198,6 +204,15 @@ pub enum Ins {
|
|||||||
/// Get slot metadata
|
/// Get slot metadata
|
||||||
GetMetadata,
|
GetMetadata,
|
||||||
|
|
||||||
|
/// Management // Read Config
|
||||||
|
ReadConfig,
|
||||||
|
|
||||||
|
/// Management // Write Config
|
||||||
|
WriteConfig,
|
||||||
|
|
||||||
|
/// Management // DeviceReset
|
||||||
|
DeviceReset,
|
||||||
|
|
||||||
/// Other/unrecognized instruction codes
|
/// Other/unrecognized instruction codes
|
||||||
Other(u8),
|
Other(u8),
|
||||||
}
|
}
|
||||||
@@ -223,6 +238,12 @@ impl Ins {
|
|||||||
Ins::Attest => 0xf9,
|
Ins::Attest => 0xf9,
|
||||||
Ins::GetSerial => 0xf8,
|
Ins::GetSerial => 0xf8,
|
||||||
Ins::GetMetadata => 0xf7,
|
Ins::GetMetadata => 0xf7,
|
||||||
|
|
||||||
|
// Management
|
||||||
|
Ins::ReadConfig => 0x1d,
|
||||||
|
Ins::WriteConfig => 0x1c,
|
||||||
|
Ins::DeviceReset => 0x1f,
|
||||||
|
|
||||||
Ins::Other(code) => code,
|
Ins::Other(code) => code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,6 +252,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,
|
||||||
@@ -318,7 +344,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;
|
||||||
@@ -395,6 +421,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
|
||||||
//
|
//
|
||||||
@@ -428,6 +457,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,
|
||||||
@@ -462,6 +492,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,
|
||||||
|
|||||||
+8
-4
@@ -31,7 +31,8 @@
|
|||||||
// 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::{Result, YubiKey};
|
use crate::{Result, YubiKey};
|
||||||
use rand_core::{OsRng, RngCore};
|
use cipher::common::Generate;
|
||||||
|
use rand_core::CryptoRng;
|
||||||
use std::fmt::{self, Debug, Display};
|
use std::fmt::{self, Debug, Display};
|
||||||
|
|
||||||
/// CCCID offset
|
/// CCCID offset
|
||||||
@@ -66,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+80
-21
@@ -41,8 +41,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use x509_cert::{
|
use x509_cert::{
|
||||||
builder::{Builder, CertificateBuilder, Profile},
|
builder::{profile::BuilderProfile, Builder, CertificateBuilder},
|
||||||
der::{self, referenced::OwnedToRef, Decode, Encode},
|
der::{referenced::OwnedToRef, Decode, Encode},
|
||||||
name::Name,
|
name::Name,
|
||||||
serial_number::SerialNumber,
|
serial_number::SerialNumber,
|
||||||
spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
|
spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
|
||||||
@@ -109,23 +109,18 @@ impl Certificate {
|
|||||||
extensions: F,
|
extensions: F,
|
||||||
) -> Result<Self>
|
) -> Result<Self>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut CertificateBuilder<'_, yubikey_signer::Signer<'_, KT>>) -> der::Result<()>,
|
F: FnOnce(&mut CertificateBuilder<SelfSigned>) -> der::Result<()>,
|
||||||
{
|
{
|
||||||
let signer = yubikey_signer::Signer::new(yubikey, key, subject_pki.owned_to_ref())?;
|
let signer =
|
||||||
let mut builder = CertificateBuilder::new(
|
yubikey_signer::Signer::<'_, KT>::new(yubikey, key, subject_pki.owned_to_ref())?;
|
||||||
Profile::Manual { issuer: None },
|
let mut builder =
|
||||||
serial,
|
CertificateBuilder::new(SelfSigned { subject }, serial, validity, subject_pki)
|
||||||
validity,
|
|
||||||
subject,
|
|
||||||
subject_pki,
|
|
||||||
&signer,
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::KeyError)?;
|
.map_err(|_| Error::KeyError)?;
|
||||||
|
|
||||||
// Add custom extensions
|
// Add custom extensions
|
||||||
extensions(&mut builder)?;
|
extensions(&mut builder)?;
|
||||||
|
|
||||||
let cert = builder.build().map_err(|_| Error::KeyError)?;
|
let cert = builder.build(&signer).map_err(|_| Error::KeyError)?;
|
||||||
let cert = Self { cert };
|
let cert = Self { cert };
|
||||||
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
cert.write(yubikey, key, CertInfo::Uncompressed)?;
|
||||||
|
|
||||||
@@ -174,23 +169,55 @@ impl Certificate {
|
|||||||
|
|
||||||
/// Returns the Issuer field of the certificate.
|
/// Returns the Issuer field of the certificate.
|
||||||
pub fn issuer(&self) -> String {
|
pub fn issuer(&self) -> String {
|
||||||
self.cert.tbs_certificate.issuer.to_string()
|
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) -> String {
|
pub fn subject(&self) -> String {
|
||||||
self.cert.tbs_certificate.subject.to_string()
|
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) -> SubjectPublicKeyInfoRef<'_> {
|
pub fn subject_pki(&self) -> SubjectPublicKeyInfoRef<'_> {
|
||||||
self.cert
|
self.cert
|
||||||
.tbs_certificate
|
.tbs_certificate()
|
||||||
.subject_public_key_info
|
.subject_public_key_info()
|
||||||
.owned_to_ref()
|
.owned_to_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [`BuilderProfile`] for self-signed certificates.
|
||||||
|
///
|
||||||
|
/// This profile has no default extensions.
|
||||||
|
pub struct SelfSigned {
|
||||||
|
subject: Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuilderProfile for SelfSigned {
|
||||||
|
fn get_issuer(&self, subject: &Name) -> Name {
|
||||||
|
// RFC 5280 Section 3.2:
|
||||||
|
//
|
||||||
|
// > Self-issued certificates are CA certificates in which the issuer and subject
|
||||||
|
// > are the same entity. [..] Self-signed certificates are self-issued
|
||||||
|
// > certificates where the digital signature may be verified by the public key
|
||||||
|
// > bound into the certificate.
|
||||||
|
subject.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_subject(&self) -> Name {
|
||||||
|
self.subject.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_extensions(
|
||||||
|
&self,
|
||||||
|
_spk: SubjectPublicKeyInfoRef<'_>,
|
||||||
|
_issuer_spk: SubjectPublicKeyInfoRef<'_>,
|
||||||
|
_tbs: &x509_cert::TbsCertificate,
|
||||||
|
) -> x509_cert::builder::Result<Vec<x509_cert::ext::Extension>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read certificate
|
/// Read certificate
|
||||||
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer> {
|
pub(crate) fn read_certificate(txn: &Transaction<'_>, slot: SlotId) -> Result<Buffer> {
|
||||||
let object_id = slot.object_id();
|
let object_id = slot.object_id();
|
||||||
@@ -251,7 +278,7 @@ pub mod yubikey_signer {
|
|||||||
oid::db::rfc5912,
|
oid::db::rfc5912,
|
||||||
Encode, Sequence,
|
Encode, Sequence,
|
||||||
};
|
};
|
||||||
use sha2::{Digest, Sha256, Sha384};
|
use sha2::{Digest, Sha256, Sha384, Sha512};
|
||||||
use signature::Keypair;
|
use signature::Keypair;
|
||||||
use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
|
use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
|
||||||
use x509_cert::spki::{
|
use x509_cert::spki::{
|
||||||
@@ -286,6 +313,22 @@ pub mod yubikey_signer {
|
|||||||
fn read_signature(input: &[u8]) -> SigResult<Self::Signature>;
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeyType for ed25519_dalek::SigningKey {
|
||||||
|
const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
|
||||||
|
type Error = ed25519_dalek::SignatureError;
|
||||||
|
type Signature = ed25519_dalek::Signature;
|
||||||
|
type VerifyingKey = ed25519_dalek::VerifyingKey;
|
||||||
|
type PublicKey = ed25519_dalek::VerifyingKey;
|
||||||
|
|
||||||
|
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
|
||||||
|
Ok(Sha512::digest(input).to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
|
||||||
|
Self::Signature::try_from(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl KeyType for p256::NistP256 {
|
impl KeyType for p256::NistP256 {
|
||||||
const ALGORITHM: AlgorithmId = AlgorithmId::EccP256;
|
const ALGORITHM: AlgorithmId = AlgorithmId::EccP256;
|
||||||
type Error = ecdsa::Error;
|
type Error = ecdsa::Error;
|
||||||
@@ -342,6 +385,22 @@ pub mod yubikey_signer {
|
|||||||
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa2048;
|
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
|
/// RSA keys used to sign certificates
|
||||||
pub struct YubiRsa<N: RsaLength> {
|
pub struct YubiRsa<N: RsaLength> {
|
||||||
_len: PhantomData<N>,
|
_len: PhantomData<N>,
|
||||||
@@ -428,20 +487,20 @@ pub mod yubikey_signer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'y, KT: KeyType> Keypair for Signer<'y, KT> {
|
impl<KT: KeyType> Keypair for Signer<'_, KT> {
|
||||||
type VerifyingKey = KT::VerifyingKey;
|
type VerifyingKey = KT::VerifyingKey;
|
||||||
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
|
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
|
||||||
self.public_key.clone()
|
self.public_key.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'y, KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'y, KT> {
|
impl<KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'_, KT> {
|
||||||
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
|
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
|
||||||
self.verifying_key().signature_algorithm_identifier()
|
self.verifying_key().signature_algorithm_identifier()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'y, KT: KeyType> signature::Signer<KT::Signature> for Signer<'y, KT> {
|
impl<KT: KeyType> signature::Signer<KT::Signature> for Signer<'_, KT> {
|
||||||
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
|
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
|
||||||
let data = KT::prepare(msg)?;
|
let data = KT::prepare(msg)?;
|
||||||
|
|
||||||
|
|||||||
+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;
|
||||||
|
|||||||
+14
-4
@@ -57,6 +57,9 @@ pub enum Error {
|
|||||||
/// Authentication error
|
/// Authentication error
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
|
|
||||||
|
/// Error while building a certificate
|
||||||
|
CertificateBuilder,
|
||||||
|
|
||||||
/// Generic error
|
/// Generic error
|
||||||
GenericError,
|
GenericError,
|
||||||
|
|
||||||
@@ -132,10 +135,11 @@ impl Error {
|
|||||||
Error::AlgorithmError => f.write_str("algorithm error"),
|
Error::AlgorithmError => f.write_str("algorithm error"),
|
||||||
Error::AppletError => f.write_str("applet error"),
|
Error::AppletError => f.write_str("applet error"),
|
||||||
Error::AppletNotFound { applet_name } => {
|
Error::AppletNotFound { applet_name } => {
|
||||||
f.write_str(&format!("{} applet not found", applet_name))
|
f.write_str(&format!("{applet_name} applet not found"))
|
||||||
}
|
}
|
||||||
Error::ArgumentError => f.write_str("argument error"),
|
Error::ArgumentError => f.write_str("argument error"),
|
||||||
Error::AuthenticationError => f.write_str("authentication error"),
|
Error::AuthenticationError => f.write_str("authentication error"),
|
||||||
|
Error::CertificateBuilder => f.write_str("certificate builder error"),
|
||||||
Error::GenericError => f.write_str("generic error"),
|
Error::GenericError => f.write_str("generic error"),
|
||||||
Error::InvalidObject => f.write_str("invalid object"),
|
Error::InvalidObject => f.write_str("invalid object"),
|
||||||
Error::KeyError => f.write_str("key error"),
|
Error::KeyError => f.write_str("key error"),
|
||||||
@@ -146,7 +150,7 @@ impl Error {
|
|||||||
|
|
||||||
Error::PcscError {
|
Error::PcscError {
|
||||||
inner: Some(pcsc_error),
|
inner: Some(pcsc_error),
|
||||||
} => f.write_fmt(format_args!("PC/SC error: {}", pcsc_error)),
|
} => f.write_fmt(format_args!("PC/SC error: {pcsc_error}")),
|
||||||
|
|
||||||
Error::PcscError { .. } => f.write_str("PC/SC error"),
|
Error::PcscError { .. } => f.write_str("PC/SC error"),
|
||||||
|
|
||||||
@@ -192,8 +196,14 @@ impl std::error::Error for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<x509_cert::der::Error> for Error {
|
impl From<der::Error> for Error {
|
||||||
fn from(_err: x509_cert::der::Error) -> Error {
|
fn from(_err: der::Error) -> Error {
|
||||||
Error::ParseError
|
Error::ParseError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<x509_cert::builder::Error> for Error {
|
||||||
|
fn from(_err: x509_cert::builder::Error) -> Error {
|
||||||
|
Error::CertificateBuilder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -51,7 +51,7 @@ 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")]
|
||||||
@@ -71,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,
|
||||||
|
|||||||
+12
-15
@@ -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;
|
||||||
@@ -87,7 +86,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write metadata
|
/// Write metadata
|
||||||
#[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);
|
||||||
@@ -104,7 +102,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delete metadata
|
/// Delete metadata
|
||||||
#[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(), &[])
|
||||||
}
|
}
|
||||||
@@ -127,7 +124,6 @@ impl<T: MetadataType> Metadata<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set metadata item
|
/// Set metadata item
|
||||||
#[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;
|
||||||
@@ -157,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(());
|
||||||
@@ -193,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,
|
||||||
@@ -214,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
|
||||||
|
|||||||
+769
-165
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -96,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
|
||||||
|
|||||||
+183
-84
@@ -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:
|
||||||
@@ -45,35 +45,37 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
apdu::{Ins, StatusWords},
|
apdu::{Ins, StatusWords},
|
||||||
certificate::{self, Certificate},
|
certificate::{self, Certificate},
|
||||||
consts::CB_OBJ_MAX,
|
|
||||||
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, PublicKey};
|
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::{pkcs8::EncodePublicKey, 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::{der::Decode, spki::SubjectPublicKeyInfoOwned};
|
use x509_cert::{
|
||||||
|
der::{asn1::BitString, Decode},
|
||||||
#[cfg(feature = "untested")]
|
spki::{AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfoOwned},
|
||||||
use {
|
|
||||||
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
|
/// PIV Applet Name
|
||||||
pub(crate) const APPLET_NAME: &str = "PIV";
|
pub(crate) const APPLET_NAME: &str = "PIV";
|
||||||
|
|
||||||
@@ -87,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;
|
||||||
|
|
||||||
@@ -172,9 +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::Management(r) => write!(f, "{:?}", r),
|
SlotId::Management(r) => write!(f, "{r:?}"),
|
||||||
SlotId::Retired(r) => write!(f, "{:?}", r),
|
SlotId::Retired(r) => write!(f, "{r:?}"),
|
||||||
_ => write!(f, "{:?}", self),
|
_ => write!(f, "{self:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,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:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +415,7 @@ impl From<ManagementSlotId> for u8 {
|
|||||||
|
|
||||||
impl Display for ManagementSlotId {
|
impl Display for ManagementSlotId {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{self:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,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 {
|
||||||
@@ -496,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,16 +550,25 @@ impl AlgorithmId {
|
|||||||
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")]
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,7 +640,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))
|
||||||
@@ -770,36 +807,44 @@ impl RsaKeyData {
|
|||||||
/// - `Ok(key_data)` if `secret_p` and `secret_q` are valid primes.
|
/// - `Ok(key_data)` if `secret_p` and `secret_q` are valid primes.
|
||||||
/// - `Err(Error::AlgorithmError)` if `secret_p`/`secret_q` are invalid primes.
|
/// - `Err(Error::AlgorithmError)` if `secret_p`/`secret_q` are invalid primes.
|
||||||
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Result<Self> {
|
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Result<Self> {
|
||||||
let p = BigUint::from_bytes_be(secret_p);
|
let p = BoxedUint::from_be_slice_vartime(secret_p);
|
||||||
let q = BigUint::from_bytes_be(secret_q);
|
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()
|
||||||
p_t.lcm(&q_t)
|
.map_err(|_| Error::AlgorithmError)?;
|
||||||
};
|
|
||||||
|
|
||||||
let exp = BigUint::from_u64(KEYDATA_RSA_EXP).ok_or(Error::AlgorithmError)?;
|
|
||||||
|
|
||||||
let d = exp.mod_inverse(&totient).ok_or(Error::AlgorithmError)?;
|
|
||||||
let d = d.to_biguint().ok_or(Error::AlgorithmError)?;
|
|
||||||
|
|
||||||
// We calculate the optimization values ahead of time, instead of making the user
|
|
||||||
// do so.
|
|
||||||
|
|
||||||
let dp = &d % (&p - BigUint::one());
|
|
||||||
let dq = &d % (&q - BigUint::one());
|
|
||||||
|
|
||||||
let qinv = q.clone().mod_inverse(&p).ok_or(Error::AlgorithmError)?;
|
|
||||||
let (_, qinv) = qinv.to_bytes_be();
|
|
||||||
|
|
||||||
Ok(RsaKeyData {
|
Ok(RsaKeyData {
|
||||||
p: Zeroizing::new(p.to_bytes_be()),
|
p: Zeroizing::new(p.to_be_bytes().to_vec()),
|
||||||
q: Zeroizing::new(q.to_bytes_be()),
|
q: Zeroizing::new(q.to_be_bytes().to_vec()),
|
||||||
dp: Zeroizing::new(dp.to_bytes_be()),
|
dp: Zeroizing::new(
|
||||||
dq: Zeroizing::new(dq.to_bytes_be()),
|
private_key
|
||||||
qinv: Zeroizing::new(qinv),
|
.dp()
|
||||||
|
.expect("invariant violation: precompute should fill the field")
|
||||||
|
.clone()
|
||||||
|
.to_be_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
),
|
||||||
|
dq: Zeroizing::new(
|
||||||
|
private_key
|
||||||
|
.dq()
|
||||||
|
.expect("invariant violation: precompute should fill the field")
|
||||||
|
.clone()
|
||||||
|
.to_be_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
),
|
||||||
|
qinv: Zeroizing::new(
|
||||||
|
private_key
|
||||||
|
.qinv()
|
||||||
|
.expect("invariant violation: precompute should fill the field")
|
||||||
|
.clone()
|
||||||
|
.retrieve()
|
||||||
|
.to_be_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,7 +855,7 @@ 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")]
|
||||||
pub fn import_rsa_key(
|
pub fn import_rsa_key(
|
||||||
yubikey: &mut YubiKey,
|
yubikey: &mut YubiKey,
|
||||||
@@ -821,7 +866,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),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,6 +918,34 @@ 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>
|
||||||
@@ -924,28 +1000,15 @@ pub fn decrypt_data(
|
|||||||
/// Read metadata
|
/// Read metadata
|
||||||
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
|
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
|
||||||
let txn = yubikey.begin_transaction()?;
|
let txn = yubikey.begin_transaction()?;
|
||||||
let templ = [0, Ins::GetMetadata.code(), 0, slot.into()];
|
|
||||||
|
|
||||||
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
|
txn.get_metadata(slot)
|
||||||
|
|
||||||
if !response.is_success() {
|
|
||||||
if response.status_words() == StatusWords::NotSupportedError {
|
|
||||||
return Err(Error::NotSupported); // Requires firmware 5.2.3
|
|
||||||
} else {
|
|
||||||
return Err(Error::GenericError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let buf = Buffer::new(response.data().into());
|
|
||||||
|
|
||||||
SlotMetadata::try_from(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata from a slot
|
/// Metadata from a slot
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SlotMetadata {
|
pub struct SlotMetadata {
|
||||||
/// Algorithm / Type of key
|
/// Algorithm / Type of key
|
||||||
pub algorithm: ManagementAlgorithmId,
|
pub algorithm: SlotAlgorithmId,
|
||||||
/// PIN and touch policy
|
/// PIN and touch policy
|
||||||
pub policy: Option<(PinPolicy, TouchPolicy)>,
|
pub policy: Option<(PinPolicy, TouchPolicy)>,
|
||||||
/// Imported or generated key
|
/// Imported or generated key
|
||||||
@@ -966,13 +1029,14 @@ impl TryFrom<Buffer> for SlotMetadata {
|
|||||||
combinator::{eof, map_res},
|
combinator::{eof, map_res},
|
||||||
multi::fold_many1,
|
multi::fold_many1,
|
||||||
number::complete::u8,
|
number::complete::u8,
|
||||||
|
Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
let out = fold_many1(
|
let out = fold_many1(
|
||||||
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|
||||||
|| {
|
|| {
|
||||||
Ok(SlotMetadata {
|
Ok(SlotMetadata {
|
||||||
algorithm: ManagementAlgorithmId::PinPuk,
|
algorithm: SlotAlgorithmId::PinPuk,
|
||||||
policy: None,
|
policy: None,
|
||||||
origin: None,
|
origin: None,
|
||||||
public: None,
|
public: None,
|
||||||
@@ -983,15 +1047,15 @@ impl TryFrom<Buffer> for SlotMetadata {
|
|||||||
|acc: Result<SlotMetadata>, tlv| match acc {
|
|acc: Result<SlotMetadata>, tlv| match acc {
|
||||||
Ok(mut metadata) => match tlv.tag {
|
Ok(mut metadata) => match tlv.tag {
|
||||||
1 => {
|
1 => {
|
||||||
metadata.algorithm = ManagementAlgorithmId::try_from(tlv.value[0])?;
|
metadata.algorithm = SlotAlgorithmId::try_from(tlv.value[0])?;
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
fn policy_parser(
|
fn policy_parser(
|
||||||
i: &[u8],
|
i: &[u8],
|
||||||
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
|
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
|
||||||
let (i, pin) = map_res(u8, PinPolicy::try_from)(i)?;
|
let (i, pin) = map_res(u8, PinPolicy::try_from).parse(i)?;
|
||||||
let (i, touch) = map_res(u8, TouchPolicy::try_from)(i)?;
|
let (i, touch) = map_res(u8, TouchPolicy::try_from).parse(i)?;
|
||||||
let (i, _) = eof(i)?;
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
Ok((i, (pin, touch)))
|
Ok((i, (pin, touch)))
|
||||||
@@ -1003,7 +1067,7 @@ impl TryFrom<Buffer> for SlotMetadata {
|
|||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> {
|
fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> {
|
||||||
let (i, origin) = map_res(u8, Origin::try_from)(i)?;
|
let (i, origin) = map_res(u8, Origin::try_from).parse(i)?;
|
||||||
let (i, _) = eof(i)?;
|
let (i, _) = eof(i)?;
|
||||||
|
|
||||||
Ok((i, origin))
|
Ok((i, origin))
|
||||||
@@ -1015,7 +1079,7 @@ impl TryFrom<Buffer> for SlotMetadata {
|
|||||||
}
|
}
|
||||||
4 => {
|
4 => {
|
||||||
match metadata.algorithm {
|
match metadata.algorithm {
|
||||||
ManagementAlgorithmId::Asymmetric(alg) => {
|
SlotAlgorithmId::Asymmetric(alg) => {
|
||||||
metadata.public = Some(read_public_key(alg, tlv.value, false)?);
|
metadata.public = Some(read_public_key(alg, tlv.value, false)?);
|
||||||
}
|
}
|
||||||
_ => Err(Error::ParseError)?,
|
_ => Err(Error::ParseError)?,
|
||||||
@@ -1063,7 +1127,8 @@ impl TryFrom<Buffer> for SlotMetadata {
|
|||||||
},
|
},
|
||||||
err => err,
|
err => err,
|
||||||
},
|
},
|
||||||
)(buf.as_ref());
|
)
|
||||||
|
.parse(buf.as_ref());
|
||||||
|
|
||||||
match out {
|
match out {
|
||||||
Ok((_, res)) => res,
|
Ok((_, res)) => res,
|
||||||
@@ -1111,7 +1176,40 @@ fn read_public_key(
|
|||||||
//
|
//
|
||||||
// 0x7f 0x49 -> Application | Constructed | 0x49
|
// 0x7f 0x49 -> Application | Constructed | 0x49
|
||||||
match algorithm {
|
match algorithm {
|
||||||
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
|
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
|
// It appears that the inner application-specific value returned by the
|
||||||
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
|
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
|
||||||
//
|
//
|
||||||
@@ -1156,8 +1254,8 @@ fn read_public_key(
|
|||||||
let exp = exp_tlv.value.to_vec();
|
let exp = exp_tlv.value.to_vec();
|
||||||
|
|
||||||
let pubkey = RsaPublicKey::new(
|
let pubkey = RsaPublicKey::new(
|
||||||
BigUint::from_bytes_be(&modulus),
|
BoxedUint::from_be_slice_vartime(&modulus),
|
||||||
BigUint::from_bytes_be(&exp),
|
BoxedUint::from_be_slice_vartime(&exp),
|
||||||
)
|
)
|
||||||
.map_err(|_| Error::InvalidObject)?;
|
.map_err(|_| Error::InvalidObject)?;
|
||||||
Ok(SubjectPublicKeyInfoOwned::from_der(
|
Ok(SubjectPublicKeyInfoOwned::from_der(
|
||||||
@@ -1215,33 +1313,34 @@ fn read_public_key(
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
/// Algorithms as reported by the metadata command.
|
/// Algorithms as reported by the metadata command.
|
||||||
pub enum ManagementAlgorithmId {
|
pub enum SlotAlgorithmId {
|
||||||
/// Used on PIN and PUK slots.
|
/// Used on PIN and PUK slots.
|
||||||
PinPuk,
|
PinPuk,
|
||||||
/// Used on the key management slot.
|
/// Used on the key management slot.
|
||||||
ThreeDes,
|
Management(MgmAlgorithmId),
|
||||||
/// Used on all other slots.
|
/// Used on all other slots.
|
||||||
Asymmetric(AlgorithmId),
|
Asymmetric(AlgorithmId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for ManagementAlgorithmId {
|
impl TryFrom<u8> for SlotAlgorithmId {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: u8) -> Result<Self> {
|
fn try_from(value: u8) -> Result<Self> {
|
||||||
match value {
|
match value {
|
||||||
0xff => Ok(ManagementAlgorithmId::PinPuk),
|
0xff => Ok(SlotAlgorithmId::PinPuk),
|
||||||
0x03 => Ok(ManagementAlgorithmId::ThreeDes),
|
oth => MgmAlgorithmId::try_from(oth)
|
||||||
oth => AlgorithmId::try_from(oth).map(ManagementAlgorithmId::Asymmetric),
|
.map(SlotAlgorithmId::Management)
|
||||||
|
.or_else(|_| AlgorithmId::try_from(oth).map(SlotAlgorithmId::Asymmetric)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ManagementAlgorithmId> for u8 {
|
impl From<SlotAlgorithmId> for u8 {
|
||||||
fn from(id: ManagementAlgorithmId) -> u8 {
|
fn from(id: SlotAlgorithmId) -> u8 {
|
||||||
match id {
|
match id {
|
||||||
ManagementAlgorithmId::PinPuk => 0xff,
|
SlotAlgorithmId::PinPuk => 0xff,
|
||||||
ManagementAlgorithmId::ThreeDes => 0x03,
|
SlotAlgorithmId::Management(oth) => oth.into(),
|
||||||
ManagementAlgorithmId::Asymmetric(oth) => oth.into(),
|
SlotAlgorithmId::Asymmetric(oth) => oth.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+152
-65
@@ -5,6 +5,7 @@ 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},
|
||||||
|
mgm::MgmKey,
|
||||||
otp,
|
otp,
|
||||||
piv::{self, AlgorithmId, SlotId},
|
piv::{self, AlgorithmId, SlotId},
|
||||||
serialization::*,
|
serialization::*,
|
||||||
@@ -15,7 +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};
|
||||||
|
|
||||||
const CB_PIN_MAX: usize = 8;
|
const CB_PIN_MAX: usize = 8;
|
||||||
|
|
||||||
@@ -60,26 +61,32 @@ 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::APPLET_ID)
|
.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}",
|
|
||||||
response.status_words().code()
|
|
||||||
);
|
|
||||||
return Err(match response.status_words() {
|
return Err(match response.status_words() {
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
|
||||||
applet_name: piv::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
_ => Error::GenericError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,7 +99,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,21 +111,11 @@ impl<'tx> Transaction<'tx> {
|
|||||||
match version.major {
|
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
|
||||||
4 => {
|
4 => {
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
self.select_application(
|
||||||
.p1(0x04)
|
otp::APPLET_ID,
|
||||||
.data(otp::APPLET_ID)
|
otp::APPLET_NAME,
|
||||||
.transmit(self, 0xFF)?
|
"failed selecting yk application",
|
||||||
.status_words();
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
|
||||||
error!("failed selecting yk application: {:04x}", sw.code());
|
|
||||||
return Err(match sw {
|
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
|
||||||
applet_name: otp::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
|
||||||
|
|
||||||
@@ -132,21 +129,11 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reselect the PIV applet
|
// reselect the PIV applet
|
||||||
let sw = Apdu::new(Ins::SelectApplication)
|
self.select_application(
|
||||||
.p1(0x04)
|
piv::APPLET_ID,
|
||||||
.data(piv::APPLET_ID)
|
piv::APPLET_NAME,
|
||||||
.transmit(self, 0xFF)?
|
"failed selecting application",
|
||||||
.status_words();
|
)?;
|
||||||
|
|
||||||
if !sw.is_success() {
|
|
||||||
error!("failed selecting application: {:04x}", sw.code());
|
|
||||||
return Err(match sw {
|
|
||||||
StatusWords::NotFoundError => Error::AppletNotFound {
|
|
||||||
applet_name: piv::APPLET_NAME,
|
|
||||||
},
|
|
||||||
_ => Error::GenericError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
response.data().try_into()
|
response.data().try_into()
|
||||||
}
|
}
|
||||||
@@ -171,6 +158,24 @@ impl<'tx> Transaction<'tx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
pub fn verify_pin(&self, pin: &[u8]) -> Result<()> {
|
pub fn verify_pin(&self, pin: &[u8]) -> Result<()> {
|
||||||
if pin.len() > CB_PIN_MAX {
|
if pin.len() > CB_PIN_MAX {
|
||||||
@@ -186,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)?;
|
||||||
@@ -243,15 +248,14 @@ 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)
|
||||||
@@ -284,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,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 {
|
||||||
@@ -323,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
|
||||||
@@ -335,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());
|
||||||
@@ -511,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-55
@@ -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, Disposition};
|
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,
|
||||||
};
|
};
|
||||||
@@ -59,7 +61,6 @@ use {
|
|||||||
transaction::ChangeRefAction,
|
transaction::ChangeRefAction,
|
||||||
Buffer, ObjectId,
|
Buffer, ObjectId,
|
||||||
},
|
},
|
||||||
secrecy::ExposeSecret,
|
|
||||||
std::time::{SystemTime, UNIX_EPOCH},
|
std::time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ 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
|
||||||
@@ -75,10 +77,11 @@ pub(crate) const KEY_CARDMGM: u8 = 0x9b;
|
|||||||
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 {
|
||||||
@@ -143,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 {
|
||||||
@@ -151,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
|
||||||
@@ -269,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)?;
|
||||||
@@ -293,7 +342,10 @@ impl YubiKey {
|
|||||||
/// `YubiKey` implements `Drop` which automatically disconnects the card using
|
/// `YubiKey` implements `Drop` which automatically disconnects the card using
|
||||||
/// `Disposition::ResetCard`; you only need to call this function if you want to
|
/// `Disposition::ResetCard`; you only need to call this function if you want to
|
||||||
/// handle errors or use a different disposition method.
|
/// handle errors or use a different disposition method.
|
||||||
pub fn disconnect(self, disposition: Disposition) -> core::result::Result<(), (Self, Error)> {
|
pub fn disconnect(
|
||||||
|
self,
|
||||||
|
disposition: pcsc::Disposition,
|
||||||
|
) -> core::result::Result<(), (Self, Error)> {
|
||||||
let Self {
|
let Self {
|
||||||
card,
|
card,
|
||||||
name,
|
name,
|
||||||
@@ -357,37 +409,46 @@ 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()?);
|
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)?;
|
||||||
|
|
||||||
@@ -396,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.
|
||||||
@@ -446,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(())
|
||||||
@@ -459,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(&[]) {
|
||||||
@@ -502,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(())
|
||||||
@@ -523,15 +577,11 @@ impl YubiKey {
|
|||||||
|
|
||||||
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(())
|
||||||
}
|
}
|
||||||
@@ -581,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)
|
||||||
@@ -593,8 +643,6 @@ impl YubiKey {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -703,16 +751,15 @@ 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 mut app_version_serial = || -> Result<(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)?;
|
||||||
|
|||||||
+155
-30
@@ -3,18 +3,16 @@
|
|||||||
#![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 cipher::common::{getrandom::SysRng, Generate};
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rand_core::{OsRng, RngCore};
|
|
||||||
use rsa::{pkcs1v15, RsaPublicKey};
|
use rsa::{pkcs1v15, RsaPublicKey};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use signature::hazmat::PrehashVerifier;
|
use signature::hazmat::PrehashVerifier;
|
||||||
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
use std::{env, str::FromStr, sync::Mutex, time::Duration};
|
||||||
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
|
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
|
||||||
use yubikey::{
|
use yubikey::{
|
||||||
certificate,
|
certificate::{yubikey_signer, Certificate},
|
||||||
certificate::yubikey_signer,
|
|
||||||
certificate::Certificate,
|
|
||||||
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
|
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
|
||||||
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
|
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
|
||||||
};
|
};
|
||||||
@@ -45,12 +43,15 @@ static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
|
|||||||
#[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:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,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:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,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());
|
||||||
@@ -90,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());
|
||||||
@@ -103,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());
|
||||||
}
|
}
|
||||||
@@ -116,32 +129,40 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -150,9 +171,10 @@ fn test_set_mgmkey() {
|
|||||||
|
|
||||||
fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> 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);
|
||||||
|
|
||||||
@@ -168,8 +190,7 @@ fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
|
|||||||
|
|
||||||
// 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
|
// 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
|
||||||
// we'll limit ourselves to 19 bytes serial.
|
// we'll limit ourselves to 19 bytes serial.
|
||||||
let mut serial = [0u8; 19];
|
let serial = <[u8; 19]>::generate();
|
||||||
OsRng.fill_bytes(&mut serial);
|
|
||||||
let serial = SerialNumber::new(&serial[..]).expect("serial can't be more than 20 bytes long");
|
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();
|
let validity = Validity::from_now(Duration::new(500000, 0)).unwrap();
|
||||||
|
|
||||||
@@ -211,6 +232,35 @@ fn generate_self_signed_rsa_cert() {
|
|||||||
assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
|
assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn generate_rsa3072() {
|
||||||
|
let mut yubikey = YUBIKEY.lock().unwrap();
|
||||||
|
let version = yubikey.version();
|
||||||
|
let default_key = MgmKey::get_default(&yubikey).unwrap();
|
||||||
|
|
||||||
|
assert!(yubikey.authenticate(&default_key).is_ok());
|
||||||
|
|
||||||
|
let slot = SlotId::Retired(RetiredSlotId::R1);
|
||||||
|
|
||||||
|
// Generate a new key in the selected slot.
|
||||||
|
let generated = piv::generate(
|
||||||
|
&mut yubikey,
|
||||||
|
slot,
|
||||||
|
AlgorithmId::Rsa3072,
|
||||||
|
PinPolicy::Default,
|
||||||
|
TouchPolicy::Default,
|
||||||
|
);
|
||||||
|
|
||||||
|
match generated {
|
||||||
|
Ok(key) => {
|
||||||
|
let pubkey = key.subject_public_key;
|
||||||
|
assert!(pubkey.bit_len() > 3072)
|
||||||
|
}
|
||||||
|
Err(e) => assert!((version.major, version.minor) < (5, 7) && e == Error::AlgorithmError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn generate_self_signed_ec_cert() {
|
fn generate_self_signed_ec_cert() {
|
||||||
@@ -233,6 +283,31 @@ fn generate_self_signed_ec_cert() {
|
|||||||
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() {
|
||||||
@@ -286,10 +361,14 @@ fn test_slot_id_display() {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_read_metadata() {
|
fn test_read_metadata() {
|
||||||
let mut yubikey = YUBIKEY.lock().unwrap();
|
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!(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);
|
||||||
|
|
||||||
@@ -313,12 +392,58 @@ fn test_read_metadata() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn test_parse_cert_from_der() {
|
fn test_parse_cert_from_der() {
|
||||||
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
|
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
|
||||||
let cert =
|
let cert = Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
|
||||||
certificate::Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cert.subject(),
|
cert.subject(),
|
||||||
"CN=Bob",
|
"CN=Bob",
|
||||||
|
|||||||
Reference in New Issue
Block a user