Compare commits

..

63 Commits

Author SHA1 Message Date
Tony Arcieri (iqlusion) 403632df76 yubikey v0.9.0-pre.0 (#666)
This prerelease includes crates from the new RustCrypto release series
including prereleases for crates which haven't yet received a new stable
release.

It also includes preliminary Curve25519/Ed25519 support.
2026-04-02 13:59:53 -06:00
Tony Arcieri (iqlusion) 95babac2d4 Upgrade dependencies; bump version to v0.9.0-pre (#665)
Note: this is not a release, but bumping the version to reflect breaking
changes that have not yet been released.

The following dependencies have been upgraded to new stable releases:
- `cipher` v0.5
- `der` v0.8
- `sha1` v0.11
- `sha2` v0.11
- `rand(_core)` v0.10

The following dependencies are prereleases which have been upgraded from
older prerelease versions:
- `aes` v0.9.0-rc.4
- `curve25519-dalek` 5.0.0-pre.6
- `des` v0.9.0-rc.3
- `ecdsa` v0.17.0-rc.16
- `ed25519-dalek` v3.0.0-pre.6
- `elliptic-curve` v0.14.0-rc.29
- `p256` v0.14.0-rc.8
- `p384` v0.14.0-rc.8
- `pbkdf2` v0.13.0-rc.10
- `rsa` v0.10.0-rc.17
- `signature` v3.0.0-rc.10
- `x25519-dalek` v3.0.0-pre.6
2026-04-02 13:35:37 -06:00
Andrew Lubawy 872ba35f54 Add Curve25519 support (#577)
Supported in PIV applet since firmware 5.7.X
2026-02-18 17:27:43 -07:00
Tony Arcieri (iqlusion) c96b50bcec mgm: extract MgmAlgorithmId::default_for_version (#633)
Adds a private method for determining the MGM key algorithm to use for a
given YubiKey `Version`
2025-12-15 19:59:48 +01:00
dependabot[bot] abcded88cf Bump actions/checkout from 5 to 6 (#648) 2025-12-15 19:59:04 +01:00
dependabot[bot] ff6d5ee56e Bump actions/cache from 4 to 5 (#651) 2025-12-15 18:08:36 +01:00
Robin Lambertz a039431fc9 Implement Hash on Serial (#647) 2025-11-06 12:59:10 -07:00
Arthur Gautier ec78d6b2f7 chore(deps): bump der/pkcs/x509-cert to latest pre-releases (#641) 2025-09-29 14:51:10 -06:00
Tony Arcieri (iqlusion) 49fc8796ab README.md: fix badges and links (#636) 2025-09-15 14:36:07 -06:00
Arthur Gautier efc587c88d bump dependencies to latest pre-releases (#637) 2025-09-11 12:16:04 -06:00
Carl Wallace 9e75924908 Change MgmKey::algorithm_id from pub(crate) to pub (#635) 2025-09-10 10:27:45 -06:00
Jack Grigg dcaf080ef2 mgm: Support AES management keys (#589) 2025-09-01 16:30:40 -06:00
dependabot[bot] 0072b174b4 Bump actions/checkout from 1 to 5 (#628) 2025-08-22 10:34:27 -06:00
dependabot[bot] 74968cbef0 Bump actions/cache from 1 to 4 (#627) 2025-08-22 10:34:14 -06:00
Tony Arcieri (iqlusion) 80968606b2 dependabot: perform consolidated lockfile updates (#626)
Update all dependencies in Cargo.lock, rather than opening individual
PRs for each of them
2025-08-22 09:48:31 -06:00
Tony Arcieri (iqlusion) 1e1fe34734 mgm: Generalize TDES logic to enable other algorithms (#625)
Co-authored-by: Jack Grigg <thestr4d@gmail.com>
Co-authored-by: Greg Bowyer <gbowyer@fastmail.co.uk>
2025-08-22 09:37:41 -06:00
Tony Arcieri (iqlusion) 7eb7a31a28 mgm: remove untested gating from tested methods (#623)
Removes the `#[cfg(feature = "untested")]` gating from all methods
tested in `tests/integration.rs` and their dependent codepaths.
2025-08-18 13:05:23 -06:00
Nazar Serhiichuk 1fc807fdcb Handle reference data not found in metadata command (#558) 2025-08-14 07:37:57 -06:00
Tony Arcieri (iqlusion) b4be1bb216 mgm: use TdesEde3::weak_key_test (#621)
Replaces the vendored weak key test with the upstream one from the `des`
crate which was added in RustCrypto/block-ciphers#465
2025-08-13 15:36:53 -06:00
Arthur Gautier 7f2b423713 chore(deps): bump nom from 7.1.3 to 8.0.0 (#614) 2025-07-21 20:20:45 -06:00
Arthur Gautier f0dbf9425c bump dependencies to latest rc, bump MSRV to 1.85 (#612) 2025-07-21 15:49:31 -06:00
Joost van Dijk 0d8096f50d add support for rsa3072 and rsa4096 (#598) 2025-02-12 14:48:29 -07:00
Arthur Gautier 13bdf9a585 Applets management (#568) 2025-02-11 19:13:01 -07:00
Jack Grigg 235eb6215e Clean up some of the management key code (#584)
* mgm: Move TDES weak key checking code into a submodule
* piv: Extract management key algorithm into a separate enum
* mgm: Check management key algorithm when fetching from Yubikey
2025-02-11 12:19:53 -07:00
dependabot[bot] 19e1cccfec Bump env_logger from 0.10.2 to 0.11.6 (#593) 2025-01-06 07:35:12 -07:00
dependabot[bot] 1af3cbbf91 Bump clap from 4.5.4 to 4.5.23 (#587) 2025-01-03 11:11:27 -07:00
dependabot[bot] 5955001e00 Bump anstream from 0.6.7 to 0.6.18 (#586) 2025-01-02 20:06:18 -07:00
Tony Arcieri (iqlusion) d204051912 clippy fixes (#585)
Runs `cargo clippy --fix` against both crates in the repo
(including `cli`)
2025-01-02 12:55:36 -07:00
Jack Grigg 626ac3bffd Migrate to current pre-release revisions of dependencies (#583)
The CHANGELOG lists the specific versions currently pinned; it will
be modified to instead reference the public releases once they exist
and this crate uses them.
2025-01-02 12:39:52 -07:00
Jack Grigg 32cd92af50 Bump MSRV to 1.81 (#582)
This is required due to the `hybrid-array` crate, which has become a
transitive dependency of the majority of our dependencies and will be
required in the very near future.
2024-11-25 11:58:24 -07:00
dependabot[bot] 0a90dc3ca8 Bump clap from 4.4.18 to 4.5.4 (#564)
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.18 to 4.5.4.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.18...v4.5.4)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 14:53:26 -06:00
Shella Stephens 69b5404370 Fix clippy (#566) 2024-04-04 14:40:51 -06:00
Shella Stephens 2db3ea55c4 MSRV 1.74 (#565) 2024-04-04 11:50:25 -06:00
dependabot[bot] b07612eb4e Bump x509-cert from 0.2.4 to 0.2.5 (#553)
Bumps [x509-cert](https://github.com/RustCrypto/formats) from 0.2.4 to 0.2.5.
- [Commits](https://github.com/RustCrypto/formats/compare/x509-cert/v0.2.4...x509-cert/v0.2.5)

---
updated-dependencies:
- dependency-name: x509-cert
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 15:35:58 -07:00
dependabot[bot] 01eb42bc60 Bump rsa from 0.9.5 to 0.9.6 (#551)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.5 to 0.9.6.
- [Changelog](https://github.com/RustCrypto/RSA/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.5...v0.9.6)

---
updated-dependencies:
- dependency-name: rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 09:23:49 -07:00
dependabot[bot] 82cb78aa95 Bump env_logger from 0.10.1 to 0.10.2 (#552)
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.10.1 to 0.10.2.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.10.1...v0.10.2)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 15:29:55 -07:00
dependabot[bot] 1c9f71a989 Bump clap from 4.4.16 to 4.4.18 (#550)
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.16 to 4.4.18.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.16...v4.4.18)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 15:28:24 -07:00
dependabot[bot] 853677b2d8 Bump once_cell from 1.18.0 to 1.19.0 (#542)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.18.0 to 1.19.0.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 08:36:10 -07:00
dependabot[bot] 6189de288b Bump rsa from 0.9.4 to 0.9.5 (#539)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.4 to 0.9.5.
- [Changelog](https://github.com/RustCrypto/RSA/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.4...v0.9.5)

---
updated-dependencies:
- dependency-name: rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 08:33:58 -07:00
dependabot[bot] 1f0d42218e Bump clap from 4.4.8 to 4.4.16 (#548)
Bumps [clap](https://github.com/clap-rs/clap) from 4.4.8 to 4.4.16.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.4.8...v4.4.16)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 08:29:37 -07:00
dependabot[bot] 164faac609 Bump termcolor from 1.4.0 to 1.4.1 (#549)
Bumps [termcolor](https://github.com/BurntSushi/termcolor) from 1.4.0 to 1.4.1.
- [Commits](https://github.com/BurntSushi/termcolor/compare/1.4.0...1.4.1)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 08:28:49 -07:00
Carl Wallace c0f3a2f841 add length check to get_version (#545) 2023-12-20 07:42:27 -07:00
Shella Stephens 3e31fe8663 .cargo/audit.toml: ignore RUSTSEC-2023-0071 (#541) 2023-12-06 08:49:24 -07:00
Shella Stephens 385db11522 Cargo.lock: Update dependencies (#538)
* Cargo.lock: Update dependencies

* MSRV 1.70.0
2023-11-20 20:10:40 -07:00
dependabot[bot] c1dc4a4319 Bump termcolor from 1.2.0 to 1.3.0 (#525)
Bumps [termcolor](https://github.com/BurntSushi/termcolor) from 1.2.0 to 1.3.0.
- [Commits](https://github.com/BurntSushi/termcolor/compare/1.2.0...1.3.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 09:34:15 -06:00
dependabot[bot] 3d78874a3b Bump sha2 from 0.10.7 to 0.10.8 (#527)
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.7 to 0.10.8.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.7...sha2-v0.10.8)

---
updated-dependencies:
- dependency-name: sha2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 09:34:00 -06:00
dependabot[bot] f04b8592ec Bump sha1 from 0.10.5 to 0.10.6 (#523)
Bumps [sha1](https://github.com/RustCrypto/hashes) from 0.10.5 to 0.10.6.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha1-v0.10.5...sha1-v0.10.6)

---
updated-dependencies:
- dependency-name: sha1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 09:33:48 -06:00
dependabot[bot] 0c57c06294 Bump elliptic-curve from 0.13.5 to 0.13.6 (#529)
Bumps [elliptic-curve](https://github.com/RustCrypto/traits) from 0.13.5 to 0.13.6.
- [Commits](https://github.com/RustCrypto/traits/compare/elliptic-curve/v0.13.5...elliptic-curve-v0.13.6)

---
updated-dependencies:
- dependency-name: elliptic-curve
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 08:09:18 -06:00
dependabot[bot] 885528a3d6 Bump rsa from 0.9.2 to 0.9.3 (#530)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.2 to 0.9.3.
- [Changelog](https://github.com/RustCrypto/RSA/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.2...v0.9.3)

---
updated-dependencies:
- dependency-name: rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 08:08:54 -06:00
Tony Arcieri (iqlusion) ee3702a65e yubikey v0.8.0 (#518) 2023-08-15 19:29:22 -06:00
Tony Arcieri (iqlusion) 45915e5e5a Make RsaKeyData::new fallible (#517)
Replaces unwraps with `Error::AlgorithmError`
2023-08-15 18:33:30 -06:00
Tony Arcieri (iqlusion) 75ce24a3ea Handle metadata command not being supported in test (#516)
Some YubiKeys don't support this command. Instead of failing the test
when it happens, log a warning message instead.
2023-08-15 18:21:45 -06:00
Tony Arcieri (iqlusion) 78313360a1 Add clippy::unwrap_used lint (#515)
Lints for usages of `unwrap()` in the `yubikey` crate (not CLI yet).

Replaces them with `?` or `expect()` as the situation warrants.
2023-08-15 18:02:25 -06:00
Tony Arcieri (iqlusion) d226209ea4 Use doc_auto_cfg (#514)
Removes manual feature annotations for docs.rs
2023-08-15 16:39:29 -06:00
Tony Arcieri (iqlusion) de142256d0 Bump clippy to use Rust 1.71 (#513) 2023-08-15 16:31:13 -06:00
hko-s 485d49a6c8 Make YubiKey::open() more robust (#504)
On systems with a physical card-reader, the previous implementation falsely
reports "multiple YubiKeys detected!", even if only one YubiKey is connected.
This change attempts to actually open each reader as a YubiKey, and only
reports "multiple YubiKeys" if it can actually open more than one.

Additionally, this change avoids resetting the YubiKeys in case we find more
than one.
2023-08-15 16:20:04 -06:00
Tony Arcieri (iqlusion) 9932d05428 Remove chrono dependency (#512)
It's no longer used as of #495
2023-08-15 16:18:08 -06:00
Tony Arcieri (iqlusion) 363648bbc5 Cargo.lock: bump dependencies (#511)
Upgrades the following dependencies:

    $ cargo update
    Updating crates.io index
    Updating aho-corasick v0.7.20 -> v1.0.4
      Adding android-tzdata v0.1.1
      Adding anstream v0.3.2
      Adding anstyle v1.0.1
      Adding anstyle-parse v0.2.1
      Adding anstyle-query v1.0.0
      Adding anstyle-wincon v1.0.2
    Updating base64ct v1.5.3 -> v1.6.0
      Adding bitflags v2.4.0
    Updating block-buffer v0.10.3 -> v0.10.4
    Updating bumpalo v3.11.1 -> v3.13.0
    Updating cc v1.0.78 -> v1.0.82
    Updating chrono v0.4.23 -> v0.4.26
    Updating cipher v0.4.3 -> v0.4.4
    Updating clap v4.0.32 -> v4.3.21
      Adding clap_builder v4.3.21
    Updating clap_derive v4.0.21 -> v4.3.12
    Updating clap_lex v0.3.0 -> v0.5.0
    Removing codespan-reporting v0.11.1
      Adding colorchoice v1.0.0
    Updating const-oid v0.9.2 -> v0.9.5
    Updating core-foundation-sys v0.8.3 -> v0.8.4
    Updating cpufeatures v0.2.5 -> v0.2.9
    Updating crypto-bigint v0.5.1 -> v0.5.2
    Removing cxx v1.0.85
    Removing cxx-build v1.0.85
    Removing cxxbridge-flags v1.0.85
    Removing cxxbridge-macro v1.0.85
    Updating der v0.7.6 -> v0.7.8
    Updating der_derive v0.7.1 -> v0.7.2
    Updating digest v0.10.6 -> v0.10.7
    Updating ecdsa v0.16.7 -> v0.16.8
    Updating elliptic-curve v0.13.4 -> v0.13.5
    Updating errno v0.2.8 -> v0.3.2
    Updating generic-array v0.14.6 -> v0.14.7
    Updating getrandom v0.2.8 -> v0.2.10
    Updating heck v0.4.0 -> v0.4.1
    Updating hermit-abi v0.2.6 -> v0.3.2
    Updating iana-time-zone v0.1.53 -> v0.1.57
    Updating iana-time-zone-haiku v0.1.1 -> v0.1.2
    Removing io-lifetimes v1.0.3
    Updating is-terminal v0.4.2 -> v0.4.9
    Updating js-sys v0.3.60 -> v0.3.64
    Updating libc v0.2.139 -> v0.2.147
    Updating libm v0.2.6 -> v0.2.7
    Removing link-cplusplus v1.0.8
    Updating linux-raw-sys v0.1.4 -> v0.4.5
    Updating log v0.4.17 -> v0.4.20
    Updating nom v7.1.2 -> v7.1.3
    Updating num-bigint-dig v0.8.2 -> v0.8.4
    Updating num-traits v0.2.15 -> v0.2.16
    Updating once_cell v1.17.0 -> v1.18.0
    Removing os_str_bytes v6.4.1
    Updating p256 v0.13.0 -> v0.13.2
    Updating pbkdf2 v0.12.1 -> v0.12.2
    Updating pkg-config v0.3.26 -> v0.3.27
    Updating primeorder v0.13.0 -> v0.13.2
    Removing proc-macro-error v1.0.4
    Removing proc-macro-error-attr v1.0.4
    Updating proc-macro2 v1.0.56 -> v1.0.66
    Updating quote v1.0.26 -> v1.0.32
    Updating regex v1.7.0 -> v1.9.3
      Adding regex-automata v0.3.6
    Updating regex-syntax v0.6.28 -> v0.7.4
    Updating rustix v0.36.6 -> v0.38.8
    Removing scratch v1.0.3
    Updating sec1 v0.7.1 -> v0.7.3
    Updating serde v1.0.152 -> v1.0.183
    Updating sha2 v0.10.6 -> v0.10.7
    Updating smallvec v1.10.0 -> v1.11.0
    Updating subtle v2.4.1 -> v2.5.0
    Removing syn v1.0.107
    Removing syn v2.0.15
      Adding syn v2.0.28
    Updating termcolor v1.1.3 -> v1.2.0
    Updating unicode-ident v1.0.6 -> v1.0.11
    Removing unicode-width v0.1.10
      Adding utf8parse v0.2.1
    Updating uuid v1.2.2 -> v1.4.1
    Updating wasm-bindgen v0.2.83 -> v0.2.87
    Updating wasm-bindgen-backend v0.2.83 -> v0.2.87
    Updating wasm-bindgen-macro v0.2.83 -> v0.2.87
    Updating wasm-bindgen-macro-support v0.2.83 -> v0.2.87
    Updating wasm-bindgen-shared v0.2.83 -> v0.2.87
      Adding windows v0.48.0
    Updating windows-sys v0.42.0 -> v0.48.0
      Adding windows-targets v0.48.2
    Updating windows_aarch64_gnullvm v0.42.0 -> v0.48.2
    Updating windows_aarch64_msvc v0.42.0 -> v0.48.2
    Updating windows_i686_gnu v0.42.0 -> v0.48.2
    Updating windows_i686_msvc v0.42.0 -> v0.48.2
    Updating windows_x86_64_gnu v0.42.0 -> v0.48.2
    Updating windows_x86_64_gnullvm v0.42.0 -> v0.48.2
    Updating windows_x86_64_msvc v0.42.0 -> v0.48.2
    Updating x509-cert v0.2.3 -> v0.2.4
2023-08-14 19:07:13 -06:00
Arthur Gautier 6a1e1603ef Use x509-cert certificate builder (#495)
Co-authored-by: Carl Wallace <carl@redhoundsoftware.com>
2023-08-14 18:31:39 -06:00
Arthur Gautier 8cf18d2986 Bump rsa to 0.9.0 (#502) 2023-05-03 06:22:45 -06:00
dependabot[bot] 07281440c0 Bump rsa from 0.9.0-pre.1 to 0.9.0-pre.2 (#500)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.0-pre.1 to 0.9.0-pre.2.
- [Release notes](https://github.com/RustCrypto/RSA/releases)
- [Changelog](https://github.com/RustCrypto/RSA/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.0-pre.1...v0.9.0-pre.2)

---
updated-dependencies:
- dependency-name: rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-27 08:54:23 -06:00
dependabot[bot] cd76a55318 Bump rsa from 0.9.0-pre.0 to 0.9.0-pre.1 (#497)
Bumps [rsa](https://github.com/RustCrypto/RSA) from 0.9.0-pre.0 to 0.9.0-pre.1.
- [Release notes](https://github.com/RustCrypto/RSA/releases)
- [Changelog](https://github.com/RustCrypto/RSA/blob/master/CHANGELOG.md)
- [Commits](https://github.com/RustCrypto/RSA/compare/v0.9.0-pre.0...v0.9.0-pre.1)

---
updated-dependencies:
- dependency-name: rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-10 10:10:55 -06:00
dependabot[bot] 23bbf1b783 Bump zeroize from 1.5.7 to 1.6.0 (#496)
Bumps [zeroize](https://github.com/RustCrypto/utils) from 1.5.7 to 1.6.0.
- [Release notes](https://github.com/RustCrypto/utils/releases)
- [Commits](https://github.com/RustCrypto/utils/compare/zeroize-v1.5.7...zeroize-v1.6.0)

---
updated-dependencies:
- dependency-name: zeroize
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 11:33:52 -06:00
31 changed files with 2980 additions and 2014 deletions
+1
View File
@@ -2,4 +2,5 @@
ignore = [
"RUSTSEC-2020-0071", # chrono
"RUSTSEC-2021-0145", # atty
"RUSTSEC-2023-0071", # rsa: Marvin Attack: potential key recovery
] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
+12 -1
View File
@@ -1,8 +1,19 @@
version: 2
updates:
- package-ecosystem: cargo
versioning-strategy: lockfile-only
directory: "/"
allow:
- dependency-type: "all"
groups:
all-deps:
patterns:
- "*"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
time: '13:00'
open-pull-requests-limit: 10
+8 -8
View File
@@ -13,7 +13,7 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -36,17 +36,17 @@ jobs:
toolchain: stable
deps: true
- platform: ubuntu-latest
toolchain: 1.65.0 # MSRV
toolchain: 1.85.0 # MSRV
deps: sudo apt-get install libpcsclite-dev
- platform: windows-latest
toolchain: 1.65.0 # MSRV
toolchain: 1.85.0 # MSRV
deps: true
- platform: macos-latest
toolchain: 1.65.0 # MSRV
toolchain: 1.85.0 # MSRV
deps: true
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v1
uses: actions/checkout@v6
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
@@ -78,11 +78,11 @@ jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
toolchain: 1.85.0 # MSRV
components: clippy
override: true
- run: sudo apt-get install libpcsclite-dev
+3 -3
View File
@@ -15,16 +15,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v1
uses: actions/checkout@v6
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v5
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v5
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }}
+70 -3
View File
@@ -6,9 +6,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- `YubiKey::disconnect`
- `impl Debug for {Context, YubiKey}`
- `Error::AppletNotFound`
- `yubikey::certificate::SelfSigned`
- `yubikey::Error::CertificateBuilder`
- `yubikey::MgmAlgorithmId`
- `yubikey::mgm`:
- `MgmKey::generate_for`
- `MgmKey::get_default`
- `impl AsRef<[u8]> for MgmKey`
### Changed
- MSRV is now 1.81.
- Migrated the public API to the following (pre-release) dependencies:
- `der 0.8.0-rc.1`
- `ecdsa 0.17.0-pre.9`
- `p256 0.14.0-pre.2`
- `p384 0.14.0-pre.2`
- `rsa 0.10.0-pre.3`
- `sha2 0.11.0-pre.4`
- `x509-cert 0.3.0-pre.0`
- `yubikey::mgm`:
- `MgmKey::generate` now takes a `rand::TryCryptoRng` argument.
- `MgmKey::generate` now requires the caller to specify the key algorithm via
an `MgmAlgorithmId` parameter.
- Use `MgmKey::generate_for` if you want to generate a key using the
preferred algorithm for a given Yubikey's firmware version.
- `MgmKey::from_bytes` now takes an `Option<MgmAlgorithmId>` argument, to
disambiguate algorithms with the same key length.
- `yubikey::piv`:
- `ManagementAlgorithmId` has been renamed to `SlotAlgorithmId`, and its
`ThreeDes` variant has been replaced by `SlotAlgorithmId::Management`
containing a `yubikey::MgmAlgorithmId`.
- Metadata command returns `Error:NotFound` instead of `Error::GenericError` when the object doesn't exist ([#558]).
### Removed
- `yubikey::mgm`:
- `MgmKey::new` (use `MgmKey::from_bytes(_, Some(MgmAlgorithmId::ThreeDes))`
instead).
- `impl AsRef<[u8; DES_LEN_3DES]> for MgmKey` (use
`impl AsRef<[u8]> for MgmKey` instead).
- `impl Default for MgmKey` (use `MgmKey::get_default` instead).
- `impl TryFrom<&[u8]> for MgmKey` (use `MgmKey::from_bytes` instead).
## 0.8.0 (2023-08-15)
### Added
- `impl Debug for {Context, YubiKey}` ([#457])
- `YubiKey::disconnect` ([#462])
- `Error::AppletNotFound` ([#476])
### Changed
- `Reader::open` now returns `Error::AppletNotFound` instead of `Error::Generic`
@@ -18,11 +61,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Reader::open` now avoids resetting the card if an error occurs (equivalent to
calling `YubiKey::disconnect(pcsc::Disposition::LeaveCard)` if `Reader::open`
succeeds).
- Raise minimum `pcsc` version to 2.3.1 and remove workaround ([#478])
- Bump asymmetric crypto dependencies; MSRV 1.65 ([#490])
- `elliptic-curve` v0.13
- `k256` v0.13
- `p256` v0.13
- `p384` v0.13
- `pbkdf2` v0.12
- `rsa` v0.9 ([#502])
- `signature` v2
- Use `x509-cert` certificate builder ([#495])
- Make `RsaKeyData::new` fallible ([#517])
### Fixed
- `StatusWords::code` now returns the correct code (including embedded `tries`
count) for `StatusWords::VerifyFailError`. Previously the returned code lost
information and was not round-trip compatible with `StatusWords::from(u16)`.
- Parsing of serial numbers ([#466])
- Make `YubiKey::open()` more robust ([#504])
[#457]: https://github.com/iqlusioninc/yubikey.rs/pull/457
[#462]: https://github.com/iqlusioninc/yubikey.rs/pull/462
[#466]: https://github.com/iqlusioninc/yubikey.rs/pull/466
[#476]: https://github.com/iqlusioninc/yubikey.rs/pull/476
[#478]: https://github.com/iqlusioninc/yubikey.rs/pull/478
[#490]: https://github.com/iqlusioninc/yubikey.rs/pull/490
[#495]: https://github.com/iqlusioninc/yubikey.rs/pull/495
[#502]: https://github.com/iqlusioninc/yubikey.rs/pull/502
[#504]: https://github.com/iqlusioninc/yubikey.rs/pull/504
[#517]: https://github.com/iqlusioninc/yubikey.rs/pull/517
## 0.7.0 (2022-11-14)
### Added
Generated
+826 -755
View File
File diff suppressed because it is too large Load Diff
+33 -25
View File
@@ -1,10 +1,10 @@
[package]
name = "yubikey"
version = "0.8.0-pre.0"
version = "0.9.0-pre.0"
description = """
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
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
"""
authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"]
@@ -14,47 +14,55 @@ readme = "README.md"
categories = ["api-bindings", "authentication", "cryptography", "hardware-support"]
keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"]
edition = "2021"
rust-version = "1.65"
rust-version = "1.85"
[workspace]
members = [".", "cli"]
[workspace.dependencies]
sha2 = "0.11"
x509-cert = { version = "0.3.0-rc.4", features = ["builder", "hazmat"] }
[dependencies]
chrono = "0.4.23"
cookie-factory = "0.3"
der-parser = "8"
des = "0.8"
elliptic-curve = "0.13"
aes = { version = "0.9.0-rc.4", features = ["zeroize"] }
bitflags = "2.5.0"
cipher = { version = "0.5", features = ["getrandom", "rand_core"] }
curve25519-dalek = "5.0.0-pre.6"
der = "0.8"
des = "0.9.0-rc.3"
ecdsa = { version = "0.17.0-rc.16", features = ["digest", "pem"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["alloc", "pkcs8"] }
elliptic-curve = "0.14.0-rc.29"
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
hmac = "0.12"
log = "0.4"
nom = "7"
num-bigint-dig = { version = "0.8", features = ["rand"] }
num-traits = "0.2"
num-integer = "0.1"
p256 = "0.13"
p384 = "0.13"
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
nom = "8"
p256 = "0.14.0-rc.8"
p384 = "0.14.0-rc.8"
pbkdf2 = { version = "0.13.0-rc.10", default-features = false, features = ["hmac"] }
pcsc = "2.3.1"
rand_core = { version = "0.6", features = ["std"] }
rsa = "=0.9.0-pre.0"
secrecy = "0.8"
sha1 = { version = "0.10", features = ["oid"] }
sha2 = { version = "0.10", features = ["oid"] }
rand = "0.10"
rand_core = "0.10"
rsa = { version = "0.10.0-rc.17", features = ["sha2"] }
sha1 = { version = "0.11", features = ["oid"] }
sha2 = { workspace = true, features = ["oid"] }
signature = "3.0.0-rc.10"
subtle = "2"
uuid = { version = "1.2", features = ["v4"] }
x509 = "0.2"
x509-parser = "0.14"
x25519-dalek = "3.0.0-pre.6"
x509-cert.workspace = true
zeroize = "1"
[dev-dependencies]
env_logger = "0.10"
env_logger = "0.11"
once_cell = "1"
signature = "2"
[features]
untested = []
[[example]]
name = "change-mode"
required-features = ["untested"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+16 -13
View File
@@ -22,7 +22,7 @@ access provided by the [`pcsc` crate].
## About
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
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.
### Supported Algorithms
- **Authentication**: `3DES`
- **Encryption**:
- RSA: `RSA1024`, `RSA2048`
- RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
- **Signatures**:
- RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
- RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
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
@@ -73,8 +77,7 @@ Rust **1.60** or newer.
- [YubiKey 4] series
- [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
## Supported Operating Systems
@@ -102,8 +105,8 @@ We would appreciate any help testing this functionality and removing the
## Testing
To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in
the default state (i.e. default PIN/PUK).
To run the full test suite, you'll need a supported YubiKey device connected
which is in the default state (i.e. default PIN/PUK).
Tests which run live against a YubiKey device are marked as `#[ignore]` by
default in order to pass when running in a CI environment. To run these
@@ -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
as a derived work.
Copyright (c) 2014-2023 Yubico AB, Tony Arcieri
Copyright (c) 2014-2025 Yubico AB, Tony Arcieri
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -208,16 +211,16 @@ or conditions.
[//]: # (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
[docs-image]: https://docs.rs/yubikey/badge.svg
[docs-link]: https://docs.rs/yubikey/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[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-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
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
[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
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[piv-tool-guide]: https://docs.yubico.com/software/yubikey/tools/pivtool/Introduction.html
[Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
+4
View File
@@ -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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- MSRV is now 1.81.
## 0.7.0 (2022-11-14)
### Changed
- Bump `clap` to v4.0 ([#438])
+6 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "yubikey-cli"
version = "0.7.0"
version = "0.9.0-pre"
description = """
Command-line interface for performing encryption and signing using RSA/ECC keys
stored on YubiKey devices.
@@ -12,15 +12,15 @@ readme = "README.md"
categories = ["command-line-utilities", "cryptography", "hardware-support"]
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
edition = "2021"
rust-version = "1.56"
rust-version = "1.85"
[dependencies]
clap = { version = "4", features = ["derive"] }
env_logger = "0.10"
env_logger = "0.11"
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
log = "0.4"
once_cell = "1"
sha2 = "0.10"
sha2.workspace = true
termcolor = "1"
x509-parser = "0.14"
yubikey = { version = "0.8.0-pre.0", path = ".." }
x509-cert.workspace = true
yubikey = { version = "=0.9.0-pre.0", path = ".." }
+2 -4
View File
@@ -25,8 +25,7 @@ Rust **1.60** or newer.
- [YubiKey 4] series
- [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
## Security Warning
@@ -84,7 +83,7 @@ or conditions.
[docs-image]: https://docs.rs/yubikey-cli/badge.svg
[docs-link]: https://docs.rs/yubikey-cli/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.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
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/
@@ -98,7 +97,6 @@ or conditions.
[PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
+12 -27
View File
@@ -9,7 +9,7 @@ use std::{
sync::Mutex,
};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
use x509_parser::parse_x509_certificate;
use x509_cert::der::Encode;
use yubikey::{certificate::Certificate, piv::*, YubiKey};
/// Print a success status message (in green if colors are enabled)
@@ -180,52 +180,37 @@ pub fn print_cert_info(
return Ok(());
}
};
let buf = cert.into_buffer();
let cert = &cert.cert;
if !buf.is_empty() {
let fingerprint = Sha256::digest(&buf);
let fingerprint = Sha256::digest(cert.to_der().unwrap());
let slot_id: u8 = slot.into();
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
match parse_x509_certificate(&buf) {
Ok((_rem, cert)) => {
print_cert_attr(
stream,
"Algorithm",
cert.tbs_certificate.subject_pki.algorithm.algorithm,
cert.tbs_certificate()
.subject_public_key_info()
.algorithm
.oid,
)?;
print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?;
print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?;
print_cert_attr(stream, "Subject", cert.tbs_certificate().subject())?;
print_cert_attr(stream, "Issuer", cert.tbs_certificate().issuer())?;
print_cert_attr(
stream,
"Fingerprint",
&hex::upper::encode_string(&fingerprint),
hex::upper::encode_string(&fingerprint),
)?;
print_cert_attr(
stream,
"Not Before",
cert.tbs_certificate
.validity
.not_before
.to_rfc2822()
.unwrap(),
cert.tbs_certificate().validity().not_before,
)?;
print_cert_attr(
stream,
"Not After",
cert.tbs_certificate
.validity
.not_after
.to_rfc2822()
.unwrap(),
cert.tbs_certificate().validity().not_after,
)?;
}
_ => {
println!("Failed to parse certificate");
return Ok(());
}
};
}
Ok(())
}
+10
View File
@@ -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
View File
@@ -82,6 +82,12 @@ impl Apdu {
self
}
/// Set this APDU's second parameter only
pub(crate) fn p2(&mut self, value: u8) -> &mut Self {
self.p2 = value;
self
}
/// Set both parameters for this APDU
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
self.p1 = p1;
@@ -198,6 +204,15 @@ pub enum Ins {
/// Get slot metadata
GetMetadata,
/// Management // Read Config
ReadConfig,
/// Management // Write Config
WriteConfig,
/// Management // DeviceReset
DeviceReset,
/// Other/unrecognized instruction codes
Other(u8),
}
@@ -223,6 +238,12 @@ impl Ins {
Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8,
Ins::GetMetadata => 0xf7,
// Management
Ins::ReadConfig => 0x1d,
Ins::WriteConfig => 0x1c,
Ins::DeviceReset => 0x1f,
Ins::Other(code) => code,
}
}
@@ -231,6 +252,11 @@ impl Ins {
impl From<u8> for Ins {
fn from(code: u8) -> Self {
match code {
// Management
0x1d => Ins::ReadConfig,
0x1c => Ins::WriteConfig,
0x1f => Ins::DeviceReset,
0x20 => Ins::Verify,
0x24 => Ins::ChangeReference,
0x2c => Ins::ResetRetry,
@@ -318,7 +344,7 @@ impl From<Vec<u8>> for Response {
}
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;
@@ -395,6 +421,9 @@ pub(crate) enum StatusWords {
/// Not enough memory
NoSpaceError,
/// Referenced data or reference data not found
ReferenceDataNotFoundError,
//
// Custom Yubico Status Word extensions
//
@@ -428,6 +457,7 @@ impl StatusWords {
StatusWords::IncorrectParamError => 0x6a80,
StatusWords::NotFoundError => 0x6a82,
StatusWords::NoSpaceError => 0x6a84,
StatusWords::ReferenceDataNotFoundError => 0x6a88,
StatusWords::IncorrectSlotError => 0x6b00,
StatusWords::NotSupportedError => 0x6d00,
StatusWords::CommandAbortedError => 0x6f00,
@@ -462,6 +492,7 @@ impl From<u16> for StatusWords {
0x6a80 => StatusWords::IncorrectParamError,
0x6a82 => StatusWords::NotFoundError,
0x6a84 => StatusWords::NoSpaceError,
0x6a88 => StatusWords::ReferenceDataNotFoundError,
0x6b00 => StatusWords::IncorrectSlotError,
0x6d00 => StatusWords::NotSupportedError,
0x6f00 => StatusWords::CommandAbortedError,
+11 -12
View File
@@ -30,8 +30,9 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{Error, Result, YubiKey};
use rand_core::{OsRng, RngCore};
use crate::{Result, YubiKey};
use cipher::common::Generate;
use rand_core::CryptoRng;
use std::fmt::{self, Debug, Display};
/// CCCID offset
@@ -48,6 +49,7 @@ const OBJ_CAPABILITY: u32 = 0x005f_c107;
/// - 0xff == Manufacturer ID (dummy)
/// - 0x02 == Card type (javaCard)
/// - next 14 bytes: card ID
#[allow(dead_code)]
const CCC_TMPL: &[u8] = &[
0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4,
@@ -65,9 +67,12 @@ impl CardId {
/// Generate a random CCC Card ID
pub fn generate() -> Self {
let mut id = [0u8; Self::BYTE_SIZE];
OsRng.fill_bytes(&mut id);
Self(id)
Self(Generate::generate())
}
/// Generate a random CCC Card ID from an [`Rng`]
pub fn generate_from_rng<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
Self(Generate::generate_from_rng(rng))
}
}
@@ -90,17 +95,11 @@ impl CccId {
pub fn get(yubikey: &mut YubiKey) -> Result<Self> {
let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(OBJ_CAPABILITY)?;
if response.len() != CCC_TMPL.len() {
return Err(Error::GenericError);
}
Ok(Self(response[..Self::BYTE_SIZE].try_into().unwrap()))
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
}
/// Set Cardholder Capability Container (CCC) ID
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
let mut buf = CCC_TMPL.to_vec();
buf[0..self.0.len()].copy_from_slice(&self.0);
+315 -499
View File
@@ -33,90 +33,27 @@
use crate::{
consts::CB_OBJ_MAX,
error::{Error, Result},
piv::{sign_data, AlgorithmId, SlotId},
piv::SlotId,
serialization::*,
transaction::Transaction,
yubikey::YubiKey,
Buffer,
};
use chrono::{DateTime, Utc};
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
use log::error;
use num_bigint_dig::BigUint;
use p256::NistP256;
use p384::NistP384;
use rsa::{PublicKeyParts, RsaPublicKey};
use sha2::{Digest, Sha256};
use std::fmt::Display;
use std::{fmt, ops::DerefMut};
use x509::{der::Oid, RelativeDistinguishedName};
use x509_parser::{parse_x509_certificate, x509::SubjectPublicKeyInfo};
use x509_cert::{
builder::{profile::BuilderProfile, Builder, CertificateBuilder},
der::{referenced::OwnedToRef, Decode, Encode},
name::Name,
serial_number::SerialNumber,
spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
time::Validity,
};
use zeroize::Zeroizing;
// TODO: Make these der_parser::oid::Oid constants when it has const fn support.
const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
const OID_NIST_P256: &str = "1.2.840.10045.3.1.7";
const OID_NIST_P384: &str = "1.3.132.0.34";
const TAG_CERT: u8 = 0x70;
const TAG_CERT_COMPRESS: u8 = 0x71;
const TAG_CERT_LRC: u8 = 0xFE;
/// A serial number for a [`Certificate`].
#[derive(Clone, Debug)]
pub struct Serial(BigUint);
impl From<BigUint> for Serial {
fn from(num: BigUint) -> Serial {
Serial(num)
}
}
impl From<[u8; 20]> for Serial {
fn from(bytes: [u8; 20]) -> Serial {
Serial(BigUint::from_bytes_be(&bytes))
}
}
impl TryFrom<&[u8]> for Serial {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Serial> {
if bytes.len() <= 20 {
Ok(Serial(BigUint::from_bytes_be(bytes)))
} else {
Err(Error::ParseError)
}
}
}
impl Serial {
fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes_be()
}
/// Returns itself formatted as x509 compatible hex string
pub fn as_x509_hex(&self) -> String {
let data = self.to_bytes();
let raw_hex_string = format!("{:02X?}", data);
raw_hex_string
.replace(", ", ":")
.replace([']', '['], "")
.to_lowercase()
}
/// Returns itself formatted as x509 compatible int string
pub fn as_x509_int(&self) -> String {
let Serial(buint) = self;
format!("{}", buint)
}
}
impl Display for Serial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_x509_hex())
}
}
/// Information about how a [`Certificate`] is stored within a YubiKey.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CertInfo {
@@ -148,210 +85,11 @@ impl From<CertInfo> for u8 {
}
}
impl x509::AlgorithmIdentifier for AlgorithmId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
// RSA encryption
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => &[1, 2, 840, 113_549, 1, 1, 1],
// EC Public Key
AlgorithmId::EccP256 | AlgorithmId::EccP384 => &[1, 2, 840, 10045, 2, 1],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
use x509::der::write::der_oid;
// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
// ```text
// ECParameters ::= CHOICE {
// namedCurve OBJECT IDENTIFIER
// -- implicitCurve NULL
// -- specifiedCurve SpecifiedECDomain
// }
// ```
match self {
AlgorithmId::EccP256 => der_oid(&[1, 2, 840, 10045, 3, 1, 7][..])(w),
AlgorithmId::EccP384 => der_oid(&[1, 3, 132, 0, 34][..])(w),
_ => Ok(w),
}
}
}
/// Information about a public key within a [`Certificate`].
#[derive(Clone, Eq, PartialEq)]
pub enum PublicKeyInfo {
/// RSA keys
Rsa {
/// RSA algorithm
algorithm: AlgorithmId,
/// Public key
pubkey: RsaPublicKey,
},
/// EC P-256 keys
EcP256(EcPublicKey<NistP256>),
/// EC P-384 keys
EcP384(EcPublicKey<NistP384>),
}
impl fmt::Debug for PublicKeyInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKeyInfo({:?})", self.algorithm())
}
}
impl PublicKeyInfo {
fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result<Self> {
match subject_pki.algorithm.algorithm.to_string().as_str() {
OID_RSA_ENCRYPTION => {
let pubkey = read_pki::rsa_pubkey(&subject_pki.subject_public_key.data)?;
Ok(PublicKeyInfo::Rsa {
algorithm: match pubkey.n().bits() {
1024 => AlgorithmId::Rsa1024,
2048 => AlgorithmId::Rsa2048,
_ => return Err(Error::AlgorithmError),
},
pubkey,
})
}
OID_EC_PUBLIC_KEY => {
let key_bytes = &subject_pki.subject_public_key.data;
let algorithm_parameters = subject_pki
.algorithm
.parameters
.as_ref()
.ok_or(Error::InvalidObject)?;
match read_pki::ec_parameters(algorithm_parameters)? {
AlgorithmId::EccP256 => EcPublicKey::<NistP256>::from_bytes(key_bytes)
.map(PublicKeyInfo::EcP256)
.map_err(|_| Error::InvalidObject),
AlgorithmId::EccP384 => EcPublicKey::<NistP384>::from_bytes(key_bytes)
.map(PublicKeyInfo::EcP384)
.map_err(|_| Error::InvalidObject),
_ => Err(Error::AlgorithmError),
}
}
_ => Err(Error::InvalidObject),
}
}
/// Returns the algorithm that this public key can be used with.
pub fn algorithm(&self) -> AlgorithmId {
match self {
PublicKeyInfo::Rsa { algorithm, .. } => *algorithm,
PublicKeyInfo::EcP256(_) => AlgorithmId::EccP256,
PublicKeyInfo::EcP384(_) => AlgorithmId::EccP384,
}
}
}
impl x509::SubjectPublicKeyInfo for PublicKeyInfo {
type AlgorithmId = AlgorithmId;
type SubjectPublicKey = Vec<u8>;
fn algorithm_id(&self) -> AlgorithmId {
self.algorithm()
}
fn public_key(&self) -> Vec<u8> {
match self {
PublicKeyInfo::Rsa { pubkey, .. } => {
cookie_factory::gen_simple(write_pki::rsa_pubkey(pubkey), vec![])
.expect("can write to Vec")
}
PublicKeyInfo::EcP256(pubkey) => pubkey.as_bytes().to_vec(),
PublicKeyInfo::EcP384(pubkey) => pubkey.as_bytes().to_vec(),
}
}
}
/// Digest algorithms.
///
/// See RFC 4055 and RFC 8017.
enum DigestId {
/// Secure Hash Algorithm 256 (SHA256)
Sha256,
}
impl x509::AlgorithmIdentifier for DigestId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
// See https://tools.ietf.org/html/rfc4055#section-2.1
DigestId::Sha256 => &[2, 16, 840, 1, 101, 3, 4, 2, 1],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
// Parameters are an explicit NULL
// See https://tools.ietf.org/html/rfc8017#appendix-A.2.4
x509::der::write::der_null()(w)
}
}
enum SignatureId {
/// Public-Key Cryptography Standards (PKCS) #1 version 1.5 signature algorithm with
/// Secure Hash Algorithm 256 (SHA256) and Rivest, Shamir and Adleman (RSA) encryption
///
/// See RFC 4055 and RFC 8017.
Sha256WithRsaEncryption,
/// Elliptic Curve Digital Signature Algorithm (DSA) coupled with the Secure Hash
/// Algorithm 256 (SHA256) algorithm
///
/// See RFC 5758.
EcdsaWithSha256,
}
impl x509::AlgorithmIdentifier for SignatureId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
SignatureId::Sha256WithRsaEncryption => &[1, 2, 840, 113_549, 1, 1, 11],
SignatureId::EcdsaWithSha256 => &[1, 2, 840, 10045, 4, 3, 2],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
// No parameters for any SignatureId
Ok(w)
}
}
/// Certificates
#[derive(Clone, Debug)]
pub struct Certificate {
serial: Serial,
#[allow(dead_code)]
issuer: String,
subject: String,
subject_pki: PublicKeyInfo,
data: Buffer,
}
impl<'a> TryFrom<&'a [u8]> for Certificate {
type Error = Error;
fn try_from(bytes: &'a [u8]) -> Result<Self> {
Self::from_bytes(bytes.to_vec())
}
/// Inner certificate
pub cert: x509_cert::Certificate,
}
impl Certificate {
@@ -360,112 +98,30 @@ impl Certificate {
///
/// `extensions` is optional; if empty, no extensions will be included. Due to the
/// need for an `O: Oid` type parameter, users who do not have any extensions should
/// use the workaround `let extensions: &[x509::Extension<'_, &[u64]>] = &[];`.
pub fn generate_self_signed<O: Oid>(
/// use the workaround `let extensions: &[x509_cert::Extension<'_, &[u64]>] = &[];`.
pub fn generate_self_signed<F, KT: yubikey_signer::KeyType>(
yubikey: &mut YubiKey,
key: SlotId,
serial: impl Into<Serial>,
not_after: Option<DateTime<Utc>>,
subject: &[RelativeDistinguishedName<'_>],
subject_pki: PublicKeyInfo,
extensions: &[x509::Extension<'_, O>],
) -> Result<Self> {
let serial = serial.into();
serial: SerialNumber,
validity: Validity,
subject: Name,
subject_pki: SubjectPublicKeyInfoOwned,
extensions: F,
) -> Result<Self>
where
F: FnOnce(&mut CertificateBuilder<SelfSigned>) -> der::Result<()>,
{
let signer =
yubikey_signer::Signer::<'_, KT>::new(yubikey, key, subject_pki.owned_to_ref())?;
let mut builder =
CertificateBuilder::new(SelfSigned { subject }, serial, validity, subject_pki)
.map_err(|_| Error::KeyError)?;
let mut tbs_cert = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
let signature_algorithm = match subject_pki.algorithm() {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => SignatureId::Sha256WithRsaEncryption,
AlgorithmId::EccP256 | AlgorithmId::EccP384 => SignatureId::EcdsaWithSha256,
};
cookie_factory::gen(
x509::write::tbs_certificate(
&serial.to_bytes(),
&signature_algorithm,
// Issuer and subject are the same in self-signed certificates.
subject,
Utc::now(),
not_after,
subject,
&subject_pki,
extensions,
),
tbs_cert.deref_mut(),
)
.expect("can serialize to Vec");
let signature = match signature_algorithm {
SignatureId::Sha256WithRsaEncryption => {
use cookie_factory::{combinator::slice, sequence::tuple};
use x509::{
der::write::{der_octet_string, der_sequence},
write::algorithm_identifier,
};
let em_len = if let AlgorithmId::Rsa1024 = subject_pki.algorithm() {
128
} else {
256
};
let h = Sha256::digest(&tbs_cert);
let t = cookie_factory::gen_simple(
der_sequence((
algorithm_identifier(&DigestId::Sha256),
der_octet_string(&h),
)),
vec![],
)
.expect("can serialize into Vec");
let em = cookie_factory::gen_simple(
tuple((
slice(&[0x00, 0x01]),
slice(&vec![0xff; em_len - t.len() - 3]),
slice(&[0x00]),
slice(t),
)),
vec![],
)
.expect("can serialize to Vec");
sign_data(yubikey, &em, subject_pki.algorithm(), key)
}
SignatureId::EcdsaWithSha256 => sign_data(
yubikey,
&Sha256::digest(&tbs_cert),
subject_pki.algorithm(),
key,
),
}?;
let mut data = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
cookie_factory::gen(
x509::write::certificate(&tbs_cert, &signature_algorithm, &signature),
data.deref_mut(),
)
.expect("can serialize to Vec");
let (issuer, subject) = parse_x509_certificate(&data)
.map(|(_, cert)| {
(
cert.tbs_certificate.issuer.to_string(),
cert.tbs_certificate.subject.to_string(),
)
})
.expect("We just serialized this correctly");
let cert = Certificate {
serial,
issuer,
subject,
subject_pki,
data,
};
// Add custom extensions
extensions(&mut builder)?;
let cert = builder.build(&signer).map_err(|_| Error::KeyError)?;
let cert = Self { cert };
cert.write(yubikey, key, CertInfo::Uncompressed)?;
Ok(cert)
@@ -480,18 +136,18 @@ impl Certificate {
return Err(Error::InvalidObject);
}
Certificate::from_bytes(buf)
Self::from_bytes(buf)
}
/// Write this certificate into the YubiKey in the given slot
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> {
let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, Some(&self.data), certinfo)
let data = self.cert.to_der().map_err(|_| Error::InvalidObject)?;
write_certificate(&txn, slot, Some(&data), certinfo)
}
/// Delete a certificate located at the given slot of the given YubiKey
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> {
let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, None, CertInfo::Uncompressed)
@@ -506,55 +162,59 @@ impl Certificate {
return Err(Error::SizeError);
}
let parsed_cert = match parse_x509_certificate(&cert) {
Ok((_, cert)) => cert,
_ => return Err(Error::InvalidObject),
};
let serial = Serial::try_from(parsed_cert.tbs_certificate.serial.to_bytes_be().as_slice())
.map_err(|_| Error::InvalidObject)?;
let issuer = parsed_cert.tbs_certificate.issuer.to_string();
let subject = parsed_cert.tbs_certificate.subject.to_string();
let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?;
Ok(Certificate {
serial,
issuer,
subject,
subject_pki,
data: cert,
})
}
/// Returns the serial number of the certificate.
pub fn serial(&self) -> &Serial {
&self.serial
x509_cert::Certificate::from_der(&cert)
.map(|cert| Self { cert })
.map_err(|_| Error::InvalidObject)
}
/// Returns the Issuer field of the certificate.
pub fn issuer(&self) -> &str {
&self.issuer
pub fn issuer(&self) -> String {
self.cert.tbs_certificate().issuer().to_string()
}
/// Returns the SubjectName field of the certificate.
pub fn subject(&self) -> &str {
&self.subject
pub fn subject(&self) -> String {
self.cert.tbs_certificate().subject().to_string()
}
/// Returns the SubjectPublicKeyInfo field of the certificate.
pub fn subject_pki(&self) -> &PublicKeyInfo {
&self.subject_pki
}
/// Extract the inner buffer
pub fn into_buffer(self) -> Buffer {
self.data
pub fn subject_pki(&self) -> SubjectPublicKeyInfoRef<'_> {
self.cert
.tbs_certificate()
.subject_public_key_info()
.owned_to_ref()
}
}
impl AsRef<[u8]> for Certificate {
fn as_ref(&self) -> &[u8] {
self.data.as_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![])
}
}
@@ -590,12 +250,7 @@ pub(crate) fn write_certificate(
) -> Result<()> {
let object_id = slot.object_id();
if data.is_none() {
return txn.save_object(object_id, &[]);
}
let data = data.unwrap();
if let Some(data) = data {
let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
@@ -604,99 +259,260 @@ pub(crate) fn write_certificate(
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
txn.save_object(object_id, &buf[..offset])
} else {
txn.save_object(object_id, &[])
}
}
mod read_pki {
use der_parser::{
asn1_rs::Any,
ber::BerObjectContent,
der::{parse_der_integer, parse_der_sequence_defined_g, DerObject},
error::BerError,
pub mod yubikey_signer {
//! Signer implementation for yubikey
use crate::{
error::{Error, Result},
piv::AlgorithmId,
piv::{sign_data, SlotId},
YubiKey,
};
use nom::{combinator, sequence::pair, IResult};
use rsa::{BigUint, RsaPublicKey};
use super::{OID_NIST_P256, OID_NIST_P384};
use crate::{piv::AlgorithmId, Error, Result};
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
/// ```text
/// RSAPublicKey ::= SEQUENCE {
/// modulus INTEGER, -- n
/// publicExponent INTEGER -- e
/// }
/// ```
pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result<RsaPublicKey> {
fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], (DerObject<'_>, DerObject<'_>), BerError> {
parse_der_sequence_defined_g(|i, _| pair(parse_der_integer, parse_der_integer)(i))(i)
}
fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> {
combinator::map(parse_rsa_pubkey, |(modulus, public_exponent)| {
let n = match modulus.content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
use der::{
asn1::{Any, OctetString},
oid::db::rfc5912,
Encode, Sequence,
};
let e = match public_exponent.content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
use sha2::{Digest, Sha256, Sha384, Sha512};
use signature::Keypair;
use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
use x509_cert::spki::{
self, AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier, EncodePublicKey,
SignatureBitStringEncoding, SubjectPublicKeyInfoRef,
};
(n, e)
})(i)
type SigResult<T> = core::result::Result<T, signature::Error>;
/// Key to be used to sign certificates
pub trait KeyType {
/// Error returned when working with signature
type Error: Into<signature::Error> + fmt::Debug;
/// The signature type returned by the signer
type Signature: SignatureBitStringEncoding
+ for<'s> TryFrom<&'s [u8], Error = Self::Error>
+ fmt::Debug;
/// The public key used to verify signature
type VerifyingKey: EncodePublicKey
+ DynSignatureAlgorithmIdentifier
+ Clone
+ From<Self::PublicKey>;
/// Public key type used to load the SPKI formatted key
type PublicKey: for<'a> TryFrom<SubjectPublicKeyInfoRef<'a>, Error = spki::Error>;
/// The algorithm used when talking with the yubikey
const ALGORITHM: AlgorithmId;
/// Prepare buffer before submitting it for signature
fn prepare(input: &[u8]) -> SigResult<Vec<u8>>;
/// Read back the signature from the device
fn read_signature(input: &[u8]) -> SigResult<Self::Signature>;
}
let (n, e) = match rsa_pubkey_parts(encoded) {
Ok((_, res)) => res,
_ => return Err(Error::InvalidObject),
impl KeyType for ed25519_dalek::SigningKey {
const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
type Error = ed25519_dalek::SignatureError;
type Signature = ed25519_dalek::Signature;
type VerifyingKey = ed25519_dalek::VerifyingKey;
type PublicKey = ed25519_dalek::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha512::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::try_from(input)
}
}
impl KeyType for p256::NistP256 {
const ALGORITHM: AlgorithmId = AlgorithmId::EccP256;
type Error = ecdsa::Error;
type Signature = p256::ecdsa::DerSignature;
type VerifyingKey = p256::ecdsa::VerifyingKey;
type PublicKey = p256::ecdsa::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha256::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::from_bytes(input)
}
}
impl KeyType for p384::NistP384 {
const ALGORITHM: AlgorithmId = AlgorithmId::EccP384;
type Error = ecdsa::Error;
type Signature = p384::ecdsa::DerSignature;
type VerifyingKey = p384::ecdsa::VerifyingKey;
type PublicKey = p384::ecdsa::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha384::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::from_bytes(input)
}
}
/// Trait used to handle subtypes of RSA keys
pub trait RsaLength {
/// The length of the RSA key in bits
const BIT_LENGTH: usize;
/// The algorithm to use when talking with the Yubikey.
const ALGORITHM: AlgorithmId;
}
/// RSA 1024 bits key
pub struct Rsa1024;
impl RsaLength for Rsa1024 {
const BIT_LENGTH: usize = 1024;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa1024;
}
/// RSA 2048 bits key
pub struct Rsa2048;
impl RsaLength for Rsa2048 {
const BIT_LENGTH: usize = 2048;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa2048;
}
/// RSA 3072 bits key
pub struct Rsa3072;
impl RsaLength for Rsa3072 {
const BIT_LENGTH: usize = 3072;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa3072;
}
/// RSA 4096 bits key
pub struct Rsa4096;
impl RsaLength for Rsa4096 {
const BIT_LENGTH: usize = 4096;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa4096;
}
/// RSA keys used to sign certificates
pub struct YubiRsa<N: RsaLength> {
_len: PhantomData<N>,
}
impl<N: RsaLength> KeyType for YubiRsa<N> {
type Error = signature::Error;
type Signature = rsa::pkcs1v15::Signature;
type VerifyingKey = rsa::pkcs1v15::VerifyingKey<Sha256>;
type PublicKey = rsa::RsaPublicKey;
const ALGORITHM: AlgorithmId = N::ALGORITHM;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
let hashed = Sha256::digest(input).to_vec();
OctetString::new(hashed)
.map_err(|e| e.into())
.and_then(Self::emsa_pkcs1_1_5)
.map_err(signature::Error::from_source)
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::try_from(input)
}
}
impl<N: RsaLength> YubiRsa<N> {
/// https://www.rfc-editor.org/rfc/rfc8017#section-9.2
fn emsa_pkcs1_1_5(digest: OctetString) -> Result<Vec<u8>> {
/// https://www.rfc-editor.org/rfc/rfc8017#appendix-A.2.4
#[derive(Debug, Sequence)]
struct DigestInfo {
digest_algorithm: AlgorithmIdentifierOwned,
digest: OctetString,
}
let em_len = N::BIT_LENGTH / 8;
let null = Any::null();
let t = DigestInfo {
digest_algorithm: AlgorithmIdentifierOwned {
oid: rfc5912::ID_SHA_256,
parameters: Some(null),
},
digest,
};
let t = t.to_der()?;
RsaPublicKey::new(n, e).map_err(|_| Error::InvalidObject)
}
let ps = vec![0xff; em_len - t.len() - 3];
assert!(ps.len() >= 8, "spec violation");
/// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
/// ```text
/// ECParameters ::= CHOICE {
/// namedCurve OBJECT IDENTIFIER
/// -- implicitCurve NULL
/// -- specifiedCurve SpecifiedECDomain
/// }
/// ```
pub(super) fn ec_parameters(parameters: &Any<'_>) -> Result<AlgorithmId> {
let curve_oid = parameters.as_oid().map_err(|_| Error::InvalidObject)?;
match curve_oid.to_string().as_str() {
OID_NIST_P256 => Ok(AlgorithmId::EccP256),
OID_NIST_P384 => Ok(AlgorithmId::EccP384),
_ => Err(Error::AlgorithmError),
}
let mut out = Vec::with_capacity(em_len);
out.write(&[0x00, 0x01]).map_err(|_| Error::MemoryError)?;
out.write(&ps).map_err(|_| Error::MemoryError)?;
out.write(&[0x00]).map_err(|_| Error::MemoryError)?;
out.write(&t).map_err(|_| Error::MemoryError)?;
Ok(out)
}
}
mod write_pki {
use cookie_factory::{SerializeFn, WriteContext};
use rsa::{BigUint, PublicKeyParts, RsaPublicKey};
use std::io::Write;
use x509::der::write::{der_integer, der_sequence};
/// Encodes a usize as an ASN.1 integer using DER.
fn der_integer_biguint<'a, W: Write + 'a>(num: &'a BigUint) -> impl SerializeFn<W> + 'a {
move |w: WriteContext<W>| der_integer(&num.to_bytes_be())(w)
/// The entrypoint to sign data with the yubikey.
pub struct Signer<'y, KT: KeyType> {
yubikey: RefCell<&'y mut YubiKey>,
key: SlotId,
public_key: KT::VerifyingKey,
}
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
/// ```text
/// RSAPublicKey ::= SEQUENCE {
/// modulus INTEGER, -- n
/// publicExponent INTEGER -- e
/// }
/// ```
pub(super) fn rsa_pubkey<'a, W: Write + 'a>(
pubkey: &'a RsaPublicKey,
) -> impl SerializeFn<W> + 'a {
der_sequence((
der_integer_biguint(pubkey.n()),
der_integer_biguint(pubkey.e()),
))
impl<'y, KT: KeyType> Signer<'y, KT> {
/// Create new Signer
pub fn new(
yubikey: &'y mut YubiKey,
key: SlotId,
subject_pki: SubjectPublicKeyInfoRef<'_>,
) -> Result<Self> {
let public_key = KT::PublicKey::try_from(subject_pki).map_err(|_| Error::ParseError)?;
let public_key = public_key.into();
Ok(Self {
yubikey: RefCell::new(yubikey),
key,
public_key,
})
}
}
impl<KT: KeyType> Keypair for Signer<'_, KT> {
type VerifyingKey = KT::VerifyingKey;
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
self.public_key.clone()
}
}
impl<KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'_, KT> {
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
self.verifying_key().signature_algorithm_identifier()
}
}
impl<KT: KeyType> signature::Signer<KT::Signature> for Signer<'_, KT> {
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
let data = KT::prepare(msg)?;
let out = sign_data(
&mut self.yubikey.borrow_mut(),
&data,
KT::ALGORITHM,
self.key,
)
.map_err(signature::Error::from_source)?;
let out = KT::read_signature(&out)?;
Ok(out)
}
}
}
+7 -11
View File
@@ -30,7 +30,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{Error, Result, YubiKey};
use crate::{Result, YubiKey};
use std::fmt::{self, Debug, Display};
use uuid::Uuid;
@@ -61,6 +61,7 @@ const OBJ_CHUID: u32 = 0x005f_c102;
/// - 0x35: Exp. Date (hard-coded)
/// - 0x3e: Signature (hard-coded, empty)
/// - 0xfe: Error Detection Code (hard-coded)
#[allow(dead_code)]
const CHUID_TMPL: &[u8] = &[
0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58,
0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00,
@@ -86,12 +87,13 @@ impl ChuId {
pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] {
self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)]
.try_into()
.unwrap()
.expect("should be FASCN_SIZE")
}
/// Return Card UUID/GUID component of CHUID
pub fn uuid(&self) -> Uuid {
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)]).unwrap()
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)])
.expect("should be UUID-sized")
}
/// Return expiration date component of CHUID
@@ -99,24 +101,18 @@ impl ChuId {
pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] {
self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)]
.try_into()
.unwrap()
.expect("should be EXPIRATION_SIZE")
}
/// Get Cardholder Unique Identifier (CHUID)
pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> {
let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(OBJ_CHUID)?;
if response.len() != CHUID_TMPL.len() {
return Err(Error::GenericError);
}
Ok(ChuId(response[..Self::BYTE_SIZE].try_into().unwrap()))
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
}
/// Set Cardholder Unique Identifier (CHUID)
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
let mut buf = CHUID_TMPL.to_vec();
buf[..Self::BYTE_SIZE].copy_from_slice(&self.0);
+1 -1
View File
@@ -111,7 +111,7 @@ impl Config {
error!("pin timestamp in admin metadata is an invalid size");
} else {
// TODO(tarcieri): double-check endianness is correct
let pin_last_changed = u32::from_le_bytes(item.try_into().unwrap());
let pin_last_changed = u32::from_le_bytes([item[0], item[1], item[2], item[3]]);
if pin_last_changed != 0 {
config.pin_last_changed =
+18 -1
View File
@@ -1,5 +1,7 @@
//! Miscellaneous constant values
#![allow(dead_code)]
/// YubiKey max buffer size
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_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
// Admin tags
@@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
// Protected tags
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
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;
+30 -2
View File
@@ -57,6 +57,9 @@ pub enum Error {
/// Authentication error
AuthenticationError,
/// Error while building a certificate
CertificateBuilder,
/// Generic error
GenericError,
@@ -132,10 +135,11 @@ impl Error {
Error::AlgorithmError => f.write_str("algorithm error"),
Error::AppletError => f.write_str("applet error"),
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::AuthenticationError => f.write_str("authentication error"),
Error::CertificateBuilder => f.write_str("certificate builder error"),
Error::GenericError => f.write_str("generic error"),
Error::InvalidObject => f.write_str("invalid object"),
Error::KeyError => f.write_str("key error"),
@@ -146,7 +150,7 @@ impl Error {
Error::PcscError {
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"),
@@ -164,6 +168,18 @@ impl Display for Error {
}
}
impl From<std::array::TryFromSliceError> for Error {
fn from(_: std::array::TryFromSliceError) -> Error {
Error::SizeError
}
}
impl From<std::time::SystemTimeError> for Error {
fn from(_: std::time::SystemTimeError) -> Error {
Error::GenericError
}
}
impl From<pcsc::Error> for Error {
fn from(err: pcsc::Error) -> Error {
Error::PcscError { inner: Some(err) }
@@ -179,3 +195,15 @@ impl std::error::Error for Error {
}
}
}
impl From<der::Error> for Error {
fn from(_err: der::Error) -> Error {
Error::ParseError
}
}
impl From<x509_cert::builder::Error> for Error {
fn from(_err: x509_cert::builder::Error) -> Error {
Error::CertificateBuilder
}
}
+11 -4
View File
@@ -2,9 +2,16 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
#![warn(
clippy::mod_module_files,
clippy::unwrap_used,
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
// Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/>
@@ -44,7 +51,7 @@ mod config;
mod consts;
mod error;
mod metadata;
mod mgm;
pub mod mgm;
#[cfg(feature = "untested")]
mod mscmap;
#[cfg(feature = "untested")]
@@ -64,7 +71,7 @@ pub use crate::{
chuid::ChuId,
config::Config,
error::{Error, Result},
mgm::{MgmKey, MgmType},
mgm::{MgmAlgorithmId, MgmKey, MgmType},
piv::Key,
policy::{PinPolicy, TouchPolicy},
reader::Context,
+14 -20
View File
@@ -30,16 +30,15 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::marker::PhantomData;
use std::{iter, marker::PhantomData};
use zeroize::Zeroizing;
use crate::{serialization::*, transaction::Transaction, Buffer, Error, Result};
#[cfg(feature = "untested")]
use crate::consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX};
#[cfg(feature = "untested")]
use std::iter;
use crate::{
consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX},
serialization::*,
transaction::Transaction,
Buffer, Error, Result,
};
const TAG_ADMIN: u8 = 0x80;
const TAG_PROTECTED: u8 = 0x88;
@@ -71,7 +70,7 @@ impl<T: MetadataType> Default for Metadata<T> {
fn default() -> Self {
Metadata {
inner: Zeroizing::new(vec![]),
_marker: PhantomData::default(),
_marker: PhantomData,
}
}
}
@@ -82,13 +81,11 @@ impl<T: MetadataType> Metadata<T> {
let data = txn.fetch_object(T::obj_id())?;
Ok(Metadata {
inner: Tlv::parse_single(data, T::tag())?,
_marker: PhantomData::default(),
_marker: PhantomData,
})
}
/// Write metadata
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> {
if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
return Err(Error::GenericError);
@@ -105,8 +102,6 @@ impl<T: MetadataType> Metadata<T> {
}
/// Delete metadata
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> {
txn.save_object(T::obj_id(), &[])
}
@@ -129,8 +124,6 @@ impl<T: MetadataType> Metadata<T> {
}
/// Set metadata item
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> {
let mut cb_temp: usize = 0;
let mut tag_temp: u8 = 0;
@@ -160,8 +153,10 @@ impl<T: MetadataType> Metadata<T> {
// We did not find an existing tag, append
assert_eq!(offset, self.inner.len());
self.inner
.extend(iter::repeat(0).take(1 + get_length_size(item.len()) + item.len()));
self.inner.extend(iter::repeat_n(
0,
1 + get_length_size(item.len()) + item.len(),
));
Tlv::write(&mut self.inner[offset..], tag, item)?;
return Ok(());
@@ -196,7 +191,7 @@ impl<T: MetadataType> Metadata<T> {
// Move remaining data
let orig_len = self.inner.len();
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(
next_offset..orig_len,
@@ -217,7 +212,6 @@ impl<T: MetadataType> Metadata<T> {
}
/// Get the size of a length tag for the given length
#[cfg(feature = "untested")]
fn get_length_size(length: usize) -> usize {
if length < 0x80 {
1
+778 -179
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -41,7 +41,6 @@ const TAG_MSCMAP: u8 = 0x81;
///
/// Defined in Microsoft's Smart Card Minidriver Specification:
/// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
#[derive(Clone, Debug)]
pub struct MsContainer {
/// Container name.
@@ -141,7 +140,7 @@ impl MsContainer {
let name_bytes_len = Self::NAME_LEN * 2;
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
name[i] = u16::from_le_bytes(chunk.try_into().unwrap());
name[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
}
let mut cert_fingerprint = [0u8; 20];
@@ -151,11 +150,10 @@ impl MsContainer {
name,
slot: bytes[name_bytes_len].try_into()?,
key_spec: bytes[name_bytes_len + 1],
key_size_bits: u16::from_le_bytes(
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)]
.try_into()
.unwrap(),
),
key_size_bits: u16::from_le_bytes([
bytes[name_bytes_len + 2],
bytes[name_bytes_len + 3],
]),
flags: bytes[name_bytes_len + 4],
pin_id: bytes[name_bytes_len + 5],
associated_echd_container: bytes[name_bytes_len + 6],
@@ -184,7 +182,10 @@ impl MsContainer {
bytes.push(self.pin_id);
bytes.push(self.associated_echd_container);
bytes.extend_from_slice(&self.cert_fingerprint);
bytes.as_slice().try_into().unwrap()
bytes
.as_slice()
.try_into()
.expect("should be REC_LEN-sized")
}
}
+3 -5
View File
@@ -57,7 +57,6 @@ const TAG_MSROOTS_MID: u8 = 0x83;
///
/// For more information, see:
/// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub struct MsRoots(Vec<u8>);
impl MsRoots {
@@ -97,10 +96,9 @@ impl MsRoots {
}
}
MsRoots::new(&data).map(Some).map_err(|e| {
error!("error parsing msroots: {:?}", e);
e
})
MsRoots::new(&data)
.map(Some)
.inspect_err(|e| error!("error parsing msroots: {:?}", e))
}
/// Write `msroots` file to YubiKey
+218 -114
View File
@@ -6,10 +6,10 @@
//! Supported algorithms:
//!
//! - **Encryption**:
//! - RSA: `RSA1024`, `RSA2048`
//! - RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
//! - **Signatures**:
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
// Adapted from yubico-piv-tool:
@@ -44,35 +44,38 @@
use crate::{
apdu::{Ins, StatusWords},
certificate::{self, Certificate, PublicKeyInfo},
consts::CB_OBJ_MAX,
certificate::{self, Certificate},
error::{Error, Result},
mgm::MgmAlgorithmId,
policy::{PinPolicy, TouchPolicy},
serialization::*,
setting,
yubikey::YubiKey,
Buffer, ObjectId,
};
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
use elliptic_curve::{sec1::Sec1Point as EcPublicKey, PublicKey};
use log::{debug, error, warn};
use p256::NistP256;
use p384::NistP384;
use rsa::{BigUint, RsaPublicKey};
use rsa::{pkcs8::EncodePublicKey, BoxedUint, RsaPublicKey};
use std::{
fmt::{Display, Formatter},
str::FromStr,
};
#[cfg(feature = "untested")]
use {
num_bigint_dig::traits::ModInverse,
num_integer::Integer,
num_traits::{FromPrimitive, One},
use x509_cert::{
der::{asn1::BitString, Decode},
spki::{AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfoOwned},
};
#[cfg(feature = "untested")]
use zeroize::Zeroizing;
#[cfg(feature = "untested")]
use crate::consts::CB_OBJ_MAX;
#[cfg(feature = "untested")]
use rsa::{traits::PrivateKeyParts, RsaPrivateKey};
/// PIV Applet Name
pub(crate) const APPLET_NAME: &str = "PIV";
@@ -86,6 +89,9 @@ const TAG_RSA_MODULUS: u8 = 0x81;
const TAG_RSA_EXP: u8 = 0x82;
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")]
const KEYDATA_LEN: usize = 1024;
@@ -171,9 +177,9 @@ impl From<SlotId> for u8 {
impl Display for SlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SlotId::Management(r) => write!(f, "{:?}", r),
SlotId::Retired(r) => write!(f, "{:?}", r),
_ => write!(f, "{:?}", self),
SlotId::Management(r) => write!(f, "{r:?}"),
SlotId::Retired(r) => write!(f, "{r:?}"),
_ => write!(f, "{self:?}"),
}
}
}
@@ -326,7 +332,7 @@ impl From<RetiredSlotId> for u8 {
impl Display for RetiredSlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
write!(f, "{self:?}")
}
}
@@ -409,7 +415,7 @@ impl From<ManagementSlotId> for u8 {
impl Display for ManagementSlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
write!(f, "{self:?}")
}
}
@@ -441,10 +447,11 @@ impl ManagementSlotId {
}
/// Personal Identity Verification (PIV) key slots
pub const SLOTS: [SlotId; 27] = [
pub const SLOTS: [SlotId; 28] = [
SlotId::Authentication,
SlotId::Signature,
SlotId::KeyManagement,
SlotId::Attestation,
SlotId::Retired(RetiredSlotId::R1),
SlotId::Retired(RetiredSlotId::R2),
SlotId::Retired(RetiredSlotId::R3),
@@ -480,11 +487,23 @@ pub enum AlgorithmId {
/// 2048-bit RSA.
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.
EccP256,
/// ECDSA with the NIST P384 curve.
EccP384,
/// ED25519
Ed25519,
/// X25519
X25519,
}
impl TryFrom<u8> for AlgorithmId {
@@ -494,8 +513,12 @@ impl TryFrom<u8> for AlgorithmId {
match value {
0x06 => Ok(AlgorithmId::Rsa1024),
0x07 => Ok(AlgorithmId::Rsa2048),
0x05 => Ok(AlgorithmId::Rsa3072),
0x16 => Ok(AlgorithmId::Rsa4096),
0x11 => Ok(AlgorithmId::EccP256),
0x14 => Ok(AlgorithmId::EccP384),
0xE0 => Ok(AlgorithmId::Ed25519),
0xE1 => Ok(AlgorithmId::X25519),
_ => Err(Error::AlgorithmError),
}
}
@@ -506,8 +529,12 @@ impl From<AlgorithmId> for u8 {
match id {
AlgorithmId::Rsa1024 => 0x06,
AlgorithmId::Rsa2048 => 0x07,
AlgorithmId::Rsa3072 => 0x05,
AlgorithmId::Rsa4096 => 0x16,
AlgorithmId::EccP256 => 0x11,
AlgorithmId::EccP384 => 0x14,
AlgorithmId::Ed25519 => 0xE0,
AlgorithmId::X25519 => 0xE1,
}
}
}
@@ -519,22 +546,29 @@ impl AlgorithmId {
}
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn get_elem_len(self) -> usize {
match self {
AlgorithmId::Rsa1024 => 64,
AlgorithmId::Rsa2048 => 128,
AlgorithmId::Rsa3072 => 192,
AlgorithmId::Rsa4096 => 256,
AlgorithmId::EccP256 => 32,
AlgorithmId::EccP384 => 48,
AlgorithmId::Ed25519 => 32,
AlgorithmId::X25519 => 32,
}
}
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn get_param_tag(self) -> u8 {
match self {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => 0x01,
AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => 0x01,
AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6,
AlgorithmId::Ed25519 => 0x07,
AlgorithmId::X25519 => 0x08,
}
}
}
@@ -591,7 +625,7 @@ pub fn generate(
algorithm: AlgorithmId,
pin_policy: PinPolicy,
touch_policy: TouchPolicy,
) -> Result<PublicKeyInfo> {
) -> Result<SubjectPublicKeyInfoOwned> {
// Keygen messages
// TODO(tarcieri): extract these into an I18N-handling type?
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
@@ -606,7 +640,10 @@ pub fn generate(
let setting_roca: setting::Setting;
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => {
if yubikey.version.major == 4
&& (yubikey.version.minor < 3
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5))
@@ -701,7 +738,6 @@ pub fn generate(
}
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn write_key(
yubikey: &mut YubiKey,
slot: SlotId,
@@ -750,7 +786,6 @@ fn write_key(
/// The key data that makes up an RSA key.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub struct RsaKeyData {
/// The secret prime `p`.
p: Buffer,
@@ -766,41 +801,51 @@ pub struct RsaKeyData {
#[cfg(feature = "untested")]
impl RsaKeyData {
/// Generates a new RSA key data set from two randomly generated, secret, primes.
/// Generates a new RSA key data set from two (randomly generated) secret primes.
///
/// Panics if `secret_p` or `secret_q` are invalid primes.
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Self {
let p = BigUint::from_bytes_be(secret_p);
let q = BigUint::from_bytes_be(secret_q);
/// # Returns
/// - `Ok(key_data)` if `secret_p` and `secret_q` are valid primes.
/// - `Err(Error::AlgorithmError)` if `secret_p`/`secret_q` are invalid primes.
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Result<Self> {
let p = BoxedUint::from_be_slice_vartime(secret_p);
let q = BoxedUint::from_be_slice_vartime(secret_q);
let exp = BoxedUint::from(KEYDATA_RSA_EXP);
let totient = {
let p_t = &p - BigUint::one();
let q_t = &p - BigUint::one();
let mut private_key = RsaPrivateKey::from_p_q(p.clone(), q.clone(), exp)
.map_err(|_| Error::AlgorithmError)?;
private_key
.precompute()
.map_err(|_| Error::AlgorithmError)?;
p_t.lcm(&q_t)
};
let exp = BigUint::from_u64(KEYDATA_RSA_EXP).unwrap();
let d = exp.mod_inverse(&totient).unwrap();
let d = d.to_biguint().unwrap();
// We calculate the optimization values ahead of time, instead of making the user
// do so.
let dp = &d % (&p - BigUint::one());
let dq = &d % (&q - BigUint::one());
let qinv = q.clone().mod_inverse(&p).unwrap();
let (_, qinv) = qinv.to_bytes_be();
RsaKeyData {
p: Zeroizing::new(p.to_bytes_be()),
q: Zeroizing::new(q.to_bytes_be()),
dp: Zeroizing::new(dp.to_bytes_be()),
dq: Zeroizing::new(dq.to_bytes_be()),
qinv: Zeroizing::new(qinv),
}
Ok(RsaKeyData {
p: Zeroizing::new(p.to_be_bytes().to_vec()),
q: Zeroizing::new(q.to_be_bytes().to_vec()),
dp: Zeroizing::new(
private_key
.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(),
),
})
}
fn total_len(&self) -> usize {
@@ -810,9 +855,8 @@ impl RsaKeyData {
/// 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_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn import_rsa_key(
yubikey: &mut YubiKey,
slot: SlotId,
@@ -822,7 +866,10 @@ pub fn import_rsa_key(
pin_policy: PinPolicy,
) -> Result<()> {
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => (),
AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => (),
_ => return Err(Error::AlgorithmError),
}
@@ -847,7 +894,6 @@ pub fn import_rsa_key(
///
/// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn import_ecc_key(
yubikey: &mut YubiKey,
slot: SlotId,
@@ -872,11 +918,38 @@ pub fn import_ecc_key(
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.
///
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer> {
let templ = [0, Ins::Attest.code(), key.into(), 0];
let txn = yubikey.begin_transaction()?;
@@ -912,7 +985,6 @@ pub fn sign_data(
/// Decrypt data using a PIV key.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn decrypt_data(
yubikey: &mut YubiKey,
input: &[u8],
@@ -928,34 +1000,21 @@ pub fn decrypt_data(
/// Read metadata
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
let txn = yubikey.begin_transaction()?;
let templ = [0, Ins::GetMetadata.code(), 0, slot.into()];
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?;
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)
txn.get_metadata(slot)
}
/// Metadata from a slot
#[derive(Debug)]
pub struct SlotMetadata {
/// Algorithm / Type of key
pub algorithm: ManagementAlgorithmId,
pub algorithm: SlotAlgorithmId,
/// PIN and touch policy
pub policy: Option<(PinPolicy, TouchPolicy)>,
/// Imported or generated key
pub origin: Option<Origin>,
/// Pub key of the key
pub public: Option<PublicKeyInfo>,
pub public: Option<SubjectPublicKeyInfoOwned>,
/// Whether PIN PUK and management key are default
pub default: Option<bool>,
/// Number of retries left
@@ -970,13 +1029,14 @@ impl TryFrom<Buffer> for SlotMetadata {
combinator::{eof, map_res},
multi::fold_many1,
number::complete::u8,
Parser,
};
let out = fold_many1(
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|| {
Ok(SlotMetadata {
algorithm: ManagementAlgorithmId::PinPuk,
algorithm: SlotAlgorithmId::PinPuk,
policy: None,
origin: None,
public: None,
@@ -987,15 +1047,15 @@ impl TryFrom<Buffer> for SlotMetadata {
|acc: Result<SlotMetadata>, tlv| match acc {
Ok(mut metadata) => match tlv.tag {
1 => {
metadata.algorithm = ManagementAlgorithmId::try_from(tlv.value[0])?;
metadata.algorithm = SlotAlgorithmId::try_from(tlv.value[0])?;
Ok(metadata)
}
2 => {
fn policy_parser(
i: &[u8],
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
let (i, pin) = map_res(u8, PinPolicy::try_from)(i)?;
let (i, touch) = map_res(u8, TouchPolicy::try_from)(i)?;
let (i, pin) = map_res(u8, PinPolicy::try_from).parse(i)?;
let (i, touch) = map_res(u8, TouchPolicy::try_from).parse(i)?;
let (i, _) = eof(i)?;
Ok((i, (pin, touch)))
@@ -1007,7 +1067,7 @@ impl TryFrom<Buffer> for SlotMetadata {
}
3 => {
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)?;
Ok((i, origin))
@@ -1019,7 +1079,7 @@ impl TryFrom<Buffer> for SlotMetadata {
}
4 => {
match metadata.algorithm {
ManagementAlgorithmId::Asymmetric(alg) => {
SlotAlgorithmId::Asymmetric(alg) => {
metadata.public = Some(read_public_key(alg, tlv.value, false)?);
}
_ => Err(Error::ParseError)?,
@@ -1067,7 +1127,8 @@ impl TryFrom<Buffer> for SlotMetadata {
},
err => err,
},
)(buf.as_ref());
)
.parse(buf.as_ref());
match out {
Ok((_, res)) => res,
@@ -1110,12 +1171,45 @@ fn read_public_key(
algorithm: AlgorithmId,
input: &[u8],
skip_asn1_tag: bool,
) -> Result<PublicKeyInfo> {
) -> Result<SubjectPublicKeyInfoOwned> {
// TODO(str4d): Response is wrapped in an ASN.1 TLV:
//
// 0x7f 0x49 -> Application | Constructed | 0x49
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
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
//
@@ -1159,14 +1253,17 @@ fn read_public_key(
}
let exp = exp_tlv.value.to_vec();
Ok(PublicKeyInfo::Rsa {
algorithm,
pubkey: RsaPublicKey::new(
BigUint::from_bytes_be(&modulus),
BigUint::from_bytes_be(&exp),
let pubkey = RsaPublicKey::new(
BoxedUint::from_be_slice_vartime(&modulus),
BoxedUint::from_be_slice_vartime(&exp),
)
.map_err(|_| Error::InvalidObject)?,
})
.map_err(|_| Error::InvalidObject)?;
Ok(SubjectPublicKeyInfoOwned::from_der(
pubkey
.to_public_key_der()
.map_err(|_| Error::ParseError)?
.as_bytes(),
)?)
}
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
@@ -1194,49 +1291,56 @@ fn read_public_key(
let point = tlv.value.to_vec();
match algorithm {
AlgorithmId::EccP256 => {
EcPublicKey::<NistP256>::from_bytes(point).map(PublicKeyInfo::EcP256)
}
AlgorithmId::EccP384 => {
EcPublicKey::<NistP384>::from_bytes(point).map(PublicKeyInfo::EcP384)
}
let pubkey = match algorithm {
AlgorithmId::EccP256 => PublicKey::<NistP256>::try_from(
EcPublicKey::<NistP256>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der(),
AlgorithmId::EccP384 => PublicKey::<NistP384>::try_from(
EcPublicKey::<NistP384>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der(),
_ => return Err(Error::AlgorithmError),
}
.map_err(|_| Error::InvalidObject)
.map_err(|_| Error::InvalidObject)?;
Ok(SubjectPublicKeyInfoOwned::from_der(pubkey.as_bytes())?)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Algorithms as reported by the metadata command.
pub enum ManagementAlgorithmId {
pub enum SlotAlgorithmId {
/// Used on PIN and PUK slots.
PinPuk,
/// Used on the key management slot.
ThreeDes,
Management(MgmAlgorithmId),
/// Used on all other slots.
Asymmetric(AlgorithmId),
}
impl TryFrom<u8> for ManagementAlgorithmId {
impl TryFrom<u8> for SlotAlgorithmId {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0xff => Ok(ManagementAlgorithmId::PinPuk),
0x03 => Ok(ManagementAlgorithmId::ThreeDes),
oth => AlgorithmId::try_from(oth).map(ManagementAlgorithmId::Asymmetric),
0xff => Ok(SlotAlgorithmId::PinPuk),
oth => MgmAlgorithmId::try_from(oth)
.map(SlotAlgorithmId::Management)
.or_else(|_| AlgorithmId::try_from(oth).map(SlotAlgorithmId::Asymmetric)),
}
}
}
impl From<ManagementAlgorithmId> for u8 {
fn from(id: ManagementAlgorithmId) -> u8 {
impl From<SlotAlgorithmId> for u8 {
fn from(id: SlotAlgorithmId) -> u8 {
match id {
ManagementAlgorithmId::PinPuk => 0xff,
ManagementAlgorithmId::ThreeDes => 0x03,
ManagementAlgorithmId::Asymmetric(oth) => oth.into(),
SlotAlgorithmId::PinPuk => 0xff,
SlotAlgorithmId::Management(oth) => oth.into(),
SlotAlgorithmId::Asymmetric(oth) => oth.into(),
}
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
//! Support for enumerating available PC/SC card readers.
use crate::{Result, YubiKey};
use crate::{Error, Result, YubiKey};
use std::{
borrow::Cow,
ffi::CStr,
@@ -43,7 +43,8 @@ impl Context {
let Self { ctx, reader_names } = self;
let reader_cstrs: Vec<_> = {
let c = ctx.lock().unwrap();
// TODO(tarcieri): better error?
let c = ctx.lock().map_err(|_| Error::GenericError)?;
// ensure PC/SC context is valid
c.is_valid()?;
@@ -90,7 +91,8 @@ impl<'ctx> Reader<'ctx> {
/// Connect to this reader, returning its `pcsc::Card`.
pub(crate) fn connect(&self) -> Result<pcsc::Card> {
let ctx = self.ctx.lock().unwrap();
// TODO(tarcieri): better error?
let ctx = self.ctx.lock().map_err(|_| Error::GenericError)?;
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
}
}
+3 -8
View File
@@ -41,7 +41,7 @@ use std::{
};
/// Source of how a setting was configured.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SettingSource {
/// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars.
User,
@@ -51,15 +51,10 @@ pub enum SettingSource {
Admin,
/// Default setting.
#[default]
Default,
}
impl Default for SettingSource {
fn default() -> Self {
Self::Default
}
}
/// Setting booleans: configuration values sourced from a file or the environment.
///
/// 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
fn from_env(key: &str) -> Option<Self> {
env::var(format!("YUBIKEY_PIV_{}", key))
env::var(format!("YUBIKEY_PIV_{key}"))
.ok()
.map(|value| Setting {
source: SettingSource::User,
+153 -70
View File
@@ -5,6 +5,7 @@ use crate::{
apdu::{Apdu, Ins, StatusWords},
consts::{CB_BUF_MAX, CB_OBJ_MAX},
error::{Error, Result},
mgm::MgmKey,
otp,
piv::{self, AlgorithmId, SlotId},
serialization::*,
@@ -15,7 +16,7 @@ use log::{error, trace};
use zeroize::Zeroizing;
#[cfg(feature = "untested")]
use crate::mgm::{MgmKey, DES_LEN_3DES};
use crate::mgm::{DeviceConfig, DeviceInfo, Lock};
const CB_PIN_MAX: usize = 8;
@@ -60,26 +61,32 @@ impl<'tx> Transaction<'tx> {
Ok(recv_buffer)
}
/// Select PIV application.
pub fn select_piv_application(&self) -> Result<()> {
self.select_application(
piv::APPLET_ID,
piv::APPLET_NAME,
"failed selecting application",
)
}
/// Select application.
pub fn select_application(&self) -> Result<()> {
pub fn select_application(
&self,
applet: &[u8],
applet_name: &'static str,
error: &'static str,
) -> Result<()> {
let response = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(piv::APPLET_ID)
.data(applet)
.transmit(self, 0xFF)
.map_err(|e| {
error!("failed communicating with card: '{}'", e);
e
})?;
.inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
if !response.is_success() {
error!(
"failed selecting application: {:04x}",
response.status_words().code()
);
error!("{}: {:04x}", error, response.status_words().code());
return Err(match response.status_words() {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: piv::APPLET_NAME,
},
StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
_ => Error::GenericError,
});
}
@@ -92,15 +99,11 @@ impl<'tx> Transaction<'tx> {
// get version from device
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);
}
if response.data().len() < 3 {
return Err(Error::SizeError);
}
Ok(Version::new(response.data()[..3].try_into().unwrap()))
Ok(response.data()[..3].try_into().map(Version::new)?)
}
/// Get YubiKey device serial number.
@@ -108,21 +111,11 @@ impl<'tx> Transaction<'tx> {
match version.major {
// YK4 requires switching to the YK applet to retrieve the serial
4 => {
let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(otp::APPLET_ID)
.transmit(self, 0xFF)?
.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,
});
}
self.select_application(
otp::APPLET_ID,
otp::APPLET_NAME,
"failed selecting yk application",
)?;
let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
@@ -136,21 +129,11 @@ impl<'tx> Transaction<'tx> {
}
// reselect the PIV applet
let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(piv::APPLET_ID)
.transmit(self, 0xFF)?
.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,
});
}
self.select_application(
piv::APPLET_ID,
piv::APPLET_NAME,
"failed selecting application",
)?;
response.data().try_into()
}
@@ -175,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.
pub fn verify_pin(&self, pin: &[u8]) -> Result<()> {
if pin.len() > CB_PIN_MAX {
@@ -190,7 +191,7 @@ impl<'tx> Transaction<'tx> {
if !pin.is_empty() {
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
data[0..pin.len()].copy_from_slice(pin);
query.data(data.as_ref());
query.data(data.as_slice());
}
let response = query.transmit(self, 261)?;
@@ -247,15 +248,14 @@ impl<'tx> Transaction<'tx> {
}
/// Set the management key (MGM).
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
let p2 = if require_touch { 0xfe } else { 0xff };
let mut data = [0u8; DES_LEN_3DES + 3];
data[0] = ALGO_3DES;
data[1] = KEY_CARDMGM;
data[2] = DES_LEN_3DES as u8;
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref());
let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
data.push(new_key.algorithm_id().into());
data.push(KEY_CARDMGM);
data.push(new_key.key_size());
data.extend_from_slice(new_key.as_ref());
let status_words = Apdu::new(Ins::SetMgmKey)
.params(0xff, p2)
@@ -288,14 +288,23 @@ impl<'tx> Transaction<'tx> {
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
let key_len = if let AlgorithmId::Rsa1024 = algorithm {
128
} else {
256
};
if in_len != key_len {
AlgorithmId::Rsa1024 => {
if in_len != 128 {
return Err(Error::SizeError);
}
}
AlgorithmId::Rsa2048 => {
if in_len != 256 {
return Err(Error::SizeError);
}
}
AlgorithmId::Rsa3072 => {
if in_len != 384 {
return Err(Error::SizeError);
}
}
AlgorithmId::Rsa4096 => {
if in_len != 512 {
return Err(Error::SizeError);
}
}
@@ -311,6 +320,19 @@ impl<'tx> Transaction<'tx> {
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 {
@@ -327,7 +349,9 @@ impl<'tx> Transaction<'tx> {
Tlv::write(
&mut buf[2..],
match (algorithm, decipher) {
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85,
(AlgorithmId::EccP256, true)
| (AlgorithmId::EccP384, true)
| (AlgorithmId::X25519, true) => 0x85,
_ => 0x81,
},
sign_in
@@ -339,10 +363,7 @@ impl<'tx> Transaction<'tx> {
let response = self
.transfer_data(&templ, &indata[..offset], 1024)
.map_err(|e| {
error!("sign command failed to communicate: {}", e);
e
})?;
.inspect_err(|e| error!("sign command failed to communicate: {}", e))?;
if !response.is_success() {
error!("failed sign command with code {:x}", response.code());
@@ -515,4 +536,66 @@ impl<'tx> Transaction<'tx> {
_ => 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)
}
}
+133 -79
View File
@@ -41,10 +41,12 @@ use crate::{
reader::{Context, Reader},
transaction::Transaction,
};
use cipher::common::getrandom::SysRng;
use log::{error, info};
use pcsc::{Card, Disposition};
use rand_core::{OsRng, RngCore};
use pcsc::Card;
use rand_core::TryRng;
use std::{
cmp::{Ord, Ordering},
fmt::{self, Display},
str::FromStr,
};
@@ -59,7 +61,6 @@ use {
transaction::ChangeRefAction,
Buffer, ObjectId,
},
secrecy::ExposeSecret,
std::time::{SystemTime, UNIX_EPOCH},
};
@@ -67,6 +68,7 @@ use {
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
/// 3DES authentication
#[cfg(feature = "untested")]
pub(crate) const ALGO_3DES: u8 = 0x03;
/// Card management key
@@ -75,10 +77,11 @@ pub(crate) const KEY_CARDMGM: u8 = 0x9b;
const TAG_DYN_AUTH: u8 = 0x7c;
/// 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.
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct Serial(pub u32);
impl From<u32> for Serial {
@@ -143,6 +146,22 @@ impl Version {
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 {
@@ -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.
///
/// Almost all functionality in this library will require an open session
@@ -178,28 +230,45 @@ impl fmt::Debug for YubiKey {
impl YubiKey {
/// Open a connection to a YubiKey.
///
/// Returns an error if there is more than one YubiKey detected.
/// Returns an error if more than one YubiKey is detected (or none at all).
///
/// NOTE: If multiple YubiKeys are connected, but we are only able to
/// open one of them (e.g. because the other one is in use, and the
/// connection doesn't allow sharing), the YubiKey that we were able to
/// open is returned.
///
/// If you need to operate in environments with more than one YubiKey
/// attached to the same system, use [`YubiKey::open_by_serial`] or
/// [`yubikey::reader::Context`][`Context`] to select from the available
/// PC/SC readers.
pub fn open() -> Result<Self> {
let mut readers = Context::open()?;
let mut reader_iter = readers.iter()?;
let mut yubikey: Option<Self> = None;
let mut readers = Context::open()?;
for reader in readers.iter()? {
if let Ok(yk_found) = reader.open() {
if let Some(yk_stored) = yubikey {
// We found two YubiKeys, so we won't use either.
// Don't reset them.
let _ = yk_stored.disconnect(pcsc::Disposition::LeaveCard);
let _ = yk_found.disconnect(pcsc::Disposition::LeaveCard);
if let Some(reader) = reader_iter.next() {
if reader_iter.next().is_some() {
error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
} else {
yubikey = Some(yk_found);
}
}
}
return reader.open();
}
if let Some(yubikey) = yubikey {
// We found exactly one YubiKey that we could open, so we return it.
Ok(yubikey)
} else {
error!("no YubiKey detected!");
Err(Error::NotFound)
}
}
/// Open a YubiKey with a specific serial number.
pub fn open_by_serial(serial: Serial) -> Result<Self> {
@@ -243,7 +312,6 @@ impl YubiKey {
/// Reconnect to a YubiKey.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reconnect(&mut self) -> Result<()> {
info!("trying to reconnect to current reader");
@@ -253,13 +321,10 @@ impl YubiKey {
pcsc::Disposition::ResetCard,
)?;
let pin = self
.pin
.as_ref()
.map(|p| Buffer::new(p.expose_secret().clone()));
let pin = self.pin.as_ref().map(|p| Buffer::new(p.clone()));
let txn = Transaction::new(&mut self.card)?;
txn.select_application()?;
txn.select_piv_application()?;
if let Some(p) = &pin {
txn.verify_pin(p)?;
@@ -277,7 +342,10 @@ impl YubiKey {
/// `YubiKey` implements `Drop` which automatically disconnects the card using
/// `Disposition::ResetCard`; you only need to call this function if you want to
/// handle errors or use a different disposition method.
pub fn disconnect(self, disposition: Disposition) -> core::result::Result<(), (Self, Error)> {
pub fn disconnect(
self,
disposition: pcsc::Disposition,
) -> core::result::Result<(), (Self, Error)> {
let Self {
card,
name,
@@ -341,37 +409,46 @@ impl YubiKey {
}
/// 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()?;
// get a challenge from the card
let challenge = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM)
let card_response = Apdu::new(Ins::Authenticate)
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data([TAG_DYN_AUTH, 0x02, 0x80, 0x00])
.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);
}
// send a response to the cards challenge and a challenge of our own.
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap());
let card_challenge = mgm_key.card_challenge(&card_response.data()[4..])?;
let challenge_len = card_challenge.len();
let mut data = [0u8; 22];
data[0] = TAG_DYN_AUTH;
data[1] = 20; // 2 + 8 + 2 +8
data[2] = 0x80;
data[3] = 8;
data[4..12].copy_from_slice(&response);
data[12] = 0x81;
data[13] = 8;
OsRng.fill_bytes(&mut data[14..22]);
// If this exceeds a `u8` then the card is giving us unexpected data.
let auth_len = (2 + challenge_len + 2 + challenge_len)
.try_into()
.map_err(|_| Error::AuthenticationError)?;
let mut challenge = [0u8; 8];
challenge.copy_from_slice(&data[14..22]);
let mut data = Vec::with_capacity(4 + challenge_len + 2 + challenge_len);
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)
.params(ALGO_3DES, KEY_CARDMGM)
.params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data(data)
.transmit(&txn, 261)?;
@@ -380,14 +457,7 @@ impl YubiKey {
}
// compare the response from the card with our challenge
let response = mgm_key.encrypt(&challenge);
use subtle::ConstantTimeEq;
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
return Err(Error::AuthenticationError);
}
Ok(())
mgm_key.check_challenge(&host_challenge, &authentication.data()[4..])
}
/// Get the PIV keys contained in this YubiKey.
@@ -397,7 +467,6 @@ impl YubiKey {
/// Deauthenticate.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn deauthenticate(&mut self) -> Result<()> {
let txn = self.begin_transaction()?;
@@ -431,7 +500,7 @@ impl YubiKey {
}
if !pin.is_empty() {
self.pin = Some(CachedPin::new(pin.into()))
self.pin = Some(pin.into())
}
Ok(())
@@ -444,7 +513,7 @@ impl YubiKey {
// Force a re-select to unverify, because once verified the spec dictates that
// subsequent verify calls will return a "verification not needed" instead of
// the number of tries left...
txn.select_application()?;
txn.select_piv_application()?;
// WRONG_PIN is expected on successful query.
match txn.verify_pin(&[]) {
@@ -456,7 +525,6 @@ impl YubiKey {
/// Set the number of PIN retries.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> {
// Special case: if either retry count is 0, it's a successful no-op
if pin_tries == 0 || puk_tries == 0 {
@@ -481,7 +549,6 @@ impl YubiKey {
///
/// The default PIN code is `123456`.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> {
{
let txn = self.begin_transaction()?;
@@ -489,7 +556,7 @@ impl YubiKey {
}
if !new_pin.is_empty() {
self.pin = Some(CachedPin::new(new_pin.into()));
self.pin = Some(new_pin.into());
}
Ok(())
@@ -497,7 +564,6 @@ impl YubiKey {
/// Set PIN last changed.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> {
let txn = yubikey.begin_transaction()?;
@@ -505,22 +571,17 @@ impl YubiKey {
// TODO(tarcieri): double check this is little endian
let tnow = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.duration_since(UNIX_EPOCH)?
.as_secs()
.to_le_bytes();
admin_data
.set_item(TAG_ADMIN_TIMESTAMP, &tnow)
.map_err(|e| {
error!("could not set pin timestamp, err = {}", e);
e
})?;
.inspect_err(|e| error!("could not set pin timestamp, err = {}", e))?;
admin_data.write(&txn).map_err(|e| {
error!("could not write admin data, err = {}", e);
e
})?;
admin_data
.write(&txn)
.inspect_err(|e| error!("could not write admin data, err = {}", e))?;
Ok(())
}
@@ -533,7 +594,6 @@ impl YubiKey {
///
/// The default PUK code is `12345678`.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> {
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
@@ -541,7 +601,6 @@ impl YubiKey {
/// Block PUK: permanently prevent the PIN from becoming unblocked.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn block_puk(&mut self) -> Result<()> {
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
let mut tries_remaining: i32 = -1;
@@ -572,7 +631,7 @@ impl YubiKey {
// Attempt to set the "PUK blocked" flag in admin data.
let mut admin_data = AdminData::read(&txn)
.map(|data| {
.inspect(|data| {
if let Ok(item) = data.get_item(TAG_ADMIN_FLAGS_1) {
if item.len() == flags.len() {
flags.copy_from_slice(item)
@@ -584,8 +643,6 @@ impl YubiKey {
);
}
}
data
})
.unwrap_or_default();
@@ -605,7 +662,6 @@ impl YubiKey {
/// Unblock a Personal Identification Number (PIN) using a previously
/// configured PIN Unblocking Key (PUK).
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> {
let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
@@ -613,7 +669,6 @@ impl YubiKey {
/// Fetch an object from the YubiKey.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer> {
let txn = self.begin_transaction()?;
txn.fetch_object(object_id)
@@ -621,7 +676,6 @@ impl YubiKey {
/// Save an object.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> {
let txn = self.begin_transaction()?;
txn.save_object(object_id, indata)
@@ -629,7 +683,6 @@ impl YubiKey {
/// Get an auth challenge.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> {
let txn = self.begin_transaction()?;
@@ -642,12 +695,15 @@ impl YubiKey {
return Err(Error::AuthenticationError);
}
Ok(response.data()[4..12].try_into().unwrap())
Ok(response
.data()
.get(4..12)
.ok_or(Error::SizeError)?
.try_into()?)
}
/// Verify an auth response.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> {
let mut data = [0u8; 12];
data[0] = 0x7c;
@@ -678,7 +734,6 @@ impl YubiKey {
///
/// The reset function is only available when both pins are blocked.
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reset_device(&mut self) -> Result<()> {
let templ = [0, Ins::Reset.code(), 0, 0];
let txn = self.begin_transaction()?;
@@ -696,16 +751,15 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self> {
let mut card = reader.connect().map_err(|e| {
error!("error connecting to reader '{}': {}", reader.name(), e);
e
})?;
let mut card = reader
.connect()
.inspect_err(|e| error!("error connecting to reader '{}': {}", reader.name(), e))?;
info!("connected to reader: {}", reader.name());
let mut app_version_serial = || -> Result<(Version, Serial)> {
let txn = Transaction::new(&mut card)?;
txn.select_application()?;
txn.select_piv_application()?;
let v = txn.get_version()?;
let s = txn.get_serial(v)?;
+177 -93
View File
@@ -3,17 +3,16 @@
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
use cipher::common::{getrandom::SysRng, Generate};
use log::trace;
use once_cell::sync::Lazy;
use rand_core::{OsRng, RngCore};
use rsa::pkcs1v15;
use rsa::{pkcs1v15, RsaPublicKey};
use sha2::{Digest, Sha256};
use signature::hazmat::PrehashVerifier;
use std::{env, str::FromStr, sync::Mutex};
use x509::RelativeDistinguishedName;
use std::{env, str::FromStr, sync::Mutex, time::Duration};
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
use yubikey::{
certificate,
certificate::{Certificate, PublicKeyInfo},
certificate::{yubikey_signer, Certificate},
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
};
@@ -44,12 +43,15 @@ static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
#[test]
#[ignore]
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() {
Ok(cccid) => trace!("CCCID: {:?}", cccid),
Err(Error::NotFound) => trace!("CCCID not found"),
Err(err) => panic!("error getting CCCID: {:?}", err),
Err(err) => panic!("error getting CCCID: {err:?}"),
}
}
@@ -60,12 +62,15 @@ fn test_get_cccid() {
#[test]
#[ignore]
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() {
Ok(chuid) => trace!("CHUID: {:?}", chuid),
Err(Error::NotFound) => trace!("CHUID not found"),
Err(err) => panic!("error getting CHUID: {:?}", err),
Err(err) => panic!("error getting CHUID: {err:?}"),
}
}
@@ -76,7 +81,10 @@ fn test_get_chuid() {
#[test]
#[ignore]
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();
assert!(config_result.is_ok());
trace!("config: {:?}", config_result.unwrap());
@@ -89,7 +97,10 @@ fn test_get_config() {
#[test]
#[ignore]
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);
assert!(keys_result.is_ok());
trace!("keys: {:?}", keys_result.unwrap());
@@ -102,7 +113,10 @@ fn test_list_keys() {
#[test]
#[ignore]
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"123456").is_ok());
}
@@ -115,43 +129,52 @@ fn test_verify_pin() {
#[test]
#[ignore]
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!(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.
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();
assert!(yubikey.authenticate(MgmKey::default()).is_err());
assert!(yubikey.authenticate(protected.clone()).is_ok());
assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(&protected).is_ok());
// 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!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_err());
assert!(yubikey.authenticate(protected.clone()).is_err());
assert!(yubikey.authenticate(manual.clone()).is_ok());
assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(&manual).is_ok());
// Set back to the default management key.
assert!(MgmKey::set_default(&mut yubikey).is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(protected).is_err());
assert!(yubikey.authenticate(manual).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(&manual).is_err());
assert!(yubikey.authenticate(&default_key).is_ok());
}
//
// Certificate support
//
fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -159,25 +182,27 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
let generated = piv::generate(
&mut yubikey,
slot,
algorithm,
KT::ALGORITHM,
PinPolicy::Default,
TouchPolicy::Default,
)
.unwrap();
let mut serial = [0u8; 20];
OsRng.fill_bytes(&mut serial);
// 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
// we'll limit ourselves to 19 bytes serial.
let serial = <[u8; 19]>::generate();
let serial = SerialNumber::new(&serial[..]).expect("serial can't be more than 20 bytes long");
let validity = Validity::from_now(Duration::new(500000, 0)).unwrap();
// Generate a self-signed certificate for the new key.
let extensions: &[x509::Extension<'_, &[u64]>] = &[];
let cert_result = Certificate::generate_self_signed(
let cert_result = Certificate::generate_self_signed::<_, KT>(
&mut yubikey,
slot,
serial,
None,
&[RelativeDistinguishedName::common_name("testSubject")],
validity,
Name::from_str("CN=testSubject").expect("parse name"),
generated,
extensions,
|_builder| Ok(()),
);
assert!(cert_result.is_ok());
@@ -189,18 +214,16 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
#[test]
#[ignore]
fn generate_self_signed_rsa_cert() {
let cert = generate_self_signed_cert(AlgorithmId::Rsa1024);
let cert = generate_self_signed_cert::<yubikey_signer::YubiRsa<yubikey_signer::Rsa1024>>();
//
// Verify that the certificate is signed correctly
//
let pubkey = match cert.subject_pki() {
PublicKeyInfo::Rsa { pubkey, .. } => pkcs1v15::VerifyingKey::<Sha256>::from(pubkey.clone()),
_ => unreachable!(),
};
let pubkey = RsaPublicKey::try_from(cert.subject_pki()).expect("valid rsa key");
let pubkey = pkcs1v15::VerifyingKey::<Sha256>::new(pubkey);
let data = cert.as_ref();
let data = cert.cert.to_der().expect("serialize certificate");
let tbs_cert_len = u16::from_be_bytes(data[6..8].try_into().unwrap()) as usize;
let msg = &data[4..8 + tbs_cert_len];
let sig = pkcs1v15::Signature::try_from(&data[data.len() - 128..]).unwrap();
@@ -209,32 +232,82 @@ fn generate_self_signed_rsa_cert() {
assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
}
#[test]
#[ignore]
fn generate_rsa3072() {
let mut yubikey = YUBIKEY.lock().unwrap();
let version = yubikey.version();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
// Generate a new key in the selected slot.
let generated = piv::generate(
&mut yubikey,
slot,
AlgorithmId::Rsa3072,
PinPolicy::Default,
TouchPolicy::Default,
);
match generated {
Ok(key) => {
let pubkey = key.subject_public_key;
assert!(pubkey.bit_len() > 3072)
}
Err(e) => assert!((version.major, version.minor) < (5, 7) && e == Error::AlgorithmError),
}
}
#[test]
#[ignore]
fn generate_self_signed_ec_cert() {
let cert = generate_self_signed_cert(AlgorithmId::EccP256);
let cert = generate_self_signed_cert::<p256::NistP256>();
//
// Verify that the certificate is signed correctly
//
let pubkey = match cert.subject_pki() {
PublicKeyInfo::EcP256(pubkey) => pubkey,
_ => unreachable!(),
};
let vk = p256::ecdsa::VerifyingKey::try_from(cert.subject_pki()).expect("ecdsa key expected");
let data = cert.as_ref();
let data = cert.cert.to_der().expect("serialize certificate");
let tbs_cert_len = data[6] as usize;
let sig_algo_len = data[7 + tbs_cert_len + 1] as usize;
let sig_start = 7 + tbs_cert_len + 2 + sig_algo_len + 3;
let msg = &data[4..7 + tbs_cert_len];
let sig = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap();
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(pubkey.as_bytes()).unwrap();
use p256::ecdsa::signature::Verifier;
assert!(vk.verify(msg, &sig).is_ok());
}
#[test]
#[ignore]
fn generate_self_signed_cv_cert() {
let cert = generate_self_signed_cert::<ed25519_dalek::SigningKey>();
//
// Verify that the certificate is signed correctly
//
let pubkey =
ed25519_dalek::VerifyingKey::try_from(cert.subject_pki()).expect("ed25519 key expected");
let data = cert.cert.to_der().expect("serialize certificate");
let cert_len = data[2] as usize;
let tbs_cert_len = data[5] as usize;
let sig_algo_len: usize = 64;
let sig_start = cert_len - sig_algo_len + 3;
let msg = &data[3..6 + tbs_cert_len];
let sig =
ed25519_dalek::Signature::from_slice(&data[sig_start..sig_start + sig_algo_len]).unwrap();
use ed25519_dalek::Verifier;
assert!(pubkey.verify(msg, &sig).is_ok());
}
#[test]
#[ignore]
fn test_slot_id_display() {
@@ -269,11 +342,11 @@ fn test_slot_id_display() {
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Pin)),
"PIN"
"Pin"
);
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Puk)),
"PUK"
"Puk"
);
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Management)),
@@ -288,10 +361,14 @@ fn test_slot_id_display() {
#[test]
#[ignore]
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.authenticate(MgmKey::default()).is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -305,61 +382,68 @@ fn test_read_metadata() {
)
.unwrap();
let metadata = piv::metadata(&mut yubikey, slot).unwrap();
assert_eq!(metadata.public, Some(generated));
match piv::metadata(&mut yubikey, slot) {
Ok(metadata) => assert_eq!(metadata.public, Some(generated)),
Err(Error::NotSupported) => {
// Some YubiKeys don't support metadata
eprintln!("metadata not supported by this YubiKey");
}
Err(err) => panic!("{}", err),
}
}
#[test]
#[ignore]
fn test_serial_string_conversions() {
//2^152+1
let serial: [u8; 20] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01,
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,
];
let s = certificate::Serial::from(serial);
assert_eq!(
s.as_x509_int(),
"5708990770823839524233143877797980545530986497"
);
assert_eq!(
s.as_x509_hex(),
"01:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01"
);
for slot in slots_to_check {
let slot = SlotId::Retired(slot);
let serial2: [u8; 20] = [
0xA1, 0xF3, 0x02, 0x30, 0x76, 0x01, 0x32, 0x48, 0x09, 0x9C, 0x10, 0xAA, 0x3F, 0xA0, 0x54,
0x0D, 0xC0, 0xB7, 0x65, 0x01,
];
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),
}
}
let s2 = certificate::Serial::from(serial2);
assert_eq!(
s2.as_x509_int(),
"924566785900861696177829411010986812227211191553"
);
assert_eq!(
s2.as_x509_hex(),
"a1:f3:02:30:76:01:32:48:09:9c:10:aa:3f:a0:54:0d:c0:b7:65:01"
);
let serial3: [u8; 20] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0x3F, 0xA0, 0x54,
0x0D, 0xC0, 0xB7, 0x65, 0x01,
];
let s3 = certificate::Serial::from(serial3);
assert_eq!(s3.as_x509_int(), "3140531249369331492097");
assert_eq!(s3.as_x509_hex(), "aa:3f:a0:54:0d:c0:b7:65:01");
panic!("No empty slots to check");
}
#[test]
#[ignore]
fn test_parse_cert_from_der() {
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
let cert =
certificate::Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
let cert = Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
assert_eq!(
cert.subject(),
"CN=Bob",