Compare commits

...

97 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
Tony Arcieri (iqlusion) cafb0b2c18 v0.8.0-pre.0 (#491) 2023-03-14 17:17:07 -06:00
Tony Arcieri (iqlusion) 0c7441a81e Bump asymmetric crypto dependencies; MSRV 1.65 (#490)
Bumps the following dependencies to the latest versions:

- `elliptic-curve` v0.13
- `k256` v0.13
- `p256` v0.13
- `p384` v0.13
- `pbkdf2` v0.12
- `rsa` v0.9.0-pre.0
- `signature` v2
2023-03-14 11:53:00 -06:00
str4d a50addc15b Fix StatusWords::code output for StatusWords::VerifyFailError (#479)
* Fix `StatusWords::code` output for `StatusWords::VerifyFailError`

Closes iqlusioninc/yubikey.rs#473.

* Refactor `Transaction::transfer_data` to match on `StatusWords`

This makes the code more reliable, such that it would have avoided
the bug in iqlusioninc/yubikey.rs#473.
2023-02-12 12:02:22 -07:00
str4d 0809f300b7 Return errors from YubiKey::open_by_serial that indicate a key may exist (#477)
* Return errors from `YubiKey::open_by_serial` that indicate a key may exist

The only such error at the moment is `pcsc::Error::SharingViolation`, which
indicates a transient failure to access a specific reader that could have
been the one we needed (and so a future retry might succeed).

Closes iqlusioninc/yubikey.rs#458.

* Avoid resetting unused devices in YubiKey::open_by_serial

We only connect to readers so that we can determine their serial. We
now try to ensure that the order in which we connect to them doesn't
have an effect on their state after we are done.
2023-02-12 10:22:05 -07:00
str4d d55079f9a6 Enable library users to detect if a smart card doesn't support PIV (#476)
* Enable library users to detect if a smart card doesn't support PIV

Closes iqlusioninc/yubikey.rs#456.

* Avoid resetting the card if we fail to select PIV or fetch version/serial
2023-02-12 10:20:34 -07:00
str4d 10241230b3 Raise minimum pcsc version to remove workaround (#478)
In iqlusioninc/yubikey.rs#88 we added a workaround for what turned out
to be a bug in `pcsc`, where an error was returned if no readers were
available, instead of returning an empty iterator. `pcsc 2.3.1` was
published in 2019, so we can safely rely on it.
2023-02-12 10:18:55 -07:00
dependabot[bot] 1e02f135f0 Bump env_logger from 0.9.3 to 0.10.0 (#452)
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.9.3 to 0.10.0.
- [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.9.3...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-08 15:24:30 -07:00
Tony Arcieri (iqlusion) 0c2633ab31 transaction: comment cleanup in get_serial (#467)
Moves comments about each YubiKey version number above the arms of the
`match` expression
2023-01-07 13:35:52 -08:00
Tony Arcieri (iqlusion) f49c617a9d Improve parsing of serial numbers (#466)
Checks the length of the data returned when querying the serial number,
returning an error if it's longer than 4 bytes, and left-padding with
zeroes if it's too short.

This fixes some potential panics due to incorrect slice lengths as were
experienced in #465
2023-01-07 08:35:37 -08:00
Tony Arcieri (iqlusion) 1d33ea1747 Cargo.lock: bump dependencies (#463) 2023-01-02 10:22:07 -08:00
str4d 18eb4bf4f4 Add YubiKey::disconnect (#462)
This exposes `pcsc::Card::disconnect` to allow alternate disposition
methods.
2023-01-02 10:15:31 -08:00
str4d 10941bfb5b Add partial Debug impls for Context and YubiKey (#457)
This enables `yubikey::Result<T>` to be debug-formatted, for example
when wrapping the output of an API method in `dbg!()`.
2023-01-01 11:16:10 -07:00
Tony Arcieri (iqlusion) 002491193e Cargo.lock: bump dependencies + audit config (#451)
This should get the security audit passing again
2022-11-28 10:19:39 -08:00
Tony Arcieri (iqlusion) 2e5139b237 yubikey-cli v0.7.0 (#446) 2022-11-14 17:17:02 -08:00
Tony Arcieri (iqlusion) d880faaefa yubikey v0.7.0 (#444) 2022-11-14 15:53:00 -08:00
Tony Arcieri (iqlusion) cc00a10c2f img: add logo-sq.png (#445)
Square logo for use with rustdoc
2022-11-14 15:15:19 -08:00
Tony Arcieri (iqlusion) 0a2e798894 Switch from subtle-encoding to base16ct (#443) 2022-11-14 14:26:07 -08:00
Tony Arcieri (iqlusion) 5c4259023f Switch from lazy_static to once_cell (#442)
The latter will hopefully eventually be upstreamed into `std`.
2022-11-14 12:52:27 -08:00
Tony Arcieri (iqlusion) 57bb088c7d yubikey-cli: bump x509-parser to v0.14 (#441) 2022-11-14 12:30:55 -08:00
Tony Arcieri (iqlusion) ccf19a3668 Bump rsa to v0.7.1 (#440) 2022-11-14 11:08:05 -08:00
dependabot[bot] db13fce53b Bump clap from 3.2.23 to 4.0.23 (#438)
Bumps [clap](https://github.com/clap-rs/clap) from 3.2.23 to 4.0.23.
- [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/v3.2.23...v4.0.23)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 10:44:39 -08:00
Ferdinand Linnenberg 0071566097 feat: fixed incorrect issuer for certificates & added x509 prints (#437) 2022-11-14 10:42:07 -08:00
Tony Arcieri (iqlusion) d8653bc6f0 Use chrono v0.4.23 or newer (#436)
Addresses RUSTSEC-2020-0159
2022-11-12 15:42:34 -08:00
Tony Arcieri (iqlusion) 603b102932 Followups from #371 (#435)
Implements the changes I suggested in that PR, which are related to
naming conventions and documentation.
2022-11-12 15:41:58 -08:00
Tony Arcieri (iqlusion) 7470b1613a Cargo.lock: bump dependencies (#434) 2022-11-12 13:27:08 -08:00
Tony Arcieri (iqlusion) 4310cc0f9a Fix build and clippy warnings (#433) 2022-11-12 13:15:42 -08:00
Arthur Gautier 87ed7b2338 Adds support for metadata command (#371)
On firmware 5.4.3, yubikey introduced a metadata command. This returns
the policy attached to as slot as well as the public key of the pair in
the slot.

https://docs.yubico.com/yesdk/users-manual/application-piv/apdu/metadata.html
2022-11-12 11:12:10 -08:00
Shella Stephens 7866d8d53e MSRV 1.60.0 (#423) 2022-11-07 08:58:45 -07:00
Shella Stephens 744238fd77 Cargo.lock: update dependencies & fix audit 2022-11-07 07:56:25 -07:00
william light bbb186f95e Display inner PC/SC errors (#420) 2022-10-31 15:00:21 -07:00
dependabot[bot] c89cc5acd0 Bump sha2 from 0.10.2 to 0.10.5 (#407)
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.2 to 0.10.5.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.2...sha2-v0.10.5)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 14:29:21 -07:00
dependabot[bot] 2294c1cc3a Bump chrono from 0.4.21 to 0.4.22 (#405)
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.21 to 0.4.22.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.21...v0.4.22)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 14:29:08 -07:00
dependabot[bot] 65e201db0f Bump uuid from 1.1.2 to 1.2.1 (#415)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.1.2 to 1.2.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.1.2...1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-18 14:28:56 -07:00
Tony Arcieri (iqlusion) b571f81007 yubikey-cli v0.6.0 (#404) 2022-08-10 17:23:41 -07:00
37 changed files with 4112 additions and 2316 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
[advisories] [advisories]
ignore = [ ignore = [
"RUSTSEC-2020-0071", # time "RUSTSEC-2020-0071", # chrono
"RUSTSEC-2020-0159", # chrono "RUSTSEC-2021-0145", # atty
] "RUSTSEC-2023-0071", # rsa: Marvin Attack: potential key recovery
] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
+17 -6
View File
@@ -1,8 +1,19 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: cargo - package-ecosystem: cargo
directory: "/" versioning-strategy: lockfile-only
schedule: directory: "/"
interval: weekly allow:
time: '13:00' - dependency-type: "all"
open-pull-requests-limit: 10 groups:
all-deps:
patterns:
- "*"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
+8 -8
View File
@@ -13,7 +13,7 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
@@ -36,17 +36,17 @@ jobs:
toolchain: stable toolchain: stable
deps: true deps: true
- platform: ubuntu-latest - platform: ubuntu-latest
toolchain: 1.57.0 # MSRV toolchain: 1.85.0 # MSRV
deps: sudo apt-get install libpcsclite-dev deps: sudo apt-get install libpcsclite-dev
- platform: windows-latest - platform: windows-latest
toolchain: 1.57.0 # MSRV toolchain: 1.85.0 # MSRV
deps: true deps: true
- platform: macos-latest - platform: macos-latest
toolchain: 1.57.0 # MSRV toolchain: 1.85.0 # MSRV
deps: true deps: true
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v1 uses: actions/checkout@v6
- name: Install stable toolchain - name: Install stable toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -78,11 +78,11 @@ jobs:
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v6
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: 1.57.0 # MSRV toolchain: 1.85.0 # MSRV
components: clippy components: clippy
override: true override: true
- run: sudo apt-get install libpcsclite-dev - run: sudo apt-get install libpcsclite-dev
+4 -4
View File
@@ -15,16 +15,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v1 uses: actions/checkout@v6
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v1 uses: actions/cache@v5
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v1 uses: actions/cache@v5
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }}
@@ -42,4 +42,4 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: audit command: audit
args: --deny warnings --ignore RUSTSEC-2019-0031 # spin args: --deny warnings
+112
View File
@@ -4,6 +4,118 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- `yubikey::certificate::SelfSigned`
- `yubikey::Error::CertificateBuilder`
- `yubikey::MgmAlgorithmId`
- `yubikey::mgm`:
- `MgmKey::generate_for`
- `MgmKey::get_default`
- `impl AsRef<[u8]> for MgmKey`
### Changed
- MSRV is now 1.81.
- Migrated the public API to the following (pre-release) dependencies:
- `der 0.8.0-rc.1`
- `ecdsa 0.17.0-pre.9`
- `p256 0.14.0-pre.2`
- `p384 0.14.0-pre.2`
- `rsa 0.10.0-pre.3`
- `sha2 0.11.0-pre.4`
- `x509-cert 0.3.0-pre.0`
- `yubikey::mgm`:
- `MgmKey::generate` now takes a `rand::TryCryptoRng` argument.
- `MgmKey::generate` now requires the caller to specify the key algorithm via
an `MgmAlgorithmId` parameter.
- Use `MgmKey::generate_for` if you want to generate a key using the
preferred algorithm for a given Yubikey's firmware version.
- `MgmKey::from_bytes` now takes an `Option<MgmAlgorithmId>` argument, to
disambiguate algorithms with the same key length.
- `yubikey::piv`:
- `ManagementAlgorithmId` has been renamed to `SlotAlgorithmId`, and its
`ThreeDes` variant has been replaced by `SlotAlgorithmId::Management`
containing a `yubikey::MgmAlgorithmId`.
- Metadata command returns `Error:NotFound` instead of `Error::GenericError` when the object doesn't exist ([#558]).
### Removed
- `yubikey::mgm`:
- `MgmKey::new` (use `MgmKey::from_bytes(_, Some(MgmAlgorithmId::ThreeDes))`
instead).
- `impl AsRef<[u8; DES_LEN_3DES]> for MgmKey` (use
`impl AsRef<[u8]> for MgmKey` instead).
- `impl Default for MgmKey` (use `MgmKey::get_default` instead).
- `impl TryFrom<&[u8]> for MgmKey` (use `MgmKey::from_bytes` instead).
## 0.8.0 (2023-08-15)
### Added
- `impl Debug for {Context, YubiKey}` ([#457])
- `YubiKey::disconnect` ([#462])
- `Error::AppletNotFound` ([#476])
### Changed
- `Reader::open` now returns `Error::AppletNotFound` instead of `Error::Generic`
if the PIV applet is not present on the device. This is returned by non-PIV
virtual smart cards like Windows Hello for Business, as well as some smart
card readers when no card is present.
- `Reader::open` now avoids resetting the card if an error occurs (equivalent to
calling `YubiKey::disconnect(pcsc::Disposition::LeaveCard)` if `Reader::open`
succeeds).
- Raise minimum `pcsc` version to 2.3.1 and remove workaround ([#478])
- Bump asymmetric crypto dependencies; MSRV 1.65 ([#490])
- `elliptic-curve` v0.13
- `k256` v0.13
- `p256` v0.13
- `p384` v0.13
- `pbkdf2` v0.12
- `rsa` v0.9 ([#502])
- `signature` v2
- Use `x509-cert` certificate builder ([#495])
- Make `RsaKeyData::new` fallible ([#517])
### Fixed
- `StatusWords::code` now returns the correct code (including embedded `tries`
count) for `StatusWords::VerifyFailError`. Previously the returned code lost
information and was not round-trip compatible with `StatusWords::from(u16)`.
- Parsing of serial numbers ([#466])
- Make `YubiKey::open()` more robust ([#504])
[#457]: https://github.com/iqlusioninc/yubikey.rs/pull/457
[#462]: https://github.com/iqlusioninc/yubikey.rs/pull/462
[#466]: https://github.com/iqlusioninc/yubikey.rs/pull/466
[#476]: https://github.com/iqlusioninc/yubikey.rs/pull/476
[#478]: https://github.com/iqlusioninc/yubikey.rs/pull/478
[#490]: https://github.com/iqlusioninc/yubikey.rs/pull/490
[#495]: https://github.com/iqlusioninc/yubikey.rs/pull/495
[#502]: https://github.com/iqlusioninc/yubikey.rs/pull/502
[#504]: https://github.com/iqlusioninc/yubikey.rs/pull/504
[#517]: https://github.com/iqlusioninc/yubikey.rs/pull/517
## 0.7.0 (2022-11-14)
### Added
- Display inner PC/SC errors ([#420])
- Support for metadata command ([#371])
- Better `certificate::Serial` inspection ([#437])
### Changed
- MSRV 1.60.0 ([#423])
- Bump `rsa` to v0.7.1 ([#440])
- Switch from `lazy_static` to `once_cell` ([#442])
- Switch from `subtle-encoding` to `base16ct` ([#443])
### Fixed
- Use `chrono` v0.4.23 or newer ([#436])
- `Certificate::issuer` was returning the subject instead ([#437])
[#371]: https://github.com/iqlusioninc/yubikey.rs/pull/371
[#420]: https://github.com/iqlusioninc/yubikey.rs/pull/420
[#423]: https://github.com/iqlusioninc/yubikey.rs/pull/423
[#436]: https://github.com/iqlusioninc/yubikey.rs/pull/436
[#437]: https://github.com/iqlusioninc/yubikey.rs/pull/437
[#440]: https://github.com/iqlusioninc/yubikey.rs/pull/440
[#442]: https://github.com/iqlusioninc/yubikey.rs/pull/442
[#443]: https://github.com/iqlusioninc/yubikey.rs/pull/443
## 0.6.0 (2022-08-10) ## 0.6.0 (2022-08-10)
### Changed ### Changed
- 2021 edition upgrade ([#343]) - 2021 edition upgrade ([#343])
Generated
+921 -822
View File
File diff suppressed because it is too large Load Diff
+38 -29
View File
@@ -1,59 +1,68 @@
[package] [package]
name = "yubikey" name = "yubikey"
version = "0.6.0" version = "0.9.0-pre.0"
description = """ description = """
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
support for hardware-backed public-key decryption and digital signatures using support for hardware-backed public-key decryption and digital signatures using
the Personal Identity Verification (PIV) application. Supports RSA (1024/2048) the Personal Identity Verification (PIV) application. Supports RSA (1024/2048/3072/4096)
or ECC (NIST P-256/P-384) algorithms e.g, PKCS#1v1.5, ECDSA or ECC (NIST P-256/P-384) algorithms e.g, PKCS#1v1.5, ECDSA
""" """
authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"] authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"]
license = "BSD-2-Clause" license = "BSD-2-Clause"
repository = "https://github.com/iqlusioninc/yubikey.rs" repository = "https://github.com/iqlusioninc/yubikey.rs"
readme = "README.md" readme = "README.md"
categories = ["api-bindings", "cryptography", "hardware-support"] categories = ["api-bindings", "authentication", "cryptography", "hardware-support"]
keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"] keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"]
edition = "2021" edition = "2021"
rust-version = "1.57" rust-version = "1.85"
[workspace] [workspace]
members = [".", "cli"] members = [".", "cli"]
[workspace.dependencies]
sha2 = "0.11"
x509-cert = { version = "0.3.0-rc.4", features = ["builder", "hazmat"] }
[dependencies] [dependencies]
chrono = "0.4" aes = { version = "0.9.0-rc.4", features = ["zeroize"] }
cookie-factory = "0.3" bitflags = "2.5.0"
der-parser = "8" cipher = { version = "0.5", features = ["getrandom", "rand_core"] }
des = "0.8" curve25519-dalek = "5.0.0-pre.6"
elliptic-curve = "0.12" der = "0.8"
hmac = "0.12" des = "0.9.0-rc.3"
ecdsa = { version = "0.17.0-rc.16", features = ["digest", "pem"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["alloc", "pkcs8"] }
elliptic-curve = "0.14.0-rc.29"
hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
log = "0.4" log = "0.4"
nom = "7" nom = "8"
num-bigint-dig = { version = "0.8", features = ["rand"] } p256 = "0.14.0-rc.8"
num-traits = "0.2" p384 = "0.14.0-rc.8"
num-integer = "0.1" pbkdf2 = { version = "0.13.0-rc.10", default-features = false, features = ["hmac"] }
pbkdf2 = { version = "0.11", default-features = false } pcsc = "2.3.1"
p256 = "0.11" rand = "0.10"
p384 = "0.11" rand_core = "0.10"
pcsc = "2" rsa = { version = "0.10.0-rc.17", features = ["sha2"] }
rand_core = { version = "0.6", features = ["std"] } sha1 = { version = "0.11", features = ["oid"] }
rsa = "0.6" sha2 = { workspace = true, features = ["oid"] }
secrecy = "0.8" signature = "3.0.0-rc.10"
sha1 = "0.10"
sha2 = "0.10"
subtle = "2" subtle = "2"
subtle-encoding = "0.5" uuid = { version = "1.2", features = ["v4"] }
uuid = { version = "1.0", features = ["v4"] } x25519-dalek = "3.0.0-pre.6"
x509 = "0.2" x509-cert.workspace = true
x509-parser = "0.14"
zeroize = "1" zeroize = "1"
[dev-dependencies] [dev-dependencies]
env_logger = "0.9" env_logger = "0.11"
lazy_static = "1" once_cell = "1"
[features] [features]
untested = [] untested = []
[[example]]
name = "change-mode"
required-features = ["untested"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
+75 -19
View File
@@ -4,22 +4,25 @@
[![crate][crate-image]][crate-link] [![crate][crate-image]][crate-link]
[![Docs][docs-image]][docs-link] [![Docs][docs-image]][docs-link]
[![Build Status][build-image]][build-link]
[![Safety Dance][safety-image]][safety-link]
[![Dependency Status][deps-image]][deps-link]
[![2-Clause BSD Licensed][license-image]][license-link] [![2-Clause BSD Licensed][license-image]][license-link]
![MSRV][msrv-image] ![MSRV][msrv-image]
[![Safety Dance][safety-image]][safety-link]
[![Build Status][build-image]][build-link]
[![dependency status][deps-image]][deps-link]
Pure Rust cross-platform host-side driver for [YubiKey] devices from [Yubico] Pure Rust cross-platform host-side driver for [YubiKey] devices from [Yubico]
with support for public-key encryption and digital signatures using the with support for public-key encryption and digital signatures using the
[Personal Identity Verification (PIV)][PIV] application. [Personal Identity Verification (PIV)][PIV] application.
Uses the Personal Computer/Smart Card ([PC/SC]) interface with cross-platform
access provided by the [`pcsc` crate].
[Documentation][docs-link] [Documentation][docs-link]
## About ## About
YubiKeys are versatile devices and through their PIV support, you can use them YubiKeys are versatile devices and through their PIV support, you can use them
to store a number of RSA (2048/1024) and ECC (NIST P-256/P-384) private keys to store a number of RSA (1024/2048/3072/4096) and ECC (NIST P-256/P-384) private keys
with configurable access control policies. Both the signing (RSASSA/ECDSA) and with configurable access control policies. Both the signing (RSASSA/ECDSA) and
encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type. encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type.
@@ -34,17 +37,47 @@ Note that while this project started as a fork of a [Yubico] project,
this fork is **NOT** an official Yubico project and is in no way supported or this fork is **NOT** an official Yubico project and is in no way supported or
endorsed by Yubico. endorsed by Yubico.
## Features
### Personal Identity Verification (PIV)
[PIV] is a [NIST] standard for both *signing* and *encryption*
using SmartCards and SmartCard-based hardware tokens like YubiKeys.
PIV-related functionality can be found in the [`piv`] module.
This library natively implements the protocol used to manage and
utilize PIV encryption and signing keys which can be generated, imported,
and stored on YubiKey devices.
See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
on which devices support PIV and the available functionality.
### Supported Algorithms
- **Authentication**: `3DES`
- **Encryption**:
- RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
- **Signatures**:
- RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
NOTE:
- RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
- `RSA3072` and `RSA4096` require a YubiKey with firmware 5.7 or newer.
## Minimum Supported Rust Version ## Minimum Supported Rust Version
Rust **1.57** or newer. Rust **1.60** or newer.
## Supported YubiKeys ## Supported YubiKeys
- [YubiKey 4] series - [YubiKey 4] series
- [YubiKey 5] series - [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported. NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
## Supported Operating Systems ## Supported Operating Systems
@@ -59,16 +92,27 @@ an experimental stage and may still contain high-severity issues.
USE AT YOUR OWN RISK! USE AT YOUR OWN RISK!
## Status
Functionality which has been successfully tested is available by default.
Any functionality which is gated on the `untested` feature has not been
properly tested and is not known to function correctly.
Please see the [`untested` functionality tracking issue] for current status.
We would appreciate any help testing this functionality and removing the
`untested` gating as well as writing more automated tests.
## Testing ## Testing
To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in To run the full test suite, you'll need a supported YubiKey device connected
the default state (i.e. default PIN/PUK). which is in the default state (i.e. default PIN/PUK).
Tests which run live against a YubiKey device are marked as `#[ignore]` by Tests which run live against a YubiKey device are marked as `#[ignore]` by
default in order to pass when running in a CI environment. To run these default in order to pass when running in a CI environment. To run these
tests locally, invoke the following command: tests locally, invoke the following command:
``` ```shell
cargo test -- --ignored cargo test -- --ignored
``` ```
@@ -77,14 +121,14 @@ information about what is happening. If you'd like to print this logging
information while running the tests, set the `RUST_LOG` environment variable information while running the tests, set the `RUST_LOG` environment variable
to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`): to a relevant loglevel (e.g. `error`, `warn`, `info`, `debug`, `trace`):
``` ```shell
RUST_LOG=info cargo test -- --ignored RUST_LOG=info cargo test -- --ignored
``` ```
To trace every message sent to/from the card i.e. the raw To trace every message sent to/from the card i.e. the raw
Application Protocol Data Unit (APDU) messages, use the `trace` log level: Application Protocol Data Unit (APDU) messages, use the `trace` log level:
``` ```text
running 1 test running 1 test
[INFO yubikey::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID' [INFO yubikey::yubikey] trying to connect to reader 'Yubico YubiKey OTP+FIDO+CCID'
[INFO yubikey::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully [INFO yubikey::yubikey] connected to 'Yubico YubiKey OTP+FIDO+CCID' successfully
@@ -110,6 +154,14 @@ Yubico, which was originally written in C. It was mechanically translated
from C into Rust using [Corrode], and then subsequently heavily from C into Rust using [Corrode], and then subsequently heavily
refactored into safer, more idiomatic Rust. refactored into safer, more idiomatic Rust.
For more information on [yubico-piv-tool] and background information on how
the YubiKey implementation of PIV works in general, see the
[Yubico PIV Tool Command Line Guide][piv-tool-guide].
## ⚠️ Security Warning
No security audits of this crate have ever been performed.
## Code of Conduct ## Code of Conduct
We abide by the [Contributor Covenant][cc-md] and ask that you do as well. We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
@@ -123,7 +175,7 @@ Yubico's [yubico-piv-tool], a C library/CLI program. The original library
was licensed under a [2-Clause BSD License][BSDL], which this library inherits was licensed under a [2-Clause BSD License][BSDL], which this library inherits
as a derived work. as a derived work.
Copyright (c) 2014-2021 Yubico AB, Tony Arcieri Copyright (c) 2014-2025 Yubico AB, Tony Arcieri
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@@ -159,16 +211,16 @@ or conditions.
[//]: # (badges) [//]: # (badges)
[crate-image]: https://img.shields.io/crates/v/yubikey.svg [crate-image]: https://img.shields.io/crates/v/yubikey?logo=rust
[crate-link]: https://crates.io/crates/yubikey [crate-link]: https://crates.io/crates/yubikey
[docs-image]: https://docs.rs/yubikey/badge.svg [docs-image]: https://docs.rs/yubikey/badge.svg
[docs-link]: https://docs.rs/yubikey/ [docs-link]: https://docs.rs/yubikey/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg [license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING [license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING
[msrv-image]: https://img.shields.io/badge/rustc-1.57+-blue.svg [msrv-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/ [safety-link]: https://github.com/rust-secure-code/safety-dance/
[build-image]: https://github.com/iqlusioninc/yubikey.rs/workflows/CI/badge.svg?branch=main&event=push [build-image]: https://github.com/iqlusioninc/yubikey.rs/actions/workflows/ci.yml/badge.svg
[build-link]: https://github.com/iqlusioninc/yubikey.rs/actions [build-link]: https://github.com/iqlusioninc/yubikey.rs/actions
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg [deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
[deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs [deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs
@@ -176,17 +228,21 @@ or conditions.
[//]: # (general links) [//]: # (general links)
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ [YubiKey]: https://www.yubico.com/products/yubikey-hardware/
[PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/ [Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo [PIV]: https://piv.idmanagement.gov/
[NIST]: https://www.nist.gov/
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4 [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/ [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[piv-tool-guide]: https://docs.yubico.com/software/yubikey/tools/pivtool/Introduction.html
[Corrode]: https://github.com/jameysharp/corrode [Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/ [cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md [cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
[BSDL]: https://opensource.org/licenses/BSD-2-Clause [BSDL]: https://opensource.org/licenses/BSD-2-Clause
[`untested` functionality tracking issue]: https://github.com/iqlusioninc/yubikey.rs/issues/280
[//]: # (github issues) [//]: # (github issues)
+28
View File
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- MSRV is now 1.81.
## 0.7.0 (2022-11-14)
### Changed
- Bump `clap` to v4.0 ([#438])
- Bump `x509-parser` to v0.14 ([#441])
- Switch from `lazy_static` to `once_cell` ([#442])
- Switch from `subtle-encoding` to `base16ct` ([#443])
- Bump `yubikey` dependency to v0.7 ([#444])
[#438]: https://github.com/iqlusioninc/yubikey.rs/pull/438
[#441]: https://github.com/iqlusioninc/yubikey.rs/pull/441
[#442]: https://github.com/iqlusioninc/yubikey.rs/pull/442
[#443]: https://github.com/iqlusioninc/yubikey.rs/pull/443
[#444]: https://github.com/iqlusioninc/yubikey.rs/pull/444
## 0.6.0 (2022-08-10)
### Changed
- 2021 edition upgrade; MSRV 1.57 ([#343])
- Migrate from `gumdrop` to `clap` v3 ([#379])
- Bump `yubikey` dependency to v0.6 ([#403])
[#343]: https://github.com/iqlusioninc/yubikey.rs/pull/343
[#379]: https://github.com/iqlusioninc/yubikey.rs/pull/379
[#403]: https://github.com/iqlusioninc/yubikey.rs/pull/403
## 0.5.0 (2021-11-21) ## 0.5.0 (2021-11-21)
### Changed ### Changed
- Bump `yubikey` dependency to v0.5 ([#327]) - Bump `yubikey` dependency to v0.5 ([#327])
+9 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "yubikey-cli" name = "yubikey-cli"
version = "0.6.0-pre" version = "0.9.0-pre"
description = """ description = """
Command-line interface for performing encryption and signing using RSA/ECC keys Command-line interface for performing encryption and signing using RSA/ECC keys
stored on YubiKey devices. stored on YubiKey devices.
@@ -12,15 +12,15 @@ readme = "README.md"
categories = ["command-line-utilities", "cryptography", "hardware-support"] categories = ["command-line-utilities", "cryptography", "hardware-support"]
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"] keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = "1.85"
[dependencies] [dependencies]
clap = { version = "3", features = ["derive"] } clap = { version = "4", features = ["derive"] }
env_logger = "0.9" env_logger = "0.11"
lazy_static = "1" hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
log = "0.4" log = "0.4"
sha2 = "0.9" once_cell = "1"
subtle-encoding = "0.5" sha2.workspace = true
termcolor = "1" termcolor = "1"
x509-parser = "0.12" x509-cert.workspace = true
yubikey = { version = "0.6", path = ".." } yubikey = { version = "=0.9.0-pre.0", path = ".." }
+4 -10
View File
@@ -18,15 +18,14 @@ utility with general-purpose public-key encryption and signing support.
## Minimum Supported Rust Version ## Minimum Supported Rust Version
Rust **1.51** or newer. Rust **1.60** or newer.
## Supported YubiKeys ## Supported YubiKeys
- [YubiKey 4] series - [YubiKey 4] series
- [YubiKey 5] series - [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported. NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
## Security Warning ## Security Warning
@@ -35,10 +34,6 @@ an experimental stage and may still contain high-severity issues.
USE AT YOUR OWN RISK! USE AT YOUR OWN RISK!
## Status
WIP. Check back later.
## Code of Conduct ## Code of Conduct
We abide by the [Contributor Covenant][cc-md] and ask that you do as well. We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
@@ -47,7 +42,7 @@ For more information, please see [CODE_OF_CONDUCT.md][cc-md].
## License ## License
Copyright (c) 2014-2021 Yubico AB, Tony Arcieri Copyright (c) 2014-2023 Yubico AB, Tony Arcieri
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@@ -88,7 +83,7 @@ or conditions.
[docs-image]: https://docs.rs/yubikey-cli/badge.svg [docs-image]: https://docs.rs/yubikey-cli/badge.svg
[docs-link]: https://docs.rs/yubikey-cli/ [docs-link]: https://docs.rs/yubikey-cli/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg [license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg [rustc-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg [maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/ [safety-link]: https://github.com/rust-secure-code/safety-dance/
@@ -102,7 +97,6 @@ or conditions.
[PIV]: https://piv.idmanagement.gov/ [PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/ [Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4 [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/ [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
+39 -51
View File
@@ -1,16 +1,15 @@
//! Status messages //! Status messages
use lazy_static::lazy_static;
use log::debug; use log::debug;
use once_cell::sync::Lazy;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{ use std::{
io::{self, Write}, io::{self, Write},
str, str,
sync::Mutex, sync::Mutex,
}; };
use subtle_encoding::hex;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
use x509_parser::parse_x509_certificate; use x509_cert::der::Encode;
use yubikey::{certificate::Certificate, piv::*, YubiKey}; use yubikey::{certificate::Certificate, piv::*, YubiKey};
/// Print a success status message (in green if colors are enabled) /// Print a success status message (in green if colors are enabled)
@@ -59,16 +58,14 @@ macro_rules! status_err {
}; };
} }
lazy_static! { /// Color configuration
/// Color configuration static COLOR_CHOICE: Lazy<Mutex<Option<ColorChoice>>> = Lazy::new(|| Mutex::new(None));
static ref COLOR_CHOICE: Mutex<Option<ColorChoice>> = Mutex::new(None);
/// Standard output /// Standard output
pub static ref STDOUT: StandardStream = StandardStream::stdout(get_color_choice()); pub static STDOUT: Lazy<StandardStream> = Lazy::new(|| StandardStream::stdout(get_color_choice()));
/// Standard error /// Standard error
pub static ref STDERR: StandardStream = StandardStream::stderr(get_color_choice()); pub static STDERR: Lazy<StandardStream> = Lazy::new(|| StandardStream::stderr(get_color_choice()));
}
/// Obtain the color configuration. /// Obtain the color configuration.
/// ///
@@ -140,14 +137,12 @@ impl Status {
/// Print the given message to stdout /// Print the given message to stdout
pub fn print_stdout(self, msg: impl AsRef<str>) { pub fn print_stdout(self, msg: impl AsRef<str>) {
self.print(&*STDOUT, msg) self.print(&STDOUT, msg).expect("error printing to stdout!")
.expect("error printing to stdout!")
} }
/// Print the given message to stderr /// Print the given message to stderr
pub fn print_stderr(self, msg: impl AsRef<str>) { pub fn print_stderr(self, msg: impl AsRef<str>) {
self.print(&*STDERR, msg) self.print(&STDERR, msg).expect("error printing to stderr!")
.expect("error printing to stderr!")
} }
/// Print the given message /// Print the given message
@@ -185,44 +180,37 @@ pub fn print_cert_info(
return Ok(()); return Ok(());
} }
}; };
let buf = cert.into_buffer(); let cert = &cert.cert;
if !buf.is_empty() { let fingerprint = Sha256::digest(cert.to_der().unwrap());
let fingerprint = Sha256::digest(&buf); let slot_id: u8 = slot.into();
let slot_id: u8 = slot.into(); print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?; print_cert_attr(
match parse_x509_certificate(&buf) { stream,
Ok((_rem, cert)) => { "Algorithm",
print_cert_attr( cert.tbs_certificate()
stream, .subject_public_key_info()
"Algorithm", .algorithm
cert.tbs_certificate.subject_pki.algorithm.algorithm, .oid,
)?; )?;
print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?; print_cert_attr(stream, "Subject", cert.tbs_certificate().subject())?;
print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?; print_cert_attr(stream, "Issuer", cert.tbs_certificate().issuer())?;
print_cert_attr( print_cert_attr(
stream, stream,
"Fingerprint", "Fingerprint",
str::from_utf8(hex::encode(fingerprint).as_slice()).unwrap(), hex::upper::encode_string(&fingerprint),
)?; )?;
print_cert_attr( print_cert_attr(
stream, stream,
"Not Before", "Not Before",
cert.tbs_certificate.validity.not_before.to_rfc2822(), cert.tbs_certificate().validity().not_before,
)?; )?;
print_cert_attr( print_cert_attr(
stream, stream,
"Not After", "Not After",
cert.tbs_certificate.validity.not_after.to_rfc2822(), cert.tbs_certificate().validity().not_after,
)?; )?;
}
_ => {
println!("Failed to parse certificate");
return Ok(());
}
};
}
Ok(()) Ok(())
} }
+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();
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+85 -2
View File
@@ -82,6 +82,12 @@ impl Apdu {
self self
} }
/// Set this APDU's second parameter only
pub(crate) fn p2(&mut self, value: u8) -> &mut Self {
self.p2 = value;
self
}
/// Set both parameters for this APDU /// Set both parameters for this APDU
pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self { pub fn params(&mut self, p1: u8, p2: u8) -> &mut Self {
self.p1 = p1; self.p1 = p1;
@@ -195,6 +201,18 @@ pub enum Ins {
/// Get device serial /// Get device serial
GetSerial, GetSerial,
/// Get slot metadata
GetMetadata,
/// Management // Read Config
ReadConfig,
/// Management // Write Config
WriteConfig,
/// Management // DeviceReset
DeviceReset,
/// Other/unrecognized instruction codes /// Other/unrecognized instruction codes
Other(u8), Other(u8),
} }
@@ -219,6 +237,13 @@ impl Ins {
Ins::SetPinRetries => 0xfa, Ins::SetPinRetries => 0xfa,
Ins::Attest => 0xf9, Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8, Ins::GetSerial => 0xf8,
Ins::GetMetadata => 0xf7,
// Management
Ins::ReadConfig => 0x1d,
Ins::WriteConfig => 0x1c,
Ins::DeviceReset => 0x1f,
Ins::Other(code) => code, Ins::Other(code) => code,
} }
} }
@@ -227,6 +252,11 @@ impl Ins {
impl From<u8> for Ins { impl From<u8> for Ins {
fn from(code: u8) -> Self { fn from(code: u8) -> Self {
match code { match code {
// Management
0x1d => Ins::ReadConfig,
0x1c => Ins::WriteConfig,
0x1f => Ins::DeviceReset,
0x20 => Ins::Verify, 0x20 => Ins::Verify,
0x24 => Ins::ChangeReference, 0x24 => Ins::ChangeReference,
0x2c => Ins::ResetRetry, 0x2c => Ins::ResetRetry,
@@ -243,6 +273,7 @@ impl From<u8> for Ins {
0xfa => Ins::SetPinRetries, 0xfa => Ins::SetPinRetries,
0xf9 => Ins::Attest, 0xf9 => Ins::Attest,
0xf8 => Ins::GetSerial, 0xf8 => Ins::GetSerial,
0xf7 => Ins::GetMetadata,
code => Ins::Other(code), code => Ins::Other(code),
} }
} }
@@ -313,7 +344,7 @@ impl From<Vec<u8>> for Response {
} }
let sw = StatusWords::from( let sw = StatusWords::from(
(bytes[bytes.len() - 2] as u16) << 8 | (bytes[bytes.len() - 1] as u16), ((bytes[bytes.len() - 2] as u16) << 8) | (bytes[bytes.len() - 1] as u16),
); );
let len = bytes.len() - 2; let len = bytes.len() - 2;
@@ -348,6 +379,12 @@ pub(crate) enum StatusWords {
/// Successful execution /// Successful execution
Success, Success,
/// The requested data was too large for the response, and there is data remaining.
BytesRemaining {
/// The number of bytes remaining, as indicated in the response.
len: u8,
},
/// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L53 /// https://github.com/Yubico/yubikey-manager/blob/1f22620b623c6b345dd9f9193ec765a542dddc80/ykman/driver_ccid.py#L53
NoInputDataError, NoInputDataError,
@@ -384,6 +421,9 @@ pub(crate) enum StatusWords {
/// Not enough memory /// Not enough memory
NoSpaceError, NoSpaceError,
/// Referenced data or reference data not found
ReferenceDataNotFoundError,
// //
// Custom Yubico Status Word extensions // Custom Yubico Status Word extensions
// //
@@ -405,8 +445,9 @@ impl StatusWords {
pub fn code(self) -> u16 { pub fn code(self) -> u16 {
match self { match self {
StatusWords::None => 0, StatusWords::None => 0,
StatusWords::BytesRemaining { len } => 0x6100 | len as u16,
StatusWords::NoInputDataError => 0x6285, StatusWords::NoInputDataError => 0x6285,
StatusWords::VerifyFailError { tries } => 0x63c0 & tries as u16, StatusWords::VerifyFailError { tries } => 0x63c0 | tries as u16,
StatusWords::WrongLengthError => 0x6700, StatusWords::WrongLengthError => 0x6700,
StatusWords::SecurityStatusError => 0x6982, StatusWords::SecurityStatusError => 0x6982,
StatusWords::AuthBlockedError => 0x6983, StatusWords::AuthBlockedError => 0x6983,
@@ -416,6 +457,7 @@ impl StatusWords {
StatusWords::IncorrectParamError => 0x6a80, StatusWords::IncorrectParamError => 0x6a80,
StatusWords::NotFoundError => 0x6a82, StatusWords::NotFoundError => 0x6a82,
StatusWords::NoSpaceError => 0x6a84, StatusWords::NoSpaceError => 0x6a84,
StatusWords::ReferenceDataNotFoundError => 0x6a88,
StatusWords::IncorrectSlotError => 0x6b00, StatusWords::IncorrectSlotError => 0x6b00,
StatusWords::NotSupportedError => 0x6d00, StatusWords::NotSupportedError => 0x6d00,
StatusWords::CommandAbortedError => 0x6f00, StatusWords::CommandAbortedError => 0x6f00,
@@ -434,6 +476,9 @@ impl From<u16> for StatusWords {
fn from(sw: u16) -> Self { fn from(sw: u16) -> Self {
match sw { match sw {
0x0000 => StatusWords::None, 0x0000 => StatusWords::None,
sw if sw & 0xff00 == 0x6100 => Self::BytesRemaining {
len: (sw & 0x00ff) as u8,
},
0x6285 => StatusWords::NoInputDataError, 0x6285 => StatusWords::NoInputDataError,
sw if sw & 0xfff0 == 0x63c0 => StatusWords::VerifyFailError { sw if sw & 0xfff0 == 0x63c0 => StatusWords::VerifyFailError {
tries: (sw & 0x000f) as u8, tries: (sw & 0x000f) as u8,
@@ -447,6 +492,7 @@ impl From<u16> for StatusWords {
0x6a80 => StatusWords::IncorrectParamError, 0x6a80 => StatusWords::IncorrectParamError,
0x6a82 => StatusWords::NotFoundError, 0x6a82 => StatusWords::NotFoundError,
0x6a84 => StatusWords::NoSpaceError, 0x6a84 => StatusWords::NoSpaceError,
0x6a88 => StatusWords::ReferenceDataNotFoundError,
0x6b00 => StatusWords::IncorrectSlotError, 0x6b00 => StatusWords::IncorrectSlotError,
0x6d00 => StatusWords::NotSupportedError, 0x6d00 => StatusWords::NotSupportedError,
0x6f00 => StatusWords::CommandAbortedError, 0x6f00 => StatusWords::CommandAbortedError,
@@ -461,3 +507,40 @@ impl From<StatusWords> for u16 {
sw.code() sw.code()
} }
} }
#[cfg(test)]
mod tests {
use super::StatusWords;
#[test]
fn status_words_round_trip() {
let round_trip = |sw: StatusWords| {
assert_eq!(StatusWords::from(sw.code()), sw);
};
round_trip(StatusWords::None);
round_trip(StatusWords::BytesRemaining { len: 1 });
round_trip(StatusWords::BytesRemaining { len: 10 });
round_trip(StatusWords::BytesRemaining { len: 0xFF });
round_trip(StatusWords::Success);
round_trip(StatusWords::NoInputDataError);
round_trip(StatusWords::VerifyFailError { tries: 0x0F });
round_trip(StatusWords::VerifyFailError { tries: 3 });
round_trip(StatusWords::VerifyFailError { tries: 2 });
round_trip(StatusWords::VerifyFailError { tries: 1 });
round_trip(StatusWords::VerifyFailError { tries: 0 });
round_trip(StatusWords::WrongLengthError);
round_trip(StatusWords::SecurityStatusError);
round_trip(StatusWords::AuthBlockedError);
round_trip(StatusWords::DataInvalidError);
round_trip(StatusWords::ConditionsNotSatisfiedError);
round_trip(StatusWords::CommandNotAllowedError);
round_trip(StatusWords::IncorrectParamError);
round_trip(StatusWords::NotFoundError);
round_trip(StatusWords::NoSpaceError);
round_trip(StatusWords::IncorrectSlotError);
round_trip(StatusWords::NotSupportedError);
round_trip(StatusWords::CommandAbortedError);
round_trip(StatusWords::Other(0x1337));
}
}
+21 -20
View File
@@ -30,13 +30,10 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{Error, Result, YubiKey}; use crate::{Result, YubiKey};
use rand_core::{OsRng, RngCore}; use cipher::common::Generate;
use std::{ use rand_core::CryptoRng;
fmt::{self, Debug, Display}, use std::fmt::{self, Debug, Display};
str,
};
use subtle_encoding::hex;
/// CCCID offset /// CCCID offset
const CCC_ID_OFFS: usize = 9; const CCC_ID_OFFS: usize = 9;
@@ -52,6 +49,7 @@ const OBJ_CAPABILITY: u32 = 0x005f_c107;
/// - 0xff == Manufacturer ID (dummy) /// - 0xff == Manufacturer ID (dummy)
/// - 0x02 == Card type (javaCard) /// - 0x02 == Card type (javaCard)
/// - next 14 bytes: card ID /// - next 14 bytes: card ID
#[allow(dead_code)]
const CCC_TMPL: &[u8] = &[ const CCC_TMPL: &[u8] = &[
0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x15, 0xa0, 0x00, 0x00, 0x01, 0x16, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf1, 0x01, 0x21, 0xf2, 0x01, 0x21, 0xf3, 0x00, 0xf4,
@@ -69,9 +67,12 @@ impl CardId {
/// Generate a random CCC Card ID /// Generate a random CCC Card ID
pub fn generate() -> Self { pub fn generate() -> Self {
let mut id = [0u8; Self::BYTE_SIZE]; Self(Generate::generate())
OsRng.fill_bytes(&mut id); }
Self(id)
/// Generate a random CCC Card ID from an [`Rng`]
pub fn generate_from_rng<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
Self(Generate::generate_from_rng(rng))
} }
} }
@@ -94,17 +95,11 @@ impl CccId {
pub fn get(yubikey: &mut YubiKey) -> Result<Self> { pub fn get(yubikey: &mut YubiKey) -> Result<Self> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(OBJ_CAPABILITY)?; let response = txn.fetch_object(OBJ_CAPABILITY)?;
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
if response.len() != CCC_TMPL.len() {
return Err(Error::GenericError);
}
Ok(Self(response[..Self::BYTE_SIZE].try_into().unwrap()))
} }
/// Set Cardholder Capability Container (CCC) ID /// Set Cardholder Capability Container (CCC) ID
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> { pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
let mut buf = CCC_TMPL.to_vec(); let mut buf = CCC_TMPL.to_vec();
buf[0..self.0.len()].copy_from_slice(&self.0); buf[0..self.0.len()].copy_from_slice(&self.0);
@@ -114,8 +109,14 @@ impl CccId {
} }
} }
impl Display for CccId { impl AsRef<[u8]> for CccId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn as_ref(&self) -> &[u8] {
f.write_str(str::from_utf8(&hex::encode(&self.0[..])).unwrap()) &self.0
}
}
impl Display for CccId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::upper::encode_string(self.as_ref()))
} }
} }
+328 -491
View File
@@ -33,71 +33,29 @@
use crate::{ use crate::{
consts::CB_OBJ_MAX, consts::CB_OBJ_MAX,
error::{Error, Result}, error::{Error, Result},
piv::{sign_data, AlgorithmId, SlotId}, piv::SlotId,
serialization::*, serialization::*,
transaction::Transaction, transaction::Transaction,
yubikey::YubiKey, yubikey::YubiKey,
Buffer, Buffer,
}; };
use chrono::{DateTime, Utc};
use elliptic_curve::sec1::EncodedPoint as EcPublicKey;
use log::error; use log::error;
use num_bigint_dig::BigUint; use x509_cert::{
use p256::NistP256; builder::{profile::BuilderProfile, Builder, CertificateBuilder},
use p384::NistP384; der::{referenced::OwnedToRef, Decode, Encode},
use rsa::{PublicKeyParts, RsaPublicKey}; name::Name,
use sha2::{Digest, Sha256}; serial_number::SerialNumber,
use std::{fmt, ops::DerefMut}; spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
use x509::{der::Oid, RelativeDistinguishedName}; time::Validity,
use x509_parser::{parse_x509_certificate, x509::SubjectPublicKeyInfo}; };
use zeroize::Zeroizing; use zeroize::Zeroizing;
// TODO: Make these der_parser::oid::Oid constants when it has const fn support.
const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
const OID_NIST_P256: &str = "1.2.840.10045.3.1.7";
const OID_NIST_P384: &str = "1.3.132.0.34";
const TAG_CERT: u8 = 0x70; const TAG_CERT: u8 = 0x70;
const TAG_CERT_COMPRESS: u8 = 0x71; const TAG_CERT_COMPRESS: u8 = 0x71;
const TAG_CERT_LRC: u8 = 0xFE; const TAG_CERT_LRC: u8 = 0xFE;
/// A serial number for a [`Certificate`].
#[derive(Clone, Debug)]
pub struct Serial(BigUint);
impl From<BigUint> for Serial {
fn from(num: BigUint) -> Serial {
Serial(num)
}
}
impl From<[u8; 20]> for Serial {
fn from(bytes: [u8; 20]) -> Serial {
Serial(BigUint::from_bytes_be(&bytes))
}
}
impl TryFrom<&[u8]> for Serial {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Serial> {
if bytes.len() <= 20 {
Ok(Serial(BigUint::from_bytes_be(bytes)))
} else {
Err(Error::ParseError)
}
}
}
impl Serial {
fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes_be()
}
}
/// Information about how a [`Certificate`] is stored within a YubiKey. /// Information about how a [`Certificate`] is stored within a YubiKey.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CertInfo { pub enum CertInfo {
/// The certificate is uncompressed. /// The certificate is uncompressed.
Uncompressed, Uncompressed,
@@ -127,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 /// Certificates
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Certificate { pub struct Certificate {
serial: Serial, /// Inner certificate
#[allow(dead_code)] pub cert: x509_cert::Certificate,
issuer: String,
subject: String,
subject_pki: PublicKeyInfo,
data: Buffer,
}
impl<'a> TryFrom<&'a [u8]> for Certificate {
type Error = Error;
fn try_from(bytes: &'a [u8]) -> Result<Self> {
Self::from_bytes(bytes.to_vec())
}
} }
impl Certificate { impl Certificate {
@@ -339,112 +98,30 @@ impl Certificate {
/// ///
/// `extensions` is optional; if empty, no extensions will be included. Due to the /// `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 /// need for an `O: Oid` type parameter, users who do not have any extensions should
/// use the workaround `let extensions: &[x509::Extension<'_, &[u64]>] = &[];`. /// use the workaround `let extensions: &[x509_cert::Extension<'_, &[u64]>] = &[];`.
pub fn generate_self_signed<O: Oid>( pub fn generate_self_signed<F, KT: yubikey_signer::KeyType>(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
key: SlotId, key: SlotId,
serial: impl Into<Serial>, serial: SerialNumber,
not_after: Option<DateTime<Utc>>, validity: Validity,
subject: &[RelativeDistinguishedName<'_>], subject: Name,
subject_pki: PublicKeyInfo, subject_pki: SubjectPublicKeyInfoOwned,
extensions: &[x509::Extension<'_, O>], extensions: F,
) -> Result<Self> { ) -> Result<Self>
let serial = serial.into(); where
F: FnOnce(&mut CertificateBuilder<SelfSigned>) -> der::Result<()>,
{
let signer =
yubikey_signer::Signer::<'_, KT>::new(yubikey, key, subject_pki.owned_to_ref())?;
let mut builder =
CertificateBuilder::new(SelfSigned { subject }, serial, validity, subject_pki)
.map_err(|_| Error::KeyError)?;
let mut tbs_cert = Buffer::new(Vec::with_capacity(CB_OBJ_MAX)); // Add custom extensions
extensions(&mut builder)?;
let signature_algorithm = match subject_pki.algorithm() {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => SignatureId::Sha256WithRsaEncryption,
AlgorithmId::EccP256 | AlgorithmId::EccP384 => SignatureId::EcdsaWithSha256,
};
cookie_factory::gen(
x509::write::tbs_certificate(
&serial.to_bytes(),
&signature_algorithm,
// Issuer and subject are the same in self-signed certificates.
subject,
Utc::now(),
not_after,
subject,
&subject_pki,
extensions,
),
tbs_cert.deref_mut(),
)
.expect("can serialize to Vec");
let signature = match signature_algorithm {
SignatureId::Sha256WithRsaEncryption => {
use cookie_factory::{combinator::slice, sequence::tuple};
use x509::{
der::write::{der_octet_string, der_sequence},
write::algorithm_identifier,
};
let em_len = if let AlgorithmId::Rsa1024 = subject_pki.algorithm() {
128
} else {
256
};
let h = Sha256::digest(&tbs_cert);
let t = cookie_factory::gen_simple(
der_sequence((
algorithm_identifier(&DigestId::Sha256),
der_octet_string(&h),
)),
vec![],
)
.expect("can serialize into Vec");
let em = cookie_factory::gen_simple(
tuple((
slice(&[0x00, 0x01]),
slice(&vec![0xff; em_len - t.len() - 3]),
slice(&[0x00]),
slice(t),
)),
vec![],
)
.expect("can serialize to Vec");
sign_data(yubikey, &em, subject_pki.algorithm(), key)
}
SignatureId::EcdsaWithSha256 => sign_data(
yubikey,
&Sha256::digest(&tbs_cert),
subject_pki.algorithm(),
key,
),
}?;
let mut data = Buffer::new(Vec::with_capacity(CB_OBJ_MAX));
cookie_factory::gen(
x509::write::certificate(&tbs_cert, &signature_algorithm, &signature),
data.deref_mut(),
)
.expect("can serialize to Vec");
let (issuer, subject) = parse_x509_certificate(&data)
.map(|(_, cert)| {
(
cert.tbs_certificate.issuer.to_string(),
cert.tbs_certificate.subject.to_string(),
)
})
.expect("We just serialized this correctly");
let cert = Certificate {
serial,
issuer,
subject,
subject_pki,
data,
};
let cert = builder.build(&signer).map_err(|_| Error::KeyError)?;
let cert = Self { cert };
cert.write(yubikey, key, CertInfo::Uncompressed)?; cert.write(yubikey, key, CertInfo::Uncompressed)?;
Ok(cert) Ok(cert)
@@ -459,18 +136,18 @@ impl Certificate {
return Err(Error::InvalidObject); return Err(Error::InvalidObject);
} }
Certificate::from_bytes(buf) Self::from_bytes(buf)
} }
/// Write this certificate into the YubiKey in the given slot /// Write this certificate into the YubiKey in the given slot
pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> { pub fn write(&self, yubikey: &mut YubiKey, slot: SlotId, certinfo: CertInfo) -> Result<()> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, Some(&self.data), certinfo) let data = self.cert.to_der().map_err(|_| Error::InvalidObject)?;
write_certificate(&txn, slot, Some(&data), certinfo)
} }
/// Delete a certificate located at the given slot of the given YubiKey /// Delete a certificate located at the given slot of the given YubiKey
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> { pub fn delete(yubikey: &mut YubiKey, slot: SlotId) -> Result<()> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
write_certificate(&txn, slot, None, CertInfo::Uncompressed) write_certificate(&txn, slot, None, CertInfo::Uncompressed)
@@ -485,55 +162,59 @@ impl Certificate {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
let parsed_cert = match parse_x509_certificate(&cert) { x509_cert::Certificate::from_der(&cert)
Ok((_, cert)) => cert, .map(|cert| Self { cert })
_ => return Err(Error::InvalidObject), .map_err(|_| Error::InvalidObject)
};
let serial = Serial::try_from(parsed_cert.tbs_certificate.serial.to_bytes_be().as_slice())
.map_err(|_| Error::InvalidObject)?;
let issuer = parsed_cert.tbs_certificate.issuer.to_string();
let subject = parsed_cert.tbs_certificate.subject.to_string();
let subject_pki = PublicKeyInfo::parse(&parsed_cert.tbs_certificate.subject_pki)?;
Ok(Certificate {
serial,
issuer,
subject,
subject_pki,
data: cert,
})
}
/// Returns the serial number of the certificate.
pub fn serial(&self) -> &Serial {
&self.serial
} }
/// Returns the Issuer field of the certificate. /// Returns the Issuer field of the certificate.
pub fn issuer(&self) -> &str { pub fn issuer(&self) -> String {
&self.subject self.cert.tbs_certificate().issuer().to_string()
} }
/// Returns the SubjectName field of the certificate. /// Returns the SubjectName field of the certificate.
pub fn subject(&self) -> &str { pub fn subject(&self) -> String {
&self.subject self.cert.tbs_certificate().subject().to_string()
} }
/// Returns the SubjectPublicKeyInfo field of the certificate. /// Returns the SubjectPublicKeyInfo field of the certificate.
pub fn subject_pki(&self) -> &PublicKeyInfo { pub fn subject_pki(&self) -> SubjectPublicKeyInfoRef<'_> {
&self.subject_pki self.cert
} .tbs_certificate()
.subject_public_key_info()
/// Extract the inner buffer .owned_to_ref()
pub fn into_buffer(self) -> Buffer {
self.data
} }
} }
impl AsRef<[u8]> for Certificate { /// A [`BuilderProfile`] for self-signed certificates.
fn as_ref(&self) -> &[u8] { ///
self.data.as_ref() /// This profile has no default extensions.
pub struct SelfSigned {
subject: Name,
}
impl BuilderProfile for SelfSigned {
fn get_issuer(&self, subject: &Name) -> Name {
// RFC 5280 Section 3.2:
//
// > Self-issued certificates are CA certificates in which the issuer and subject
// > are the same entity. [..] Self-signed certificates are self-issued
// > certificates where the digital signature may be verified by the public key
// > bound into the certificate.
subject.clone()
}
fn get_subject(&self) -> Name {
self.subject.clone()
}
fn build_extensions(
&self,
_spk: SubjectPublicKeyInfoRef<'_>,
_issuer_spk: SubjectPublicKeyInfoRef<'_>,
_tbs: &x509_cert::TbsCertificate,
) -> x509_cert::builder::Result<Vec<x509_cert::ext::Extension>> {
Ok(vec![])
} }
} }
@@ -569,113 +250,269 @@ pub(crate) fn write_certificate(
) -> Result<()> { ) -> Result<()> {
let object_id = slot.object_id(); let object_id = slot.object_id();
if data.is_none() { if let Some(data) = data {
return txn.save_object(object_id, &[]); let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
// write compression info and LRC trailer
offset += Tlv::write(&mut buf[offset..], TAG_CERT_COMPRESS, &[certinfo.into()])?;
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
txn.save_object(object_id, &buf[..offset])
} else {
txn.save_object(object_id, &[])
} }
let data = data.unwrap();
let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
// write compression info and LRC trailer
offset += Tlv::write(&mut buf[offset..], TAG_CERT_COMPRESS, &[certinfo.into()])?;
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
txn.save_object(object_id, &buf[..offset])
} }
mod read_pki { pub mod yubikey_signer {
use der_parser::{ //! Signer implementation for yubikey
asn1_rs::Any,
ber::BerObjectContent, use crate::{
der::{parse_der_integer, parse_der_sequence_defined_g, DerObject}, error::{Error, Result},
error::BerError, piv::AlgorithmId,
piv::{sign_data, SlotId},
YubiKey,
};
use der::{
asn1::{Any, OctetString},
oid::db::rfc5912,
Encode, Sequence,
};
use sha2::{Digest, Sha256, Sha384, Sha512};
use signature::Keypair;
use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
use x509_cert::spki::{
self, AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier, EncodePublicKey,
SignatureBitStringEncoding, SubjectPublicKeyInfoRef,
}; };
use nom::{combinator, sequence::pair, IResult};
use rsa::{BigUint, RsaPublicKey};
use super::{OID_NIST_P256, OID_NIST_P384}; type SigResult<T> = core::result::Result<T, signature::Error>;
use crate::{piv::AlgorithmId, Error, Result};
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1): /// Key to be used to sign certificates
/// ```text pub trait KeyType {
/// RSAPublicKey ::= SEQUENCE { /// Error returned when working with signature
/// modulus INTEGER, -- n type Error: Into<signature::Error> + fmt::Debug;
/// publicExponent INTEGER -- e /// The signature type returned by the signer
/// } type Signature: SignatureBitStringEncoding
/// ``` + for<'s> TryFrom<&'s [u8], Error = Self::Error>
pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result<RsaPublicKey> { + fmt::Debug;
fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], (DerObject<'_>, DerObject<'_>), BerError> { /// The public key used to verify signature
parse_der_sequence_defined_g(|i, _| pair(parse_der_integer, parse_der_integer)(i))(i) 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>;
fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> { /// The algorithm used when talking with the yubikey
combinator::map(parse_rsa_pubkey, |(modulus, public_exponent)| { const ALGORITHM: AlgorithmId;
let n = match modulus.content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
};
let e = match public_exponent.content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
};
(n, e) /// Prepare buffer before submitting it for signature
})(i) 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),
};
RsaPublicKey::new(n, e).map_err(|_| Error::InvalidObject)
} }
/// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1): impl KeyType for ed25519_dalek::SigningKey {
/// ```text const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
/// ECParameters ::= CHOICE { type Error = ed25519_dalek::SignatureError;
/// namedCurve OBJECT IDENTIFIER type Signature = ed25519_dalek::Signature;
/// -- implicitCurve NULL type VerifyingKey = ed25519_dalek::VerifyingKey;
/// -- specifiedCurve SpecifiedECDomain type PublicKey = ed25519_dalek::VerifyingKey;
/// }
/// ```
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() { fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
OID_NIST_P256 => Ok(AlgorithmId::EccP256), Ok(Sha512::digest(input).to_vec())
OID_NIST_P384 => Ok(AlgorithmId::EccP384), }
_ => Err(Error::AlgorithmError),
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()?;
let ps = vec![0xff; em_len - t.len() - 3];
assert!(ps.len() >= 8, "spec violation");
let mut out = Vec::with_capacity(em_len);
out.write(&[0x00, 0x01]).map_err(|_| Error::MemoryError)?;
out.write(&ps).map_err(|_| Error::MemoryError)?;
out.write(&[0x00]).map_err(|_| Error::MemoryError)?;
out.write(&t).map_err(|_| Error::MemoryError)?;
Ok(out)
}
}
/// The entrypoint to sign data with the yubikey.
pub struct Signer<'y, KT: KeyType> {
yubikey: RefCell<&'y mut YubiKey>,
key: SlotId,
public_key: KT::VerifyingKey,
}
impl<'y, KT: KeyType> Signer<'y, KT> {
/// Create new Signer
pub fn new(
yubikey: &'y mut YubiKey,
key: SlotId,
subject_pki: SubjectPublicKeyInfoRef<'_>,
) -> Result<Self> {
let public_key = KT::PublicKey::try_from(subject_pki).map_err(|_| Error::ParseError)?;
let public_key = public_key.into();
Ok(Self {
yubikey: RefCell::new(yubikey),
key,
public_key,
})
}
}
impl<KT: KeyType> Keypair for Signer<'_, KT> {
type VerifyingKey = KT::VerifyingKey;
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
self.public_key.clone()
}
}
impl<KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'_, KT> {
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
self.verifying_key().signature_algorithm_identifier()
}
}
impl<KT: KeyType> signature::Signer<KT::Signature> for Signer<'_, KT> {
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
let data = KT::prepare(msg)?;
let out = sign_data(
&mut self.yubikey.borrow_mut(),
&data,
KT::ALGORITHM,
self.key,
)
.map_err(signature::Error::from_source)?;
let out = KT::read_signature(&out)?;
Ok(out)
} }
} }
} }
mod write_pki {
use cookie_factory::{SerializeFn, WriteContext};
use rsa::{BigUint, PublicKeyParts, RsaPublicKey};
use std::io::Write;
use x509::der::write::{der_integer, der_sequence};
/// Encodes a usize as an ASN.1 integer using DER.
fn der_integer_biguint<'a, W: Write + 'a>(num: &'a BigUint) -> impl SerializeFn<W> + 'a {
move |w: WriteContext<W>| der_integer(&num.to_bytes_be())(w)
}
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
/// ```text
/// RSAPublicKey ::= SEQUENCE {
/// modulus INTEGER, -- n
/// publicExponent INTEGER -- e
/// }
/// ```
pub(super) fn rsa_pubkey<'a, W: Write + 'a>(
pubkey: &'a RsaPublicKey,
) -> impl SerializeFn<W> + 'a {
der_sequence((
der_integer_biguint(pubkey.n()),
der_integer_biguint(pubkey.e()),
))
}
}
+17 -19
View File
@@ -30,12 +30,8 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use crate::{Error, Result, YubiKey}; use crate::{Result, YubiKey};
use std::{ use std::fmt::{self, Debug, Display};
fmt::{self, Debug, Display},
str,
};
use subtle_encoding::hex;
use uuid::Uuid; use uuid::Uuid;
/// FASC-N offset /// FASC-N offset
@@ -65,6 +61,7 @@ const OBJ_CHUID: u32 = 0x005f_c102;
/// - 0x35: Exp. Date (hard-coded) /// - 0x35: Exp. Date (hard-coded)
/// - 0x3e: Signature (hard-coded, empty) /// - 0x3e: Signature (hard-coded, empty)
/// - 0xfe: Error Detection Code (hard-coded) /// - 0xfe: Error Detection Code (hard-coded)
#[allow(dead_code)]
const CHUID_TMPL: &[u8] = &[ const CHUID_TMPL: &[u8] = &[
0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58, 0x30, 0x19, 0xd4, 0xe7, 0x39, 0xda, 0x73, 0x9c, 0xed, 0x39, 0xce, 0x73, 0x9d, 0x83, 0x68, 0x58,
0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00, 0x21, 0x08, 0x42, 0x10, 0x84, 0x21, 0xc8, 0x42, 0x10, 0xc3, 0xeb, 0x34, 0x10, 0x00, 0x00, 0x00,
@@ -90,12 +87,13 @@ impl ChuId {
pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] { pub fn fascn(&self) -> [u8; Self::FASCN_SIZE] {
self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)] self.0[CHUID_FASCN_OFFS..(CHUID_FASCN_OFFS + Self::FASCN_SIZE)]
.try_into() .try_into()
.unwrap() .expect("should be FASCN_SIZE")
} }
/// Return Card UUID/GUID component of CHUID /// Return Card UUID/GUID component of CHUID
pub fn uuid(&self) -> Uuid { pub fn uuid(&self) -> Uuid {
Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)]).unwrap() Uuid::from_slice(&self.0[CHUID_GUID_OFFS..(CHUID_GUID_OFFS + 16)])
.expect("should be UUID-sized")
} }
/// Return expiration date component of CHUID /// Return expiration date component of CHUID
@@ -103,24 +101,18 @@ impl ChuId {
pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] { pub fn expiration(&self) -> [u8; Self::EXPIRATION_SIZE] {
self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)] self.0[CHUID_EXPIRATION_OFFS..(CHUID_EXPIRATION_OFFS + Self::EXPIRATION_SIZE)]
.try_into() .try_into()
.unwrap() .expect("should be EXPIRATION_SIZE")
} }
/// Get Cardholder Unique Identifier (CHUID) /// Get Cardholder Unique Identifier (CHUID)
pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> { pub fn get(yubikey: &mut YubiKey) -> Result<ChuId> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
let response = txn.fetch_object(OBJ_CHUID)?; let response = txn.fetch_object(OBJ_CHUID)?;
Ok(response[..Self::BYTE_SIZE].try_into().map(Self)?)
if response.len() != CHUID_TMPL.len() {
return Err(Error::GenericError);
}
Ok(ChuId(response[..Self::BYTE_SIZE].try_into().unwrap()))
} }
/// Set Cardholder Unique Identifier (CHUID) /// Set Cardholder Unique Identifier (CHUID)
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> { pub fn set(&self, yubikey: &mut YubiKey) -> Result<()> {
let mut buf = CHUID_TMPL.to_vec(); let mut buf = CHUID_TMPL.to_vec();
buf[..Self::BYTE_SIZE].copy_from_slice(&self.0); buf[..Self::BYTE_SIZE].copy_from_slice(&self.0);
@@ -130,8 +122,14 @@ impl ChuId {
} }
} }
impl Display for ChuId { impl AsRef<[u8]> for ChuId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn as_ref(&self) -> &[u8] {
f.write_str(str::from_utf8(&hex::encode(&self.0[..])).unwrap()) &self.0
}
}
impl Display for ChuId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::upper::encode_string(self.as_ref()))
} }
} }
+1 -1
View File
@@ -111,7 +111,7 @@ impl Config {
error!("pin timestamp in admin metadata is an invalid size"); error!("pin timestamp in admin metadata is an invalid size");
} else { } else {
// TODO(tarcieri): double-check endianness is correct // TODO(tarcieri): double-check endianness is correct
let pin_last_changed = u32::from_le_bytes(item.try_into().unwrap()); let pin_last_changed = u32::from_le_bytes([item[0], item[1], item[2], item[3]]);
if pin_last_changed != 0 { if pin_last_changed != 0 {
config.pin_last_changed = config.pin_last_changed =
+18 -1
View File
@@ -1,5 +1,7 @@
//! Miscellaneous constant values //! Miscellaneous constant values
#![allow(dead_code)]
/// YubiKey max buffer size /// YubiKey max buffer size
pub(crate) const CB_BUF_MAX: usize = 3072; pub(crate) const CB_BUF_MAX: usize = 3072;
@@ -7,7 +9,6 @@ pub(crate) const CB_BUF_MAX: usize = 3072;
pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9; pub(crate) const CB_OBJ_MAX: usize = CB_BUF_MAX - 9;
pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len pub(crate) const CB_OBJ_TAG_MIN: usize = 2; // 1 byte tag + 1 byte len
#[cfg(feature = "untested")]
pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len pub(crate) const CB_OBJ_TAG_MAX: usize = CB_OBJ_TAG_MIN + 2; // 1 byte tag + 3 bytes len
// Admin tags // Admin tags
@@ -18,3 +19,19 @@ pub(crate) const TAG_ADMIN_TIMESTAMP: u8 = 0x83;
// Protected tags // Protected tags
pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81; pub(crate) const TAG_PROTECTED_FLAGS_1: u8 = 0x81;
pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89; pub(crate) const TAG_PROTECTED_MGM: u8 = 0x89;
// Management
pub(crate) const TAG_USB_SUPPORTED: u8 = 0x01;
pub(crate) const TAG_SERIAL: u8 = 0x02;
pub(crate) const TAG_USB_ENABLED: u8 = 0x03;
pub(crate) const TAG_FORM_FACTOR: u8 = 0x04;
pub(crate) const TAG_VERSION: u8 = 0x05;
pub(crate) const TAG_AUTO_EJECT_TIMEOUT: u8 = 0x06;
pub(crate) const TAG_CHALRESP_TIMEOUT: u8 = 0x07;
pub(crate) const TAG_DEVICE_FLAGS: u8 = 0x08;
pub(crate) const TAG_APP_VERSIONS: u8 = 0x09;
pub(crate) const TAG_CONFIG_LOCK: u8 = 0x0A;
pub(crate) const TAG_UNLOCK: u8 = 0x0B;
pub(crate) const TAG_REBOOT: u8 = 0x0C;
pub(crate) const TAG_NFC_SUPPORTED: u8 = 0x0D;
pub(crate) const TAG_NFC_ENABLED: u8 = 0x0E;
+61 -18
View File
@@ -45,12 +45,21 @@ pub enum Error {
/// Applet error /// Applet error
AppletError, AppletError,
/// We tried to select an applet that could not be found.
AppletNotFound {
/// Human-readable name of the applet.
applet_name: &'static str,
},
/// Argument error /// Argument error
ArgumentError, ArgumentError,
/// Authentication error /// Authentication error
AuthenticationError, AuthenticationError,
/// Error while building a certificate
CertificateBuilder,
/// Generic error /// Generic error
GenericError, GenericError,
@@ -121,31 +130,53 @@ impl Error {
} }
/// Error message /// Error message
pub fn msg(self) -> &'static str { pub fn msg(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Error::AlgorithmError => "algorithm error", Error::AlgorithmError => f.write_str("algorithm error"),
Error::AppletError => "applet error", Error::AppletError => f.write_str("applet error"),
Error::ArgumentError => "argument error", Error::AppletNotFound { applet_name } => {
Error::AuthenticationError => "authentication error", f.write_str(&format!("{applet_name} applet not found"))
Error::GenericError => "generic error", }
Error::InvalidObject => "invalid object", Error::ArgumentError => f.write_str("argument error"),
Error::KeyError => "key error", Error::AuthenticationError => f.write_str("authentication error"),
Error::MemoryError => "memory error", Error::CertificateBuilder => f.write_str("certificate builder error"),
Error::NotSupported => "not supported", Error::GenericError => f.write_str("generic error"),
Error::NotFound => "not found", Error::InvalidObject => f.write_str("invalid object"),
Error::ParseError => "parse error", Error::KeyError => f.write_str("key error"),
Error::PcscError { .. } => "PC/SC error", Error::MemoryError => f.write_str("memory error"),
Error::PinLocked => "PIN locked", Error::NotSupported => f.write_str("not supported"),
Error::RangeError => "range error", Error::NotFound => f.write_str("not found"),
Error::SizeError => "size error", Error::ParseError => f.write_str("parse error"),
Error::WrongPin { .. } => "wrong pin",
Error::PcscError {
inner: Some(pcsc_error),
} => f.write_fmt(format_args!("PC/SC error: {pcsc_error}")),
Error::PcscError { .. } => f.write_str("PC/SC error"),
Error::PinLocked => f.write_str("PIN locked"),
Error::RangeError => f.write_str("range error"),
Error::SizeError => f.write_str("size error"),
Error::WrongPin { .. } => f.write_str("wrong pin"),
} }
} }
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.msg()) self.msg(f)
}
}
impl From<std::array::TryFromSliceError> for Error {
fn from(_: std::array::TryFromSliceError) -> Error {
Error::SizeError
}
}
impl From<std::time::SystemTimeError> for Error {
fn from(_: std::time::SystemTimeError) -> Error {
Error::GenericError
} }
} }
@@ -164,3 +195,15 @@ impl std::error::Error for Error {
} }
} }
} }
impl From<der::Error> for Error {
fn from(_err: der::Error) -> Error {
Error::ParseError
}
}
impl From<x509_cert::builder::Error> for Error {
fn from(_err: x509_cert::builder::Error) -> Error {
Error::CertificateBuilder
}
}
+17 -108
View File
@@ -1,102 +1,17 @@
//! **yubikey.rs**: pure Rust cross-platform host-side driver for [YubiKey] #![doc = include_str!("../README.md")]
//! devices from [Yubico] using the Personal Computer/Smart Card ([PC/SC]) #![doc(
//! interface as provided by the [`pcsc` crate]. html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png"
//! )]
//! # Features #![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! ## Personal Identity Verification (PIV) #![forbid(unsafe_code)]
//! [PIV] is a [NIST] standard for both *signing* and *encryption* #![warn(
//! using SmartCards and SmartCard-based hardware tokens like YubiKeys. clippy::mod_module_files,
//! clippy::unwrap_used,
//! PIV-related functionality can be found in the [`piv`] module. missing_docs,
//! rust_2018_idioms,
//! This library natively implements the protocol used to manage and unused_lifetimes,
//! utilize PIV encryption and signing keys which can be generated, imported, unused_qualifications
//! and stored on YubiKey devices. )]
//!
//! See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
//! on which devices support PIV and the available functionality.
//!
//! # Minimum Supported Rust Version
//! Rust **1.57** or newer.
//!
//! # Supported YubiKeys
//! - [YubiKey 4] series
//! - [YubiKey 5] series
//!
//! NOTE: Nano and USB-C variants of the above are also supported.
//! Pre-YK4 [YubiKey NEO] series is **NOT** supported.
//!
//! # Supported Operating Systems
//! - Linux
//! - macOS
//! - Windows
//!
//! # Supported Algorithms
//! - **Authentication**: `3DES`
//! - **Encryption**:
//! - RSA: `RSA1024`, `RSA2048`
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
//! - **Signatures**:
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
//!
//! NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
//!
//! # Status
//! Functionality which has been successfully tested is available by default.
//!
//! Any functionality which is gated on the `untested` feature has not been
//! properly tested and is not known to function correctly.
//!
//! Please see the [`untested` functionality tracking issue] for current status.
//! We would appreciate any help testing this functionality and removing the
//! `untested` gating as well as writing more automated tests.
//!
//! # History
//! This library is a Rust translation of the [yubico-piv-tool] utility by
//! Yubico, which was originally written in C. It was mechanically translated
//! from C into Rust using [Corrode], and then subsequently heavily
//! refactored into safer, more idiomatic Rust.
//!
//! For more information on [yubico-piv-tool] and background information on how
//! the YubiKey implementation of PIV works in general, see the
//! [Yubico PIV Tool Command Line Guide][piv-tool-guide].
//!
//! # Security Warning
//! No security audits of this crate have ever been performed. Presently it is in
//! an experimental stage and may still contain high-severity issues.
//!
//! USE AT YOUR OWN RISK!
//!
//! # Code of Conduct
//! We abide by the [Contributor Covenant][cc-md] and ask that you do as well.
//!
//! For more information, please see [CODE_OF_CONDUCT.md][cc-md].
//!
//! # License
//! **yubikey.rs** is a fork of and originally a mechanical translation from
//! Yubico's [yubico-piv-tool], a C library/CLI program.
//!
//! The original library was licensed under a [2-Clause BSD License][BSDL],
//! which this library inherits as a derived work.
//!
//! [YubiKey]: https://www.yubico.com/products/yubikey-hardware/
//! [PIV]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-73-4.pdf
//! [Yubico]: https://www.yubico.com/
//! [PC/SC]: https://en.wikipedia.org/wiki/PC/SC
//! [`pcsc` crate]: https://github.com/bluetech/pcsc-rust
//! [NIST]: https://www.nist.gov/
//! [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
//! [YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
//! [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
//! [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
//! [`untested` functionality tracking issue]: https://github.com/iqlusioninc/yubikey.rs/issues/280
//! [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
//! [Corrode]: https://github.com/jameysharp/corrode
//! [piv-tool-guide]: https://www.yubico.com/wp-content/uploads/2016/05/Yubico_PIV_Tool_Command_Line_Guide_en.pdf
//! [cc-web]: https://contributor-covenant.org/
//! [cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
//! [BSDL]: https://opensource.org/licenses/BSD-2-Clause
// Adapted from yubico-piv-tool: // Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/> // <https://github.com/Yubico/yubico-piv-tool/>
@@ -128,13 +43,6 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo.png"
)]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
mod apdu; mod apdu;
mod cccid; mod cccid;
pub mod certificate; pub mod certificate;
@@ -143,11 +51,12 @@ mod config;
mod consts; mod consts;
mod error; mod error;
mod metadata; mod metadata;
mod mgm; pub mod mgm;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
mod mscmap; mod mscmap;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
mod msroots; mod msroots;
mod otp;
pub mod piv; pub mod piv;
mod policy; mod policy;
pub mod reader; pub mod reader;
@@ -162,7 +71,7 @@ pub use crate::{
chuid::ChuId, chuid::ChuId,
config::Config, config::Config,
error::{Error, Result}, error::{Error, Result},
mgm::{MgmKey, MgmType}, mgm::{MgmAlgorithmId, MgmKey, MgmType},
piv::Key, piv::Key,
policy::{PinPolicy, TouchPolicy}, policy::{PinPolicy, TouchPolicy},
reader::Context, reader::Context,
+14 -20
View File
@@ -30,16 +30,15 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::marker::PhantomData; use std::{iter, marker::PhantomData};
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::{serialization::*, transaction::Transaction, Buffer, Error, Result}; use crate::{
consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX},
#[cfg(feature = "untested")] serialization::*,
use crate::consts::{CB_OBJ_MAX, CB_OBJ_TAG_MAX}; transaction::Transaction,
Buffer, Error, Result,
#[cfg(feature = "untested")] };
use std::iter;
const TAG_ADMIN: u8 = 0x80; const TAG_ADMIN: u8 = 0x80;
const TAG_PROTECTED: u8 = 0x88; const TAG_PROTECTED: u8 = 0x88;
@@ -71,7 +70,7 @@ impl<T: MetadataType> Default for Metadata<T> {
fn default() -> Self { fn default() -> Self {
Metadata { Metadata {
inner: Zeroizing::new(vec![]), inner: Zeroizing::new(vec![]),
_marker: PhantomData::default(), _marker: PhantomData,
} }
} }
} }
@@ -82,13 +81,11 @@ impl<T: MetadataType> Metadata<T> {
let data = txn.fetch_object(T::obj_id())?; let data = txn.fetch_object(T::obj_id())?;
Ok(Metadata { Ok(Metadata {
inner: Tlv::parse_single(data, T::tag())?, inner: Tlv::parse_single(data, T::tag())?,
_marker: PhantomData::default(), _marker: PhantomData,
}) })
} }
/// Write metadata /// Write metadata
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> { pub(crate) fn write(&self, txn: &Transaction<'_>) -> Result<()> {
if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX { if self.inner.len() > CB_OBJ_MAX - CB_OBJ_TAG_MAX {
return Err(Error::GenericError); return Err(Error::GenericError);
@@ -105,8 +102,6 @@ impl<T: MetadataType> Metadata<T> {
} }
/// Delete metadata /// Delete metadata
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> { pub(crate) fn delete(txn: &Transaction<'_>) -> Result<()> {
txn.save_object(T::obj_id(), &[]) txn.save_object(T::obj_id(), &[])
} }
@@ -129,8 +124,6 @@ impl<T: MetadataType> Metadata<T> {
} }
/// Set metadata item /// Set metadata item
#[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> { pub(crate) fn set_item(&mut self, tag: u8, item: &[u8]) -> Result<()> {
let mut cb_temp: usize = 0; let mut cb_temp: usize = 0;
let mut tag_temp: u8 = 0; let mut tag_temp: u8 = 0;
@@ -160,8 +153,10 @@ impl<T: MetadataType> Metadata<T> {
// We did not find an existing tag, append // We did not find an existing tag, append
assert_eq!(offset, self.inner.len()); assert_eq!(offset, self.inner.len());
self.inner self.inner.extend(iter::repeat_n(
.extend(iter::repeat(0).take(1 + get_length_size(item.len()) + item.len())); 0,
1 + get_length_size(item.len()) + item.len(),
));
Tlv::write(&mut self.inner[offset..], tag, item)?; Tlv::write(&mut self.inner[offset..], tag, item)?;
return Ok(()); return Ok(());
@@ -196,7 +191,7 @@ impl<T: MetadataType> Metadata<T> {
// Move remaining data // Move remaining data
let orig_len = self.inner.len(); let orig_len = self.inner.len();
if cb_moved > 0 { if cb_moved > 0 {
self.inner.extend(iter::repeat(0).take(cb_moved as usize)); self.inner.extend(iter::repeat_n(0, cb_moved as usize));
} }
self.inner.copy_within( self.inner.copy_within(
next_offset..orig_len, next_offset..orig_len,
@@ -217,7 +212,6 @@ impl<T: MetadataType> Metadata<T> {
} }
/// Get the size of a length tag for the given length /// Get the size of a length tag for the given length
#[cfg(feature = "untested")]
fn get_length_size(length: usize) -> usize { fn get_length_size(length: usize) -> usize {
if length < 0x80 { if length < 0x80 {
1 1
+808 -199
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: /// Defined in Microsoft's Smart Card Minidriver Specification:
/// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)> /// <https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn631754(v=vs.85)>
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MsContainer { pub struct MsContainer {
/// Container name. /// Container name.
@@ -141,7 +140,7 @@ impl MsContainer {
let name_bytes_len = Self::NAME_LEN * 2; let name_bytes_len = Self::NAME_LEN * 2;
for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() { for (i, chunk) in bytes[..name_bytes_len].chunks_exact(2).enumerate() {
name[i] = u16::from_le_bytes(chunk.try_into().unwrap()); name[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
} }
let mut cert_fingerprint = [0u8; 20]; let mut cert_fingerprint = [0u8; 20];
@@ -151,11 +150,10 @@ impl MsContainer {
name, name,
slot: bytes[name_bytes_len].try_into()?, slot: bytes[name_bytes_len].try_into()?,
key_spec: bytes[name_bytes_len + 1], key_spec: bytes[name_bytes_len + 1],
key_size_bits: u16::from_le_bytes( key_size_bits: u16::from_le_bytes([
bytes[(name_bytes_len + 2)..(name_bytes_len + 4)] bytes[name_bytes_len + 2],
.try_into() bytes[name_bytes_len + 3],
.unwrap(), ]),
),
flags: bytes[name_bytes_len + 4], flags: bytes[name_bytes_len + 4],
pin_id: bytes[name_bytes_len + 5], pin_id: bytes[name_bytes_len + 5],
associated_echd_container: bytes[name_bytes_len + 6], associated_echd_container: bytes[name_bytes_len + 6],
@@ -184,7 +182,10 @@ impl MsContainer {
bytes.push(self.pin_id); bytes.push(self.pin_id);
bytes.push(self.associated_echd_container); bytes.push(self.associated_echd_container);
bytes.extend_from_slice(&self.cert_fingerprint); bytes.extend_from_slice(&self.cert_fingerprint);
bytes.as_slice().try_into().unwrap() bytes
.as_slice()
.try_into()
.expect("should be REC_LEN-sized")
} }
} }
+3 -5
View File
@@ -57,7 +57,6 @@ const TAG_MSROOTS_MID: u8 = 0x83;
/// ///
/// For more information, see: /// For more information, see:
/// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots> /// <https://docs.microsoft.com/en-us/windows-hardware/drivers/smartcard/developer-guidelines#-interoperability-with-msroots>
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub struct MsRoots(Vec<u8>); pub struct MsRoots(Vec<u8>);
impl MsRoots { impl MsRoots {
@@ -97,10 +96,9 @@ impl MsRoots {
} }
} }
MsRoots::new(&data).map(Some).map_err(|e| { MsRoots::new(&data)
error!("error parsing msroots: {:?}", e); .map(Some)
e .inspect_err(|e| error!("error parsing msroots: {:?}", e))
})
} }
/// Write `msroots` file to YubiKey /// Write `msroots` file to YubiKey
+5
View File
@@ -0,0 +1,5 @@
/// YubiKey OTP Applet Name
pub(crate) const APPLET_NAME: &str = "YubiKey OTP";
/// YubiKey OTP Applet ID. Needed to query serial on YK4.
pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
+587 -158
View File
@@ -6,10 +6,10 @@
//! Supported algorithms: //! Supported algorithms:
//! //!
//! - **Encryption**: //! - **Encryption**:
//! - RSA: `RSA1024`, `RSA2048` //! - RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
//! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384) //! - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
//! - **Signatures**: //! - **Signatures**:
//! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048` //! - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
//! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384) //! - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
// Adapted from yubico-piv-tool: // Adapted from yubico-piv-tool:
@@ -44,35 +44,44 @@
use crate::{ use crate::{
apdu::{Ins, StatusWords}, apdu::{Ins, StatusWords},
certificate::{self, Certificate, PublicKeyInfo}, certificate::{self, Certificate},
error::{Error, Result}, error::{Error, Result},
mgm::MgmAlgorithmId,
policy::{PinPolicy, TouchPolicy}, policy::{PinPolicy, TouchPolicy},
serialization::*, serialization::*,
setting, setting,
yubikey::YubiKey, yubikey::YubiKey,
Buffer, ObjectId, Buffer, ObjectId,
}; };
use elliptic_curve::sec1::EncodedPoint as EcPublicKey; use elliptic_curve::{sec1::Sec1Point as EcPublicKey, PublicKey};
use log::{debug, error, warn}; use log::{debug, error, warn};
use p256::NistP256; use p256::NistP256;
use p384::NistP384; use p384::NistP384;
use rsa::{BigUint, RsaPublicKey}; use rsa::{pkcs8::EncodePublicKey, BoxedUint, RsaPublicKey};
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{Display, Formatter},
str::FromStr, str::FromStr,
}; };
use x509_cert::{
#[cfg(feature = "untested")] der::{asn1::BitString, Decode},
use { spki::{AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfoOwned},
crate::consts::CB_OBJ_MAX,
num_bigint_dig::traits::ModInverse,
num_integer::Integer,
num_traits::{FromPrimitive, One},
}; };
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use zeroize::Zeroizing; use zeroize::Zeroizing;
#[cfg(feature = "untested")]
use crate::consts::CB_OBJ_MAX;
#[cfg(feature = "untested")]
use rsa::{traits::PrivateKeyParts, RsaPrivateKey};
/// PIV Applet Name
pub(crate) const APPLET_NAME: &str = "PIV";
/// PIV Applet ID
pub(crate) const APPLET_ID: &[u8] = &[0xa0, 0x00, 0x00, 0x03, 0x08];
const CB_ECC_POINTP256: usize = 65; const CB_ECC_POINTP256: usize = 65;
const CB_ECC_POINTP384: usize = 97; const CB_ECC_POINTP384: usize = 97;
@@ -80,6 +89,9 @@ const TAG_RSA_MODULUS: u8 = 0x81;
const TAG_RSA_EXP: u8 = 0x82; const TAG_RSA_EXP: u8 = 0x82;
const TAG_ECC_POINT: u8 = 0x86; const TAG_ECC_POINT: u8 = 0x86;
/// OID for ed25519 and x25519 algorithms
pub const OID_X25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.110");
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
const KEYDATA_LEN: usize = 1024; const KEYDATA_LEN: usize = 1024;
@@ -126,6 +138,9 @@ pub enum SlotId {
/// attestation of other keys generated on device with instruction `f9`. This slot is /// attestation of other keys generated on device with instruction `f9`. This slot is
/// not cleared on reset, but can be overwritten. /// not cleared on reset, but can be overwritten.
Attestation, Attestation,
/// Thse slots are used for management. PIN PUK and Management Key.
Management(ManagementSlotId),
} }
impl TryFrom<u8> for SlotId { impl TryFrom<u8> for SlotId {
@@ -138,7 +153,9 @@ impl TryFrom<u8> for SlotId {
0x9d => Ok(SlotId::KeyManagement), 0x9d => Ok(SlotId::KeyManagement),
0x9e => Ok(SlotId::CardAuthentication), 0x9e => Ok(SlotId::CardAuthentication),
0xf9 => Ok(SlotId::Attestation), 0xf9 => Ok(SlotId::Attestation),
_ => RetiredSlotId::try_from(value).map(SlotId::Retired), _ => RetiredSlotId::try_from(value)
.map(SlotId::Retired)
.or_else(|_| ManagementSlotId::try_from(value).map(SlotId::Management)),
} }
} }
} }
@@ -152,6 +169,7 @@ impl From<SlotId> for u8 {
SlotId::CardAuthentication => 0x9e, SlotId::CardAuthentication => 0x9e,
SlotId::Retired(retired) => retired.into(), SlotId::Retired(retired) => retired.into(),
SlotId::Attestation => 0xf9, SlotId::Attestation => 0xf9,
SlotId::Management(mgmt) => mgmt.into(),
} }
} }
} }
@@ -159,8 +177,9 @@ impl From<SlotId> for u8 {
impl Display for SlotId { impl Display for SlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
SlotId::Retired(r) => write!(f, "{:?}", r), SlotId::Management(r) => write!(f, "{r:?}"),
_ => write!(f, "{:?}", self), SlotId::Retired(r) => write!(f, "{r:?}"),
_ => write!(f, "{self:?}"),
} }
} }
} }
@@ -175,7 +194,10 @@ impl FromStr for SlotId {
"9d" => Ok(SlotId::KeyManagement), "9d" => Ok(SlotId::KeyManagement),
"9e" => Ok(SlotId::CardAuthentication), "9e" => Ok(SlotId::CardAuthentication),
"f9" => Ok(SlotId::Attestation), "f9" => Ok(SlotId::Attestation),
_ => s.parse().map(SlotId::Retired), _ => s
.parse()
.map(SlotId::Management)
.or_else(|_| s.parse().map(SlotId::Retired)),
} }
} }
} }
@@ -189,6 +211,7 @@ impl SlotId {
SlotId::KeyManagement => 0x005f_c10b, SlotId::KeyManagement => 0x005f_c10b,
SlotId::CardAuthentication => 0x005f_c101, SlotId::CardAuthentication => 0x005f_c101,
SlotId::Retired(retired) => retired.object_id(), SlotId::Retired(retired) => retired.object_id(),
SlotId::Management(mgmt) => mgmt.object_id(),
SlotId::Attestation => 0x005f_ff01, SlotId::Attestation => 0x005f_ff01,
} }
} }
@@ -309,7 +332,7 @@ impl From<RetiredSlotId> for u8 {
impl Display for RetiredSlotId { impl Display for RetiredSlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self) write!(f, "{self:?}")
} }
} }
@@ -341,11 +364,94 @@ impl RetiredSlotId {
} }
} }
/// Management slot IDs.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub enum ManagementSlotId {
/// Personal Identification Number (PIN).
Pin,
/// PIN Unblocking Key (PUK).
Puk,
/// Management Key.
Management,
}
impl TryFrom<u8> for ManagementSlotId {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0x80 => Ok(ManagementSlotId::Pin),
0x81 => Ok(ManagementSlotId::Puk),
0x9b => Ok(ManagementSlotId::Management),
_ => Err(Error::InvalidObject),
}
}
}
impl FromStr for ManagementSlotId {
type Err = Error;
fn from_str(value: &str) -> Result<Self> {
match value {
"80" => Ok(ManagementSlotId::Pin),
"81" => Ok(ManagementSlotId::Puk),
"9b" => Ok(ManagementSlotId::Management),
_ => Err(Error::InvalidObject),
}
}
}
impl From<ManagementSlotId> for u8 {
fn from(slot: ManagementSlotId) -> u8 {
match slot {
ManagementSlotId::Pin => 0x80,
ManagementSlotId::Puk => 0x81,
ManagementSlotId::Management => 0x9b,
}
}
}
impl Display for ManagementSlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl ManagementSlotId {
/// Returns the [`ObjectId`] that corresponds to a given [`ManagementSlotId`].
///
/// These correspond to the "BER-TLV Tag" values from Table 3 of [NIST SP 800-73-4]:
/// "Object Identifiers of the PIV Data Objects for Interoperable Use".
///
/// [NIST SP 800-73-4]: https://csrc.nist.gov/publications/detail/sp/800-73/4/final
pub(crate) fn object_id(self) -> ObjectId {
match self {
// Data Object: "X.509 Certificate for Key Management"
// OID: 2.16.840.1.101.3.7.2.1.2
// BER-TLV Tag: '5FC10B'
ManagementSlotId::Pin => 0x005f_c10b,
// Data Object: "Key History Object"
// OID: 2.16.840.1.101.3.7.2.96.96
// BER-TLV Tag: '5FC10C'
ManagementSlotId::Puk => 0x005f_c10c,
// Data Object: "Retired X.509 Certificate for Key Management 3"
// OID: 2.16.840.1.101.3.7.2.16.3
// BER-TLV Tag: '5FC10F'
ManagementSlotId::Management => 0x005f_c10f,
}
}
}
/// Personal Identity Verification (PIV) key slots /// Personal Identity Verification (PIV) key slots
pub const SLOTS: [SlotId; 24] = [ pub const SLOTS: [SlotId; 28] = [
SlotId::Authentication, SlotId::Authentication,
SlotId::Signature, SlotId::Signature,
SlotId::KeyManagement, SlotId::KeyManagement,
SlotId::Attestation,
SlotId::Retired(RetiredSlotId::R1), SlotId::Retired(RetiredSlotId::R1),
SlotId::Retired(RetiredSlotId::R2), SlotId::Retired(RetiredSlotId::R2),
SlotId::Retired(RetiredSlotId::R3), SlotId::Retired(RetiredSlotId::R3),
@@ -367,6 +473,9 @@ pub const SLOTS: [SlotId; 24] = [
SlotId::Retired(RetiredSlotId::R19), SlotId::Retired(RetiredSlotId::R19),
SlotId::Retired(RetiredSlotId::R20), SlotId::Retired(RetiredSlotId::R20),
SlotId::CardAuthentication, SlotId::CardAuthentication,
SlotId::Management(ManagementSlotId::Pin),
SlotId::Management(ManagementSlotId::Puk),
SlotId::Management(ManagementSlotId::Management),
]; ];
/// Algorithm identifiers /// Algorithm identifiers
@@ -378,11 +487,23 @@ pub enum AlgorithmId {
/// 2048-bit RSA. /// 2048-bit RSA.
Rsa2048, Rsa2048,
/// 3072-bit RSA. Requires firmware 5.7 or newer
Rsa3072,
/// 4096-bit RSA. Requires firmware 5.7 or newer
Rsa4096,
/// ECDSA with the NIST P256 curve. /// ECDSA with the NIST P256 curve.
EccP256, EccP256,
/// ECDSA with the NIST P384 curve. /// ECDSA with the NIST P384 curve.
EccP384, EccP384,
/// ED25519
Ed25519,
/// X25519
X25519,
} }
impl TryFrom<u8> for AlgorithmId { impl TryFrom<u8> for AlgorithmId {
@@ -392,8 +513,12 @@ impl TryFrom<u8> for AlgorithmId {
match value { match value {
0x06 => Ok(AlgorithmId::Rsa1024), 0x06 => Ok(AlgorithmId::Rsa1024),
0x07 => Ok(AlgorithmId::Rsa2048), 0x07 => Ok(AlgorithmId::Rsa2048),
0x05 => Ok(AlgorithmId::Rsa3072),
0x16 => Ok(AlgorithmId::Rsa4096),
0x11 => Ok(AlgorithmId::EccP256), 0x11 => Ok(AlgorithmId::EccP256),
0x14 => Ok(AlgorithmId::EccP384), 0x14 => Ok(AlgorithmId::EccP384),
0xE0 => Ok(AlgorithmId::Ed25519),
0xE1 => Ok(AlgorithmId::X25519),
_ => Err(Error::AlgorithmError), _ => Err(Error::AlgorithmError),
} }
} }
@@ -404,8 +529,12 @@ impl From<AlgorithmId> for u8 {
match id { match id {
AlgorithmId::Rsa1024 => 0x06, AlgorithmId::Rsa1024 => 0x06,
AlgorithmId::Rsa2048 => 0x07, AlgorithmId::Rsa2048 => 0x07,
AlgorithmId::Rsa3072 => 0x05,
AlgorithmId::Rsa4096 => 0x16,
AlgorithmId::EccP256 => 0x11, AlgorithmId::EccP256 => 0x11,
AlgorithmId::EccP384 => 0x14, AlgorithmId::EccP384 => 0x14,
AlgorithmId::Ed25519 => 0xE0,
AlgorithmId::X25519 => 0xE1,
} }
} }
} }
@@ -417,22 +546,29 @@ impl AlgorithmId {
} }
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn get_elem_len(self) -> usize { fn get_elem_len(self) -> usize {
match self { match self {
AlgorithmId::Rsa1024 => 64, AlgorithmId::Rsa1024 => 64,
AlgorithmId::Rsa2048 => 128, AlgorithmId::Rsa2048 => 128,
AlgorithmId::Rsa3072 => 192,
AlgorithmId::Rsa4096 => 256,
AlgorithmId::EccP256 => 32, AlgorithmId::EccP256 => 32,
AlgorithmId::EccP384 => 48, AlgorithmId::EccP384 => 48,
AlgorithmId::Ed25519 => 32,
AlgorithmId::X25519 => 32,
} }
} }
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn get_param_tag(self) -> u8 { fn get_param_tag(self) -> u8 {
match self { match self {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => 0x01, AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => 0x01,
AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6, AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6,
AlgorithmId::Ed25519 => 0x07,
AlgorithmId::X25519 => 0x08,
} }
} }
} }
@@ -489,7 +625,7 @@ pub fn generate(
algorithm: AlgorithmId, algorithm: AlgorithmId,
pin_policy: PinPolicy, pin_policy: PinPolicy,
touch_policy: TouchPolicy, touch_policy: TouchPolicy,
) -> Result<PublicKeyInfo> { ) -> Result<SubjectPublicKeyInfoOwned> {
// Keygen messages // Keygen messages
// TODO(tarcieri): extract these into an I18N-handling type? // TODO(tarcieri): extract these into an I18N-handling type?
const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA"; const SZ_SETTING_ROCA: &str = "Enable_Unsafe_Keygen_ROCA";
@@ -504,7 +640,10 @@ pub fn generate(
let setting_roca: setting::Setting; let setting_roca: setting::Setting;
match algorithm { match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => {
if yubikey.version.major == 4 if yubikey.version.major == 4
&& (yubikey.version.minor < 3 && (yubikey.version.minor < 3
|| yubikey.version.minor == 3 && (yubikey.version.patch < 5)) || yubikey.version.minor == 3 && (yubikey.version.patch < 5))
@@ -594,105 +733,11 @@ pub fn generate(
} }
} }
// TODO(str4d): Response is wrapped in an ASN.1 TLV: let value = response.data();
// read_public_key(algorithm, value, true)
// 0x7f 0x49 -> Application | Constructed | 0x49
match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
// It appears that the inner application-specific value returned by the
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
//
// - Use a full ASN.1 parser on the entire datastructure:
//
// RSA 1024:
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
// | tag | len:136 |0x81| len:128 | modulus |0x82|len3| exp |
//
// RSA 2048:
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
// | tag | len:265 |0x81| len:256 | modulus |0x82|len3| exp |
//
// - Skip the first 5 bytes and use crate::serialize::get_length during TLV
// parsing (which treats 128 as a single-byte definite length instead of an
// indefinite length):
//
// RSA 1024:
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
// | |0x81|len128| modulus |0x82|len3| exp |
//
// RSA 2048:
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
// | |0x81| len:256 | modulus |0x82|len3| exp |
//
// Because of the above, treat this for now as a 2-byte ASN.1 tag with a
// 3-byte length.
let data = &response.data()[5..];
let (data, modulus_tlv) = Tlv::parse(data)?;
if modulus_tlv.tag != TAG_RSA_MODULUS {
error!("Failed to parse public key structure (modulus)");
return Err(Error::ParseError);
}
let modulus = modulus_tlv.value.to_vec();
let (_, exp_tlv) = Tlv::parse(data)?;
if exp_tlv.tag != TAG_RSA_EXP {
error!("failed to parse public key structure (public exponent)");
return Err(Error::ParseError);
}
let exp = exp_tlv.value.to_vec();
Ok(PublicKeyInfo::Rsa {
algorithm,
pubkey: RsaPublicKey::new(
BigUint::from_bytes_be(&modulus),
BigUint::from_bytes_be(&exp),
)
.map_err(|_| Error::InvalidObject)?,
})
}
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
let data = &response.data()[3..];
let len = if let AlgorithmId::EccP256 = algorithm {
CB_ECC_POINTP256
} else {
CB_ECC_POINTP384
};
let (_, tlv) = Tlv::parse(data)?;
if tlv.tag != TAG_ECC_POINT {
error!("failed to parse public key structure");
return Err(Error::ParseError);
}
// the curve point should always be determined by the curve
if tlv.value.len() != len {
error!("unexpected length");
return Err(Error::AlgorithmError);
}
let point = tlv.value.to_vec();
match algorithm {
AlgorithmId::EccP256 => {
EcPublicKey::<NistP256>::from_bytes(point).map(PublicKeyInfo::EcP256)
}
AlgorithmId::EccP384 => {
EcPublicKey::<NistP384>::from_bytes(point).map(PublicKeyInfo::EcP384)
}
_ => return Err(Error::AlgorithmError),
}
.map_err(|_| Error::InvalidObject)
}
}
} }
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
fn write_key( fn write_key(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
slot: SlotId, slot: SlotId,
@@ -741,7 +786,6 @@ fn write_key(
/// The key data that makes up an RSA key. /// The key data that makes up an RSA key.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub struct RsaKeyData { pub struct RsaKeyData {
/// The secret prime `p`. /// The secret prime `p`.
p: Buffer, p: Buffer,
@@ -757,41 +801,51 @@ pub struct RsaKeyData {
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
impl RsaKeyData { impl RsaKeyData {
/// Generates a new RSA key data set from two randomly generated, secret, primes. /// Generates a new RSA key data set from two (randomly generated) secret primes.
/// ///
/// Panics if `secret_p` or `secret_q` are invalid primes. /// # Returns
pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Self { /// - `Ok(key_data)` if `secret_p` and `secret_q` are valid primes.
let p = BigUint::from_bytes_be(secret_p); /// - `Err(Error::AlgorithmError)` if `secret_p`/`secret_q` are invalid primes.
let q = BigUint::from_bytes_be(secret_q); pub fn new(secret_p: &[u8], secret_q: &[u8]) -> Result<Self> {
let p = BoxedUint::from_be_slice_vartime(secret_p);
let q = BoxedUint::from_be_slice_vartime(secret_q);
let exp = BoxedUint::from(KEYDATA_RSA_EXP);
let totient = { let mut private_key = RsaPrivateKey::from_p_q(p.clone(), q.clone(), exp)
let p_t = &p - BigUint::one(); .map_err(|_| Error::AlgorithmError)?;
let q_t = &p - BigUint::one(); private_key
.precompute()
.map_err(|_| Error::AlgorithmError)?;
p_t.lcm(&q_t) Ok(RsaKeyData {
}; p: Zeroizing::new(p.to_be_bytes().to_vec()),
q: Zeroizing::new(q.to_be_bytes().to_vec()),
let exp = BigUint::from_u64(KEYDATA_RSA_EXP).unwrap(); dp: Zeroizing::new(
private_key
let d = exp.mod_inverse(&totient).unwrap(); .dp()
let d = d.to_biguint().unwrap(); .expect("invariant violation: precompute should fill the field")
.clone()
// We calculate the optimization values ahead of time, instead of making the user .to_be_bytes()
// do so. .to_vec(),
),
let dp = &d % (&p - BigUint::one()); dq: Zeroizing::new(
let dq = &d % (&q - BigUint::one()); private_key
.dq()
let qinv = q.clone().mod_inverse(&p).unwrap(); .expect("invariant violation: precompute should fill the field")
let (_, qinv) = qinv.to_bytes_be(); .clone()
.to_be_bytes()
RsaKeyData { .to_vec(),
p: Zeroizing::new(p.to_bytes_be()), ),
q: Zeroizing::new(q.to_bytes_be()), qinv: Zeroizing::new(
dp: Zeroizing::new(dp.to_bytes_be()), private_key
dq: Zeroizing::new(dq.to_bytes_be()), .qinv()
qinv: Zeroizing::new(qinv), .expect("invariant violation: precompute should fill the field")
} .clone()
.retrieve()
.to_be_bytes()
.to_vec(),
),
})
} }
fn total_len(&self) -> usize { fn total_len(&self) -> usize {
@@ -801,9 +855,8 @@ impl RsaKeyData {
/// Imports a private RSA encryption or signing key into the YubiKey. /// Imports a private RSA encryption or signing key into the YubiKey.
/// ///
/// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048`. /// Errors if `algorithm` isn't `AlgorithmId::Rsa1024` or `AlgorithmId::Rsa2048` or `AlgorithmId::Rsa3072` or `AlgorithmId::Rsa4096`.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn import_rsa_key( pub fn import_rsa_key(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
slot: SlotId, slot: SlotId,
@@ -813,7 +866,10 @@ pub fn import_rsa_key(
pin_policy: PinPolicy, pin_policy: PinPolicy,
) -> Result<()> { ) -> Result<()> {
match algorithm { match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => (), AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => (),
_ => return Err(Error::AlgorithmError), _ => return Err(Error::AlgorithmError),
} }
@@ -838,7 +894,6 @@ pub fn import_rsa_key(
/// ///
/// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`. /// Errors if `algorithm` isn't `AlgorithmId::EccP256` or ` AlgorithmId::EccP384`.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn import_ecc_key( pub fn import_ecc_key(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
slot: SlotId, slot: SlotId,
@@ -863,11 +918,38 @@ pub fn import_ecc_key(
Ok(()) Ok(())
} }
/// Imports a private ECDH/EdDSA encryption or signing key into the YubiKey.
///
/// Errors if `algorithm` isn't `AlgorithmId::Ed25519` or ` AlgorithmId::X25519`.
#[cfg(feature = "untested")]
pub fn import_cv_key(
yubikey: &mut YubiKey,
slot: SlotId,
algorithm: AlgorithmId,
key_data: &[u8],
touch_policy: TouchPolicy,
pin_policy: PinPolicy,
) -> Result<()> {
match algorithm {
AlgorithmId::Ed25519 | AlgorithmId::X25519 => (),
_ => return Err(Error::AlgorithmError),
}
if key_data.len() > KEYDATA_LEN {
return Err(Error::SizeError);
}
let params = vec![key_data];
write_key(yubikey, slot, params, pin_policy, touch_policy, algorithm)?;
Ok(())
}
/// Generate an attestation certificate for a stored key. /// Generate an attestation certificate for a stored key.
/// ///
/// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html> /// <https://developers.yubico.com/PIV/Introduction/PIV_attestation.html>
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer> { pub fn attest(yubikey: &mut YubiKey, key: SlotId) -> Result<Buffer> {
let templ = [0, Ins::Attest.code(), key.into(), 0]; let templ = [0, Ins::Attest.code(), key.into(), 0];
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
@@ -903,7 +985,6 @@ pub fn sign_data(
/// Decrypt data using a PIV key. /// Decrypt data using a PIV key.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn decrypt_data( pub fn decrypt_data(
yubikey: &mut YubiKey, yubikey: &mut YubiKey,
input: &[u8], input: &[u8],
@@ -915,3 +996,351 @@ pub fn decrypt_data(
// don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS // don't attempt to reselect in crypt operations to avoid problems with PIN_ALWAYS
txn.authenticated_command(input, algorithm, key, true) txn.authenticated_command(input, algorithm, key, true)
} }
/// Read metadata
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
let txn = yubikey.begin_transaction()?;
txn.get_metadata(slot)
}
/// Metadata from a slot
#[derive(Debug)]
pub struct SlotMetadata {
/// Algorithm / Type of key
pub algorithm: SlotAlgorithmId,
/// PIN and touch policy
pub policy: Option<(PinPolicy, TouchPolicy)>,
/// Imported or generated key
pub origin: Option<Origin>,
/// Pub key of the key
pub public: Option<SubjectPublicKeyInfoOwned>,
/// Whether PIN PUK and management key are default
pub default: Option<bool>,
/// Number of retries left
pub retries: Option<Retries>,
}
impl TryFrom<Buffer> for SlotMetadata {
type Error = Error;
fn try_from(buf: Buffer) -> Result<Self> {
use nom::{
combinator::{eof, map_res},
multi::fold_many1,
number::complete::u8,
Parser,
};
let out = fold_many1(
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|| {
Ok(SlotMetadata {
algorithm: SlotAlgorithmId::PinPuk,
policy: None,
origin: None,
public: None,
default: None,
retries: None,
})
},
|acc: Result<SlotMetadata>, tlv| match acc {
Ok(mut metadata) => match tlv.tag {
1 => {
metadata.algorithm = SlotAlgorithmId::try_from(tlv.value[0])?;
Ok(metadata)
}
2 => {
fn policy_parser(
i: &[u8],
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
let (i, pin) = map_res(u8, PinPolicy::try_from).parse(i)?;
let (i, touch) = map_res(u8, TouchPolicy::try_from).parse(i)?;
let (i, _) = eof(i)?;
Ok((i, (pin, touch)))
}
metadata.policy =
Some(policy_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
Ok(metadata)
}
3 => {
fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> {
let (i, origin) = map_res(u8, Origin::try_from).parse(i)?;
let (i, _) = eof(i)?;
Ok((i, origin))
}
metadata.origin =
Some(origin_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
Ok(metadata)
}
4 => {
match metadata.algorithm {
SlotAlgorithmId::Asymmetric(alg) => {
metadata.public = Some(read_public_key(alg, tlv.value, false)?);
}
_ => Err(Error::ParseError)?,
}
Ok(metadata)
}
5 => {
fn default_parser(i: &[u8]) -> nom::IResult<&[u8], bool> {
let (i, default) = u8(i)?;
let (i, _) = eof(i)?;
Ok((i, default == 1))
}
metadata.default =
Some(default_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
Ok(metadata)
}
6 => {
fn retries_parser(i: &[u8]) -> nom::IResult<&[u8], Retries> {
let (i, retry_count) = u8(i)?;
let (i, remaining_count) = u8(i)?;
let (i, _) = eof(i)?;
Ok((
i,
Retries {
retry_count,
remaining_count,
},
))
}
metadata.retries =
Some(retries_parser(tlv.value).map_err(|_| Error::ParseError)?.1);
Ok(metadata)
}
_unsupported => {
// New unsupported tags
// https://docs.yubico.com/yesdk/users-manual/application-piv/apdu/metadata.html
Ok(metadata)
}
},
err => err,
},
)
.parse(buf.as_ref());
match out {
Ok((_, res)) => res,
_ => Err(Error::ParseError),
}
}
}
/// The number of retries used and remaining.
#[derive(Debug, PartialEq, Eq)]
pub struct Retries {
/// TODO
pub retry_count: u8,
/// Remaining attempts
pub remaining_count: u8,
}
/// Origin of a slot
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Origin {
/// The key has been imported
Imported,
/// The key has been generated on the YubiKey
Generated,
}
impl TryFrom<u8> for Origin {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
1 => Ok(Origin::Generated),
2 => Ok(Origin::Imported),
_ => Err(Error::GenericError),
}
}
}
fn read_public_key(
algorithm: AlgorithmId,
input: &[u8],
skip_asn1_tag: bool,
) -> Result<SubjectPublicKeyInfoOwned> {
// TODO(str4d): Response is wrapped in an ASN.1 TLV:
//
// 0x7f 0x49 -> Application | Constructed | 0x49
match algorithm {
AlgorithmId::X25519 | AlgorithmId::Ed25519 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
let data = if skip_asn1_tag { &input[3..] } else { input };
let (_, tlv) = Tlv::parse(data)?;
let pk_data: [u8; 32] = tlv.value.try_into().map_err(|_| Error::InvalidObject)?;
match algorithm {
AlgorithmId::Ed25519 => SubjectPublicKeyInfoOwned::from_der(
ed25519_dalek::VerifyingKey::from_bytes(&pk_data)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der()
.map_err(|_| Error::InvalidObject)?
.as_bytes(),
)
.map_err(|_| Error::InvalidObject),
AlgorithmId::X25519 => Ok(SubjectPublicKeyInfoOwned {
algorithm: AlgorithmIdentifier {
oid: OID_X25519,
parameters: None,
},
subject_public_key: BitString::from_bytes(
x25519_dalek::PublicKey::from(pk_data).as_bytes(),
)
.map_err(|_| Error::InvalidObject)?,
}),
_ => Err(Error::AlgorithmError),
}
}
AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => {
// It appears that the inner application-specific value returned by the
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
//
// - Use a full ASN.1 parser on the entire datastructure:
//
// RSA 1024:
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
// | tag | len:136 |0x81| len:128 | modulus |0x82|len3| exp |
//
// RSA 2048:
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
// | tag | len:265 |0x81| len:256 | modulus |0x82|len3| exp |
//
// - Skip the first 5 bytes and use crate::serialize::get_length during TLV
// parsing (which treats 128 as a single-byte definite length instead of an
// indefinite length):
//
// RSA 1024:
// [127, 73, 129, 136, 129, 129, 128, [ 128 octets ], 130, 3, 1, 0, 1]
// | |0x81|len128| modulus |0x82|len3| exp |
//
// RSA 2048:
// [127, 73, 130, 1, 9, 129, 130, 1, 0, [ 256 octets ], 130, 3, 1, 0, 1]
// | |0x81| len:256 | modulus |0x82|len3| exp |
//
// Because of the above, treat this for now as a 2-byte ASN.1 tag with a
// 3-byte length.
let data = if skip_asn1_tag { &input[5..] } else { input };
let (data, modulus_tlv) = Tlv::parse(data)?;
if modulus_tlv.tag != TAG_RSA_MODULUS {
error!("Failed to parse public key structure (modulus)");
return Err(Error::ParseError);
}
let modulus = modulus_tlv.value.to_vec();
let (_, exp_tlv) = Tlv::parse(data)?;
if exp_tlv.tag != TAG_RSA_EXP {
error!("failed to parse public key structure (public exponent)");
return Err(Error::ParseError);
}
let exp = exp_tlv.value.to_vec();
let pubkey = RsaPublicKey::new(
BoxedUint::from_be_slice_vartime(&modulus),
BoxedUint::from_be_slice_vartime(&exp),
)
.map_err(|_| Error::InvalidObject)?;
Ok(SubjectPublicKeyInfoOwned::from_der(
pubkey
.to_public_key_der()
.map_err(|_| Error::ParseError)?
.as_bytes(),
)?)
}
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
let data = if skip_asn1_tag { &input[3..] } else { input };
let len = if let AlgorithmId::EccP256 = algorithm {
CB_ECC_POINTP256
} else {
CB_ECC_POINTP384
};
let (_, tlv) = Tlv::parse(data)?;
if tlv.tag != TAG_ECC_POINT {
error!("failed to parse public key structure");
return Err(Error::ParseError);
}
// the curve point should always be determined by the curve
if tlv.value.len() != len {
error!("unexpected length");
return Err(Error::AlgorithmError);
}
let point = tlv.value.to_vec();
let pubkey = match algorithm {
AlgorithmId::EccP256 => PublicKey::<NistP256>::try_from(
EcPublicKey::<NistP256>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der(),
AlgorithmId::EccP384 => PublicKey::<NistP384>::try_from(
EcPublicKey::<NistP384>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der(),
_ => return Err(Error::AlgorithmError),
}
.map_err(|_| Error::InvalidObject)?;
Ok(SubjectPublicKeyInfoOwned::from_der(pubkey.as_bytes())?)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Algorithms as reported by the metadata command.
pub enum SlotAlgorithmId {
/// Used on PIN and PUK slots.
PinPuk,
/// Used on the key management slot.
Management(MgmAlgorithmId),
/// Used on all other slots.
Asymmetric(AlgorithmId),
}
impl TryFrom<u8> for SlotAlgorithmId {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0xff => Ok(SlotAlgorithmId::PinPuk),
oth => MgmAlgorithmId::try_from(oth)
.map(SlotAlgorithmId::Management)
.or_else(|_| AlgorithmId::try_from(oth).map(SlotAlgorithmId::Asymmetric)),
}
}
}
impl From<SlotAlgorithmId> for u8 {
fn from(id: SlotAlgorithmId) -> u8 {
match id {
SlotAlgorithmId::PinPuk => 0xff,
SlotAlgorithmId::Management(oth) => oth.into(),
SlotAlgorithmId::Asymmetric(oth) => oth.into(),
}
}
}
+31 -3
View File
@@ -1,12 +1,12 @@
//! Enums representing key policies. //! Enums representing key policies.
use crate::{serialization::Tlv, Result}; use crate::{serialization::Tlv, Error, Result};
/// Specifies how often the PIN needs to be entered for access to the credential in a /// Specifies how often the PIN needs to be entered for access to the credential in a
/// given slot. /// given slot.
/// ///
/// This policy must be set when keys are generated or imported, and cannot be changed later. /// This policy must be set when keys are generated or imported, and cannot be changed later.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PinPolicy { pub enum PinPolicy {
/// Use the default PIN policy for the slot. See the slot's documentation for details. /// Use the default PIN policy for the slot. See the slot's documentation for details.
Default, Default,
@@ -35,6 +35,20 @@ impl From<PinPolicy> for u8 {
} }
} }
impl TryFrom<u8> for PinPolicy {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0 => Ok(PinPolicy::Default),
1 => Ok(PinPolicy::Never),
2 => Ok(PinPolicy::Once),
3 => Ok(PinPolicy::Always),
_ => Err(Error::GenericError),
}
}
}
impl PinPolicy { impl PinPolicy {
/// Writes the `PinPolicy` in the format the YubiKey expects during key generation or /// Writes the `PinPolicy` in the format the YubiKey expects during key generation or
/// importation. /// importation.
@@ -50,7 +64,7 @@ impl PinPolicy {
/// addition to the [`PinPolicy`]. /// addition to the [`PinPolicy`].
/// ///
/// This policy must be set when keys are generated or imported, and cannot be changed later. /// This policy must be set when keys are generated or imported, and cannot be changed later.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TouchPolicy { pub enum TouchPolicy {
/// Use the default touch policy for the slot. /// Use the default touch policy for the slot.
Default, Default,
@@ -90,3 +104,17 @@ impl TouchPolicy {
} }
} }
} }
impl TryFrom<u8> for TouchPolicy {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0 => Ok(TouchPolicy::Default),
1 => Ok(TouchPolicy::Never),
2 => Ok(TouchPolicy::Always),
3 => Ok(TouchPolicy::Cached),
_ => Err(Error::GenericError),
}
}
}
+12 -3
View File
@@ -1,9 +1,10 @@
//! Support for enumerating available PC/SC card readers. //! Support for enumerating available PC/SC card readers.
use crate::{Result, YubiKey}; use crate::{Error, Result, YubiKey};
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::CStr, ffi::CStr,
fmt,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@@ -19,6 +20,12 @@ pub struct Context {
reader_names: Vec<u8>, reader_names: Vec<u8>,
} }
impl fmt::Debug for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context").finish_non_exhaustive()
}
}
impl Context { impl Context {
/// Open a PC/SC context, which can be used to enumerate available PC/SC /// Open a PC/SC context, which can be used to enumerate available PC/SC
/// readers (which can be used to connect to YubiKeys). /// readers (which can be used to connect to YubiKeys).
@@ -36,7 +43,8 @@ impl Context {
let Self { ctx, reader_names } = self; let Self { ctx, reader_names } = self;
let reader_cstrs: Vec<_> = { let reader_cstrs: Vec<_> = {
let c = ctx.lock().unwrap(); // TODO(tarcieri): better error?
let c = ctx.lock().map_err(|_| Error::GenericError)?;
// ensure PC/SC context is valid // ensure PC/SC context is valid
c.is_valid()?; c.is_valid()?;
@@ -83,7 +91,8 @@ impl<'ctx> Reader<'ctx> {
/// Connect to this reader, returning its `pcsc::Card`. /// Connect to this reader, returning its `pcsc::Card`.
pub(crate) fn connect(&self) -> Result<pcsc::Card> { pub(crate) fn connect(&self) -> Result<pcsc::Card> {
let ctx = self.ctx.lock().unwrap(); // TODO(tarcieri): better error?
let ctx = self.ctx.lock().map_err(|_| Error::GenericError)?;
Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?) Ok(ctx.connect(self.name, pcsc::ShareMode::Shared, pcsc::Protocols::T1)?)
} }
} }
+3 -8
View File
@@ -41,7 +41,7 @@ use std::{
}; };
/// Source of how a setting was configured. /// Source of how a setting was configured.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SettingSource { pub enum SettingSource {
/// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars. /// User-specified setting: sourced via `YUBIKEY_PIV_*` environment vars.
User, User,
@@ -51,15 +51,10 @@ pub enum SettingSource {
Admin, Admin,
/// Default setting. /// Default setting.
#[default]
Default, Default,
} }
impl Default for SettingSource {
fn default() -> Self {
Self::Default
}
}
/// Setting booleans: configuration values sourced from a file or the environment. /// Setting booleans: configuration values sourced from a file or the environment.
/// ///
/// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a /// These can be configured globally in `/etc/yubico/yubikeypiv.conf` by a
@@ -122,7 +117,7 @@ impl Setting {
/// Get a setting boolean from an environment variable /// Get a setting boolean from an environment variable
fn from_env(key: &str) -> Option<Self> { fn from_env(key: &str) -> Option<Self> {
env::var(format!("YUBIKEY_PIV_{}", key)) env::var(format!("YUBIKEY_PIV_{key}"))
.ok() .ok()
.map(|value| Setting { .map(|value| Setting {
source: SettingSource::User, source: SettingSource::User,
+200 -105
View File
@@ -5,7 +5,9 @@ use crate::{
apdu::{Apdu, Ins, StatusWords}, apdu::{Apdu, Ins, StatusWords},
consts::{CB_BUF_MAX, CB_OBJ_MAX}, consts::{CB_BUF_MAX, CB_OBJ_MAX},
error::{Error, Result}, error::{Error, Result},
piv::{AlgorithmId, SlotId}, mgm::MgmKey,
otp,
piv::{self, AlgorithmId, SlotId},
serialization::*, serialization::*,
yubikey::*, yubikey::*,
Buffer, ObjectId, Buffer, ObjectId,
@@ -14,13 +16,7 @@ use log::{error, trace};
use zeroize::Zeroizing; use zeroize::Zeroizing;
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
use crate::mgm::{MgmKey, DES_LEN_3DES}; use crate::mgm::{DeviceConfig, DeviceInfo, Lock};
/// PIV Applet ID
const PIV_AID: [u8; 5] = [0xa0, 0x00, 0x00, 0x03, 0x08];
/// YubiKey OTP Applet ID. Needed to query serial on YK4.
const YK_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01];
const CB_PIN_MAX: usize = 8; const CB_PIN_MAX: usize = 8;
@@ -65,23 +61,34 @@ impl<'tx> Transaction<'tx> {
Ok(recv_buffer) Ok(recv_buffer)
} }
/// Select PIV application.
pub fn select_piv_application(&self) -> Result<()> {
self.select_application(
piv::APPLET_ID,
piv::APPLET_NAME,
"failed selecting application",
)
}
/// Select application. /// Select application.
pub fn select_application(&self) -> Result<()> { pub fn select_application(
&self,
applet: &[u8],
applet_name: &'static str,
error: &'static str,
) -> Result<()> {
let response = Apdu::new(Ins::SelectApplication) let response = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(&PIV_AID) .data(applet)
.transmit(self, 0xFF) .transmit(self, 0xFF)
.map_err(|e| { .inspect_err(|e| error!("failed communicating with card: '{}'", e))?;
error!("failed communicating with card: '{}'", e);
e
})?;
if !response.is_success() { if !response.is_success() {
error!( error!("{}: {:04x}", error, response.status_words().code());
"failed selecting application: {:04x}", return Err(match response.status_words() {
response.status_words().code() StatusWords::NotFoundError => Error::AppletNotFound { applet_name },
); _ => Error::GenericError,
return Err(Error::GenericError); });
} }
Ok(()) Ok(())
@@ -92,74 +99,81 @@ impl<'tx> Transaction<'tx> {
// get version from device // get version from device
let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?; let response = Apdu::new(Ins::GetVersion).transmit(self, 261)?;
if !response.is_success() { if !response.is_success() || response.data().is_empty() {
return Err(Error::GenericError); return Err(Error::GenericError);
} }
if response.data().len() < 3 { Ok(response.data()[..3].try_into().map(Version::new)?)
return Err(Error::SizeError);
}
Ok(Version::new(response.data()[..3].try_into().unwrap()))
} }
/// Get YubiKey device serial number. /// Get YubiKey device serial number.
pub fn get_serial(&self, version: Version) -> Result<Serial> { pub fn get_serial(&self, version: Version) -> Result<Serial> {
let response = if version.major < 5 { match version.major {
// YK4 requires switching to the yk applet to retrieve the serial // YK4 requires switching to the YK applet to retrieve the serial
let sw = Apdu::new(Ins::SelectApplication) 4 => {
.p1(0x04) self.select_application(
.data(&YK_AID) otp::APPLET_ID,
.transmit(self, 0xFF)? otp::APPLET_NAME,
.status_words(); "failed selecting yk application",
)?;
if !sw.is_success() { let response = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
error!("failed selecting yk application: {:04x}", sw.code());
return Err(Error::GenericError); if !response.is_success() {
// TODO(tarcieri): still reselect the PIV applet in this case?
error!(
"failed retrieving serial number: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
// reselect the PIV applet
self.select_application(
piv::APPLET_ID,
piv::APPLET_NAME,
"failed selecting application",
)?;
response.data().try_into()
} }
let resp = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?;
if !resp.is_success() {
error!(
"failed retrieving serial number: {:04x}",
resp.status_words().code()
);
return Err(Error::GenericError);
}
// reselect the PIV applet
let sw = Apdu::new(Ins::SelectApplication)
.p1(0x04)
.data(&PIV_AID)
.transmit(self, 0xFF)?
.status_words();
if !sw.is_success() {
error!("failed selecting application: {:04x}", sw.code());
return Err(Error::GenericError);
}
resp
} else {
// YK5 implements getting the serial as a PIV applet command (0xf8) // YK5 implements getting the serial as a PIV applet command (0xf8)
let resp = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?; 5 => {
let response = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
if !resp.is_success() { if !response.is_success() {
error!( error!(
"failed retrieving serial number: {:04x}", "failed retrieving serial number: {:04x}",
resp.status_words().code() response.status_words().code()
); );
return Err(Error::GenericError); return Err(Error::GenericError);
}
response.data().try_into()
} }
resp // Other versions unsupported
}; _ => Err(Error::NotSupported),
}
}
response.data()[..4] /// Read metadata
.try_into() pub(crate) fn get_metadata(&self, slot: SlotId) -> Result<piv::SlotMetadata> {
.map(|serial| Serial::from(u32::from_be_bytes(serial))) let response = Apdu::new(Ins::GetMetadata)
.map_err(|_| Error::SizeError) .p2(slot.into())
.transmit(self, CB_OBJ_MAX)?;
match response.status_words() {
StatusWords::Success => {
let buf = Buffer::new(response.data().into());
piv::SlotMetadata::try_from(buf)
}
StatusWords::ReferenceDataNotFoundError => Err(Error::NotFound),
// Requires firmware 5.2.3
StatusWords::NotSupportedError => Err(Error::NotSupported),
_ => Err(Error::GenericError),
}
} }
/// Verify device PIN. /// Verify device PIN.
@@ -177,7 +191,7 @@ impl<'tx> Transaction<'tx> {
if !pin.is_empty() { if !pin.is_empty() {
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]); let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
data[0..pin.len()].copy_from_slice(pin); data[0..pin.len()].copy_from_slice(pin);
query.data(data.as_ref()); query.data(data.as_slice());
} }
let response = query.transmit(self, 261)?; let response = query.transmit(self, 261)?;
@@ -234,19 +248,18 @@ impl<'tx> Transaction<'tx> {
} }
/// Set the management key (MGM). /// Set the management key (MGM).
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> { pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
let p2 = if require_touch { 0xfe } else { 0xff }; let p2 = if require_touch { 0xfe } else { 0xff };
let mut data = [0u8; DES_LEN_3DES + 3]; let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
data[0] = ALGO_3DES; data.push(new_key.algorithm_id().into());
data[1] = KEY_CARDMGM; data.push(KEY_CARDMGM);
data[2] = DES_LEN_3DES as u8; data.push(new_key.key_size());
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref()); data.extend_from_slice(new_key.as_ref());
let status_words = Apdu::new(Ins::SetMgmKey) let status_words = Apdu::new(Ins::SetMgmKey)
.params(0xff, p2) .params(0xff, p2)
.data(&data) .data(data)
.transmit(self, 261)? .transmit(self, 261)?
.status_words(); .status_words();
@@ -275,14 +288,23 @@ impl<'tx> Transaction<'tx> {
let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()]; let templ = [0, Ins::Authenticate.code(), algorithm.into(), key.into()];
match algorithm { match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { AlgorithmId::Rsa1024 => {
let key_len = if let AlgorithmId::Rsa1024 = algorithm { if in_len != 128 {
128 return Err(Error::SizeError);
} else { }
256 }
}; AlgorithmId::Rsa2048 => {
if in_len != 256 {
if in_len != key_len { return Err(Error::SizeError);
}
}
AlgorithmId::Rsa3072 => {
if in_len != 384 {
return Err(Error::SizeError);
}
}
AlgorithmId::Rsa4096 => {
if in_len != 512 {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
} }
@@ -298,6 +320,19 @@ impl<'tx> Transaction<'tx> {
return Err(Error::SizeError); return Err(Error::SizeError);
} }
} }
AlgorithmId::X25519 => {
if !decipher {
return Err(Error::NotSupported);
}
if in_len != 32 {
return Err(Error::SizeError);
}
}
AlgorithmId::Ed25519 => {
if decipher {
return Err(Error::NotSupported);
}
}
} }
let bytes = if in_len < 0x80 { let bytes = if in_len < 0x80 {
@@ -314,7 +349,9 @@ impl<'tx> Transaction<'tx> {
Tlv::write( Tlv::write(
&mut buf[2..], &mut buf[2..],
match (algorithm, decipher) { match (algorithm, decipher) {
(AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85, (AlgorithmId::EccP256, true)
| (AlgorithmId::EccP384, true)
| (AlgorithmId::X25519, true) => 0x85,
_ => 0x81, _ => 0x81,
}, },
sign_in sign_in
@@ -326,10 +363,7 @@ impl<'tx> Transaction<'tx> {
let response = self let response = self
.transfer_data(&templ, &indata[..offset], 1024) .transfer_data(&templ, &indata[..offset], 1024)
.map_err(|e| { .inspect_err(|e| error!("sign command failed to communicate: {}", e))?;
error!("sign command failed to communicate: {}", e);
e
})?;
if !response.is_success() { if !response.is_success() {
error!("failed sign command with code {:x}", response.code()); error!("failed sign command with code {:x}", response.code());
@@ -387,11 +421,12 @@ impl<'tx> Transaction<'tx> {
.data(&in_data[in_offset..(in_offset + this_size)]) .data(&in_data[in_offset..(in_offset + this_size)])
.transmit(self, 261)?; .transmit(self, 261)?;
sw = response.status_words().code(); sw = response.status_words();
if !response.is_success() && (sw >> 8 != 0x61) { match sw {
StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
// TODO(tarcieri): is this really OK? // TODO(tarcieri): is this really OK?
return Ok(Response::new(sw.into(), out_data)); _ => return Ok(Response::new(sw, out_data)),
} }
if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) { if !out_data.is_empty() && (out_data.len() - response.data().len() > max_out) {
@@ -412,17 +447,15 @@ impl<'tx> Transaction<'tx> {
} }
} }
while sw >> 8 == 0x61 { while let StatusWords::BytesRemaining { len } = sw {
trace!( trace!("The card indicates there is {} bytes more data for us", len);
"The card indicates there is {} bytes more data for us",
sw & 0xff
);
let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?; let response = Apdu::new(Ins::GetResponseApdu).transmit(self, 261)?;
sw = response.status_words().code(); sw = response.status_words();
if sw != StatusWords::Success.code() && (sw >> 8 != 0x61) { match sw {
return Ok(Response::new(sw.into(), vec![])); StatusWords::Success | StatusWords::BytesRemaining { .. } => (),
_ => return Ok(Response::new(sw, vec![])),
} }
if out_data.len() + response.data().len() > max_out { if out_data.len() + response.data().len() > max_out {
@@ -438,7 +471,7 @@ impl<'tx> Transaction<'tx> {
out_data.extend_from_slice(&response.data()[..response.data().len()]); out_data.extend_from_slice(&response.data()[..response.data().len()]);
} }
Ok(Response::new(sw.into(), out_data)) Ok(Response::new(sw, out_data))
} }
/// Fetch an object. /// Fetch an object.
@@ -503,4 +536,66 @@ impl<'tx> Transaction<'tx> {
_ => Err(Error::GenericError), _ => Err(Error::GenericError),
} }
} }
/// Write configuration to the YubiKey
#[cfg(feature = "untested")]
pub fn write_config(
&mut self,
version: Version,
config: DeviceConfig,
current_lock: Option<Lock>,
new_lock: Option<Lock>,
) -> Result<()> {
if version
< (Version {
major: 5,
minor: 0,
patch: 0,
})
{
return Err(Error::NotSupported);
}
let data = config.as_tlv(true, current_lock, new_lock)?;
let response = Apdu::new(Ins::WriteConfig)
.params(0x00, 0x00)
.data(&data)
.transmit(self, 2)?;
if !response.is_success() {
error!(
"Unable to write_config: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
Ok(())
}
/// Write configuration to the YubiKey
#[cfg(feature = "untested")]
pub fn read_config(&mut self) -> Result<DeviceInfo> {
let mut data = [0u8; CB_BUF_MAX];
let mut len = data.len();
let data_remaining = &mut data[..];
len -= data_remaining.len();
let response = Apdu::new(Ins::ReadConfig)
.params(0x00, 0x00)
.data(&data[..len])
.transmit(self, CB_BUF_MAX + 2)?;
if !response.is_success() {
error!(
"Unable to read configuration: {:04x}",
response.status_words().code()
);
return Err(Error::GenericError);
}
let data = response.data();
DeviceInfo::parse(data)
}
} }
+254 -116
View File
@@ -41,10 +41,12 @@ use crate::{
reader::{Context, Reader}, reader::{Context, Reader},
transaction::Transaction, transaction::Transaction,
}; };
use cipher::common::getrandom::SysRng;
use log::{error, info}; use log::{error, info};
use pcsc::Card; use pcsc::Card;
use rand_core::{OsRng, RngCore}; use rand_core::TryRng;
use std::{ use std::{
cmp::{Ord, Ordering},
fmt::{self, Display}, fmt::{self, Display},
str::FromStr, str::FromStr,
}; };
@@ -55,10 +57,10 @@ use {
apdu::StatusWords, apdu::StatusWords,
consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP}, consts::{TAG_ADMIN_FLAGS_1, TAG_ADMIN_TIMESTAMP},
metadata::AdminData, metadata::AdminData,
mgm,
transaction::ChangeRefAction, transaction::ChangeRefAction,
Buffer, ObjectId, Buffer, ObjectId,
}, },
secrecy::ExposeSecret,
std::time::{SystemTime, UNIX_EPOCH}, std::time::{SystemTime, UNIX_EPOCH},
}; };
@@ -66,24 +68,20 @@ use {
pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01; pub(crate) const ADMIN_FLAGS_1_PUK_BLOCKED: u8 = 0x01;
/// 3DES authentication /// 3DES authentication
#[cfg(feature = "untested")]
pub(crate) const ALGO_3DES: u8 = 0x03; pub(crate) const ALGO_3DES: u8 = 0x03;
/// Card management key /// Card management key
pub(crate) const KEY_CARDMGM: u8 = 0x9b; pub(crate) const KEY_CARDMGM: u8 = 0x9b;
/// MGMT Applet ID.
///
/// <https://developers.yubico.com/PIV/Introduction/Admin_access.html>
#[cfg(feature = "untested")]
const MGMT_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x47, 0x11, 0x17];
const TAG_DYN_AUTH: u8 = 0x7c; const TAG_DYN_AUTH: u8 = 0x7c;
/// Cached YubiKey PIN. /// Cached YubiKey PIN.
pub type CachedPin = secrecy::SecretVec<u8>; // TODO(tarcieri): add a newtype for this with a zeroize impl
pub type CachedPin = Vec<u8>;
/// YubiKey serial number. /// YubiKey serial number.
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct Serial(pub u32); pub struct Serial(pub u32);
impl From<u32> for Serial { impl From<u32> for Serial {
@@ -98,6 +96,20 @@ impl From<Serial> for u32 {
} }
} }
impl TryFrom<&[u8]> for Serial {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
if bytes.len() > 4 {
return Err(Error::SizeError);
}
let mut arr = [0u8; 4];
arr[(4 - bytes.len())..].copy_from_slice(bytes);
Ok(Self(u32::from_be_bytes(arr)))
}
}
impl FromStr for Serial { impl FromStr for Serial {
type Err = Error; type Err = Error;
@@ -134,6 +146,22 @@ impl Version {
patch: bytes[2], patch: bytes[2],
} }
} }
#[cfg(feature = "untested")]
pub(crate) fn parse(input: &[u8]) -> Result<Self> {
use nom::{combinator::eof, number::complete::u8};
let (i, major) = u8(input).map_err(|_: nom::Err<()>| Error::ParseError)?;
let (i, minor) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
let (i, patch) = u8(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
let (_i, _) = eof(i).map_err(|_: nom::Err<()>| Error::ParseError)?;
Ok(Self {
major,
minor,
patch,
})
}
} }
impl Display for Version { impl Display for Version {
@@ -142,6 +170,39 @@ impl Display for Version {
} }
} }
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
if self.major > other.major {
return Ordering::Greater;
}
if self.major < other.major {
return Ordering::Less;
}
if self.minor > other.minor {
return Ordering::Greater;
}
if self.minor < other.minor {
return Ordering::Less;
}
if self.patch > other.patch {
return Ordering::Greater;
}
if self.patch < other.patch {
return Ordering::Less;
}
Ordering::Equal
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// YubiKey device: primary API for opening a session and performing various operations. /// YubiKey device: primary API for opening a session and performing various operations.
/// ///
/// Almost all functionality in this library will require an open session /// Almost all functionality in this library will require an open session
@@ -156,64 +217,101 @@ pub struct YubiKey {
pub(crate) serial: Serial, pub(crate) serial: Serial,
} }
impl fmt::Debug for YubiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("YubiKey")
.field("name", &self.name)
.field("version", &self.version)
.field("serial", &self.serial)
.finish_non_exhaustive()
}
}
impl YubiKey { impl YubiKey {
/// Open a connection to a YubiKey. /// Open a connection to a YubiKey.
/// ///
/// Returns an error if there is more than one YubiKey detected. /// Returns an error if more than one YubiKey is detected (or none at all).
///
/// NOTE: If multiple YubiKeys are connected, but we are only able to
/// open one of them (e.g. because the other one is in use, and the
/// connection doesn't allow sharing), the YubiKey that we were able to
/// open is returned.
/// ///
/// If you need to operate in environments with more than one YubiKey /// If you need to operate in environments with more than one YubiKey
/// attached to the same system, use [`YubiKey::open_by_serial`] or /// attached to the same system, use [`YubiKey::open_by_serial`] or
/// [`yubikey::reader::Context`][`Context`] to select from the available /// [`yubikey::reader::Context`][`Context`] to select from the available
/// PC/SC readers. /// PC/SC readers.
pub fn open() -> Result<Self> { pub fn open() -> Result<Self> {
let mut readers = Context::open().map_err(|e| match e { let mut yubikey: Option<Self> = None;
Error::PcscError {
inner: Some(pcsc::Error::NoReadersAvailable),
} => Error::NotFound,
other => other,
})?;
let mut reader_iter = readers.iter()?;
if let Some(reader) = reader_iter.next() { let mut readers = Context::open()?;
if reader_iter.next().is_some() { for reader in readers.iter()? {
error!("multiple YubiKeys detected!"); if let Ok(yk_found) = reader.open() {
return Err(Error::PcscError { inner: None }); 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);
error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None });
} else {
yubikey = Some(yk_found);
}
} }
return reader.open();
} }
error!("no YubiKey detected!"); if let Some(yubikey) = yubikey {
Err(Error::NotFound) // 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. /// Open a YubiKey with a specific serial number.
pub fn open_by_serial(serial: Serial) -> Result<Self> { pub fn open_by_serial(serial: Serial) -> Result<Self> {
let mut readers = Context::open().map_err(|e| match e { let mut readers = Context::open()?;
Error::PcscError {
inner: Some(pcsc::Error::NoReadersAvailable), let mut open_error = None;
} => Error::NotFound,
other => other,
})?;
for reader in readers.iter()? { for reader in readers.iter()? {
let yubikey = match reader.open() { let yubikey = match reader.open() {
Ok(yk) => yk, Ok(yk) => yk,
Err(_) => continue, Err(e) => {
// Save the first error we see that indicates we might have been able
// to find a matching YubiKey.
if open_error.is_none() {
if let Error::PcscError {
inner: Some(pcsc::Error::SharingViolation),
} = e
{
open_error = Some(e);
}
}
continue;
}
}; };
if serial == yubikey.serial() { if serial == yubikey.serial() {
return Ok(yubikey); return Ok(yubikey);
} else {
// We didn't want this YubiKey; don't reset it.
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
} }
} }
error!("no YubiKey detected with serial: {}", serial); Err(if let Some(e) = open_error {
Err(Error::NotFound) e
} else {
error!("no YubiKey detected with serial: {}", serial);
Error::NotFound
})
} }
/// Reconnect to a YubiKey. /// Reconnect to a YubiKey.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reconnect(&mut self) -> Result<()> { pub fn reconnect(&mut self) -> Result<()> {
info!("trying to reconnect to current reader"); info!("trying to reconnect to current reader");
@@ -223,13 +321,10 @@ impl YubiKey {
pcsc::Disposition::ResetCard, pcsc::Disposition::ResetCard,
)?; )?;
let pin = self let pin = self.pin.as_ref().map(|p| Buffer::new(p.clone()));
.pin
.as_ref()
.map(|p| Buffer::new(p.expose_secret().clone()));
let txn = Transaction::new(&mut self.card)?; let txn = Transaction::new(&mut self.card)?;
txn.select_application()?; txn.select_piv_application()?;
if let Some(p) = &pin { if let Some(p) = &pin {
txn.verify_pin(p)?; txn.verify_pin(p)?;
@@ -238,6 +333,41 @@ impl YubiKey {
Ok(()) Ok(())
} }
/// Disconnect from the YubiKey.
///
/// In case of error, ownership of the YubiKey is returned to the caller.
///
/// # Note
///
/// `YubiKey` implements `Drop` which automatically disconnects the card using
/// `Disposition::ResetCard`; you only need to call this function if you want to
/// handle errors or use a different disposition method.
pub fn disconnect(
self,
disposition: pcsc::Disposition,
) -> core::result::Result<(), (Self, Error)> {
let Self {
card,
name,
pin,
version,
serial,
} = self;
card.disconnect(disposition).map_err(|(card, e)| {
(
Self {
card,
name,
pin,
version,
serial,
},
e.into(),
)
})
}
/// Begin a transaction. /// Begin a transaction.
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> { pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> {
// TODO(tarcieri): reconnect support // TODO(tarcieri): reconnect support
@@ -279,38 +409,47 @@ impl YubiKey {
} }
/// Authenticate to the card using the provided management key (MGM). /// Authenticate to the card using the provided management key (MGM).
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<()> { pub fn authenticate(&mut self, mgm_key: &MgmKey) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// get a challenge from the card // get a challenge from the card
let challenge = Apdu::new(Ins::Authenticate) let card_response = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM) .params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data(&[TAG_DYN_AUTH, 0x02, 0x80, 0x00]) .data([TAG_DYN_AUTH, 0x02, 0x80, 0x00])
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
if !challenge.is_success() || challenge.data().len() < 12 { if !card_response.is_success() || card_response.data().len() < 5 {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
// send a response to the cards challenge and a challenge of our own. // send a response to the cards challenge and a challenge of our own.
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().unwrap()); let card_challenge = mgm_key.card_challenge(&card_response.data()[4..])?;
let challenge_len = card_challenge.len();
let mut data = [0u8; 22]; // If this exceeds a `u8` then the card is giving us unexpected data.
data[0] = TAG_DYN_AUTH; let auth_len = (2 + challenge_len + 2 + challenge_len)
data[1] = 20; // 2 + 8 + 2 +8 .try_into()
data[2] = 0x80; .map_err(|_| Error::AuthenticationError)?;
data[3] = 8;
data[4..12].copy_from_slice(&response);
data[12] = 0x81;
data[13] = 8;
OsRng.fill_bytes(&mut data[14..22]);
let mut challenge = [0u8; 8]; let mut data = Vec::with_capacity(4 + challenge_len + 2 + challenge_len);
challenge.copy_from_slice(&data[14..22]); data.push(TAG_DYN_AUTH);
data.push(auth_len);
data.push(0x80);
data.push(challenge_len as u8);
data.extend_from_slice(&card_challenge);
data.push(0x81);
data.push(challenge_len as u8);
let mut host_challenge = vec![0u8; challenge_len];
SysRng
.try_fill_bytes(&mut host_challenge)
.map_err(|_| Error::GenericError)?;
data.extend_from_slice(&host_challenge);
let authentication = Apdu::new(Ins::Authenticate) let authentication = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM) .params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data(&data) .data(data)
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
if !authentication.is_success() { if !authentication.is_success() {
@@ -318,14 +457,7 @@ impl YubiKey {
} }
// compare the response from the card with our challenge // compare the response from the card with our challenge
let response = mgm_key.encrypt(&challenge); mgm_key.check_challenge(&host_challenge, &authentication.data()[4..])
use subtle::ConstantTimeEq;
if response.ct_eq(&authentication.data()[4..12]).unwrap_u8() != 1 {
return Err(Error::AuthenticationError);
}
Ok(())
} }
/// Get the PIV keys contained in this YubiKey. /// Get the PIV keys contained in this YubiKey.
@@ -335,13 +467,12 @@ impl YubiKey {
/// Deauthenticate. /// Deauthenticate.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn deauthenticate(&mut self) -> Result<()> { pub fn deauthenticate(&mut self) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let status_words = Apdu::new(Ins::SelectApplication) let status_words = Apdu::new(Ins::SelectApplication)
.p1(0x04) .p1(0x04)
.data(MGMT_AID) .data(mgm::APPLET_ID)
.transmit(&txn, 255)? .transmit(&txn, 255)?
.status_words(); .status_words();
@@ -350,7 +481,12 @@ impl YubiKey {
"Failed selecting mgmt application: {:04x}", "Failed selecting mgmt application: {:04x}",
status_words.code() status_words.code()
); );
return Err(Error::GenericError); return Err(match status_words {
StatusWords::NotFoundError => Error::AppletNotFound {
applet_name: mgm::APPLET_NAME,
},
_ => Error::GenericError,
});
} }
Ok(()) Ok(())
@@ -364,7 +500,7 @@ impl YubiKey {
} }
if !pin.is_empty() { if !pin.is_empty() {
self.pin = Some(CachedPin::new(pin.into())) self.pin = Some(pin.into())
} }
Ok(()) Ok(())
@@ -377,7 +513,7 @@ impl YubiKey {
// Force a re-select to unverify, because once verified the spec dictates that // Force a re-select to unverify, because once verified the spec dictates that
// subsequent verify calls will return a "verification not needed" instead of // subsequent verify calls will return a "verification not needed" instead of
// the number of tries left... // the number of tries left...
txn.select_application()?; txn.select_piv_application()?;
// WRONG_PIN is expected on successful query. // WRONG_PIN is expected on successful query.
match txn.verify_pin(&[]) { match txn.verify_pin(&[]) {
@@ -389,7 +525,6 @@ impl YubiKey {
/// Set the number of PIN retries. /// Set the number of PIN retries.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> { pub fn set_pin_retries(&mut self, pin_tries: u8, puk_tries: u8) -> Result<()> {
// Special case: if either retry count is 0, it's a successful no-op // Special case: if either retry count is 0, it's a successful no-op
if pin_tries == 0 || puk_tries == 0 { if pin_tries == 0 || puk_tries == 0 {
@@ -414,7 +549,6 @@ impl YubiKey {
/// ///
/// The default PIN code is `123456`. /// The default PIN code is `123456`.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> { pub fn change_pin(&mut self, current_pin: &[u8], new_pin: &[u8]) -> Result<()> {
{ {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
@@ -422,7 +556,7 @@ impl YubiKey {
} }
if !new_pin.is_empty() { if !new_pin.is_empty() {
self.pin = Some(CachedPin::new(new_pin.into())); self.pin = Some(new_pin.into());
} }
Ok(()) Ok(())
@@ -430,7 +564,6 @@ impl YubiKey {
/// Set PIN last changed. /// Set PIN last changed.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> { pub fn set_pin_last_changed(yubikey: &mut YubiKey) -> Result<()> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
@@ -438,22 +571,17 @@ impl YubiKey {
// TODO(tarcieri): double check this is little endian // TODO(tarcieri): double check this is little endian
let tnow = SystemTime::now() let tnow = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)?
.unwrap()
.as_secs() .as_secs()
.to_le_bytes(); .to_le_bytes();
admin_data admin_data
.set_item(TAG_ADMIN_TIMESTAMP, &tnow) .set_item(TAG_ADMIN_TIMESTAMP, &tnow)
.map_err(|e| { .inspect_err(|e| error!("could not set pin timestamp, err = {}", e))?;
error!("could not set pin timestamp, err = {}", e);
e
})?;
admin_data.write(&txn).map_err(|e| { admin_data
error!("could not write admin data, err = {}", e); .write(&txn)
e .inspect_err(|e| error!("could not write admin data, err = {}", e))?;
})?;
Ok(()) Ok(())
} }
@@ -466,7 +594,6 @@ impl YubiKey {
/// ///
/// The default PUK code is `12345678`. /// The default PUK code is `12345678`.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> { pub fn change_puk(&mut self, current_puk: &[u8], new_puk: &[u8]) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk) txn.change_ref(ChangeRefAction::ChangePuk, current_puk, new_puk)
@@ -474,7 +601,6 @@ impl YubiKey {
/// Block PUK: permanently prevent the PIN from becoming unblocked. /// Block PUK: permanently prevent the PIN from becoming unblocked.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn block_puk(&mut self) -> Result<()> { pub fn block_puk(&mut self) -> Result<()> {
let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44]; let mut puk = [0x30, 0x42, 0x41, 0x44, 0x46, 0x30, 0x30, 0x44];
let mut tries_remaining: i32 = -1; let mut tries_remaining: i32 = -1;
@@ -505,7 +631,7 @@ impl YubiKey {
// Attempt to set the "PUK blocked" flag in admin data. // Attempt to set the "PUK blocked" flag in admin data.
let mut admin_data = AdminData::read(&txn) let mut admin_data = AdminData::read(&txn)
.map(|data| { .inspect(|data| {
if let Ok(item) = data.get_item(TAG_ADMIN_FLAGS_1) { if let Ok(item) = data.get_item(TAG_ADMIN_FLAGS_1) {
if item.len() == flags.len() { if item.len() == flags.len() {
flags.copy_from_slice(item) flags.copy_from_slice(item)
@@ -517,8 +643,6 @@ impl YubiKey {
); );
} }
} }
data
}) })
.unwrap_or_default(); .unwrap_or_default();
@@ -538,7 +662,6 @@ impl YubiKey {
/// Unblock a Personal Identification Number (PIN) using a previously /// Unblock a Personal Identification Number (PIN) using a previously
/// configured PIN Unblocking Key (PUK). /// configured PIN Unblocking Key (PUK).
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> { pub fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin) txn.change_ref(ChangeRefAction::UnblockPin, puk, new_pin)
@@ -546,7 +669,6 @@ impl YubiKey {
/// Fetch an object from the YubiKey. /// Fetch an object from the YubiKey.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer> { pub fn fetch_object(&mut self, object_id: ObjectId) -> Result<Buffer> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
txn.fetch_object(object_id) txn.fetch_object(object_id)
@@ -554,7 +676,6 @@ impl YubiKey {
/// Save an object. /// Save an object.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> { pub fn save_object(&mut self, object_id: ObjectId, indata: &mut [u8]) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
txn.save_object(object_id, indata) txn.save_object(object_id, indata)
@@ -562,25 +683,27 @@ impl YubiKey {
/// Get an auth challenge. /// Get an auth challenge.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> { pub fn get_auth_challenge(&mut self) -> Result<[u8; 8]> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
let response = Apdu::new(Ins::Authenticate) let response = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM) .params(ALGO_3DES, KEY_CARDMGM)
.data(&[0x7c, 0x02, 0x81, 0x00]) .data([0x7c, 0x02, 0x81, 0x00])
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
if !response.is_success() { if !response.is_success() {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
Ok(response.data()[4..12].try_into().unwrap()) Ok(response
.data()
.get(4..12)
.ok_or(Error::SizeError)?
.try_into()?)
} }
/// Verify an auth response. /// Verify an auth response.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> { pub fn verify_auth_response(&mut self, response: [u8; 8]) -> Result<()> {
let mut data = [0u8; 12]; let mut data = [0u8; 12];
data[0] = 0x7c; data[0] = 0x7c;
@@ -594,7 +717,7 @@ impl YubiKey {
// send the response to the card and a challenge of our own. // send the response to the card and a challenge of our own.
let status_words = Apdu::new(Ins::Authenticate) let status_words = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM) .params(ALGO_3DES, KEY_CARDMGM)
.data(&data) .data(data)
.transmit(&txn, 261)? .transmit(&txn, 261)?
.status_words(); .status_words();
@@ -611,7 +734,6 @@ impl YubiKey {
/// ///
/// The reset function is only available when both pins are blocked. /// The reset function is only available when both pins are blocked.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reset_device(&mut self) -> Result<()> { pub fn reset_device(&mut self) -> Result<()> {
let templ = [0, Ins::Reset.code(), 0, 0]; let templ = [0, Ins::Reset.code(), 0, 0];
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
@@ -629,30 +751,46 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error; type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self> { fn try_from(reader: &'a Reader<'_>) -> Result<Self> {
let mut card = reader.connect().map_err(|e| { let mut card = reader
error!("error connecting to reader '{}': {}", reader.name(), e); .connect()
e .inspect_err(|e| error!("error connecting to reader '{}': {}", reader.name(), e))?;
})?;
info!("connected to reader: {}", reader.name()); info!("connected to reader: {}", reader.name());
let (version, serial) = { let mut app_version_serial = || -> Result<(Version, Serial)> {
let txn = Transaction::new(&mut card)?; let txn = Transaction::new(&mut card)?;
txn.select_application()?; txn.select_piv_application()?;
let v = txn.get_version()?; let v = txn.get_version()?;
let s = txn.get_serial(v)?; let s = txn.get_serial(v)?;
(v, s) Ok((v, s))
}; };
let yubikey = YubiKey { match app_version_serial() {
card, Err(e) => {
name: String::from(reader.name()), error!("Could not use reader: {}", e);
pin: None,
version,
serial,
};
Ok(yubikey) // We were unable to use the card, so we've effectively only connected as
// a side-effect of determining this. Avoid disrupting its internal state
// any further (e.g. preserve the PIN cache of whatever applet is selected
// currently).
if let Err((_, e)) = card.disconnect(pcsc::Disposition::LeaveCard) {
error!("Failed to disconnect gracefully from card: {}", e);
}
Err(e)
}
Ok((version, serial)) => {
let yubikey = YubiKey {
card,
name: String::from(reader.name()),
pin: None,
version,
serial,
};
Ok(yubikey)
}
}
} }
} }
+85
View File
@@ -0,0 +1,85 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
d4:29:8f:df:8a:af:7b:c0:d7:bf:19:9d:90:d5:ef:ca
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Ferdinand Linnenberg CA
Validity
Not Before: Feb 10 12:25:37 2022 GMT
Not After : May 15 12:25:37 2024 GMT
Subject: CN=Bob
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:d5:27:9b:99:1b:3a:36:64:36:c8:e5:78:64:b6:
9d:70:9d:29:6c:0e:85:91:4b:78:3b:dc:16:c3:09:
8c:d3:74:20:8c:6f:ed:c3:90:c9:1b:4d:80:d5:46:
da:52:7f:d2:2f:bc:b2:f7:40:8d:ad:dd:24:b9:5c:
dc:a2:21:2f:48:ec:06:93:8b:89:f0:cd:63:ff:a1:
fd:ce:36:d5:07:7a:1e:0e:cf:68:a8:c1:b3:7f:62:
84:b7:e1:cf:25:7b:3f:a8:3c:ac:07:1a:fd:c2:e1:
e0:9e:26:24:c1:0d:6d:9d:c6:57:6a:b4:39:28:3d:
88:3e:c9:6a:89:90:72:4a:7b:75:c5:5e:1b:5e:5c:
32:54:a3:ff:eb:01:68:7f:89:b4:4c:01:3f:08:8e:
6c:61:49:60:26:0b:26:58:81:d7:1a:57:ee:52:5c:
05:47:de:da:eb:b5:92:9d:5b:ce:26:18:44:59:3e:
27:d0:61:86:e2:f4:c6:d9:c7:2b:1f:cb:ea:78:f0:
a1:a9:57:d7:98:4c:c1:2f:ae:6a:38:b4:34:53:2e:
5a:9e:f8:58:c7:51:e7:fd:b8:27:cd:87:72:26:c1:
7d:14:c7:cd:fb:f2:04:8a:c4:8f:61:cf:a8:78:bd:
21:be:28:cb:e8:a8:65:29:28:82:46:2f:18:e6:ff:
6f:53
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Subject Key Identifier:
B5:A5:F0:37:25:97:AD:BE:F1:43:52:45:4D:8B:A0:5E:E9:78:21:B8
X509v3 Authority Key Identifier:
keyid:26:4E:EB:B0:A5:1B:08:A8:90:2A:85:04:73:84:B5:A5:2C:61:D6:91
DirName:/CN=Ferdinand Linnenberg CA
serial:8C:E0:40:D9:D8:60:E5:77
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Key Usage:
Digital Signature
Signature Algorithm: sha256WithRSAEncryption
19:f3:eb:c1:95:e6:d5:a9:33:d7:2e:02:d8:3a:91:84:81:14:
93:fc:03:4d:b1:4b:9d:0b:9b:94:93:9f:1a:0d:87:31:a1:fa:
a6:c7:3a:6b:18:24:12:ab:28:fb:c8:e3:09:a2:5d:50:49:00:
d9:18:e6:4a:09:18:e0:1c:da:d3:19:96:3d:74:72:fe:e0:8f:
ee:59:54:66:2e:57:72:b8:91:55:06:13:e5:9e:89:a2:3a:13:
3b:45:30:d3:cd:15:0e:81:eb:4f:b0:6a:a4:6d:00:7d:5b:c0:
4a:7f:97:d0:27:27:31:ae:3e:72:f1:74:fe:86:8e:29:a9:42:
23:26:22:db:08:8b:df:e9:d3:83:8d:81:10:36:d7:33:68:5e:
cb:93:cb:1e:12:c8:cb:be:5e:5c:8e:58:b0:1d:06:5e:c9:98:
b7:f1:49:fe:c4:03:de:b4:2b:da:9d:2c:7d:98:37:1c:6c:a8:
95:21:6f:23:e3:2e:09:bc:6c:e5:ed:e2:50:d8:f7:da:45:39:
d8:34:8a:57:0c:4f:d0:0d:80:06:d6:34:63:72:27:d1:50:d1:
d2:21:2c:97:57:17:98:02:95:3a:96:ed:75:9f:cc:f3:b8:f1:
3a:85:f9:58:08:9b:a0:75:fd:9b:fd:31:dd:08:dc:14:3d:f4:
68:aa:d4:30
-----BEGIN CERTIFICATE-----
MIIDXzCCAkegAwIBAgIRANQpj9+Kr3vA178ZnZDV78owDQYJKoZIhvcNAQELBQAw
IjEgMB4GA1UEAwwXRmVyZGluYW5kIExpbm5lbmJlcmcgQ0EwHhcNMjIwMjEwMTIy
NTM3WhcNMjQwNTE1MTIyNTM3WjAOMQwwCgYDVQQDDANCb2IwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDVJ5uZGzo2ZDbI5Xhktp1wnSlsDoWRS3g73BbD
CYzTdCCMb+3DkMkbTYDVRtpSf9IvvLL3QI2t3SS5XNyiIS9I7AaTi4nwzWP/of3O
NtUHeh4Oz2iowbN/YoS34c8lez+oPKwHGv3C4eCeJiTBDW2dxldqtDkoPYg+yWqJ
kHJKe3XFXhteXDJUo//rAWh/ibRMAT8IjmxhSWAmCyZYgdcaV+5SXAVH3trrtZKd
W84mGERZPifQYYbi9MbZxysfy+p48KGpV9eYTMEvrmo4tDRTLlqe+FjHUef9uCfN
h3ImwX0Ux8378gSKxI9hz6h4vSG+KMvoqGUpKIJGLxjm/29TAgMBAAGjgaMwgaAw
CQYDVR0TBAIwADAdBgNVHQ4EFgQUtaXwNyWXrb7xQ1JFTYugXul4IbgwUgYDVR0j
BEswSYAUJk7rsKUbCKiQKoUEc4S1pSxh1pGhJqQkMCIxIDAeBgNVBAMMF0ZlcmRp
bmFuZCBMaW5uZW5iZXJnIENBggkAjOBA2dhg5XcwEwYDVR0lBAwwCgYIKwYBBQUH
AwIwCwYDVR0PBAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQAZ8+vBlebVqTPXLgLY
OpGEgRST/ANNsUudC5uUk58aDYcxofqmxzprGCQSqyj7yOMJol1QSQDZGOZKCRjg
HNrTGZY9dHL+4I/uWVRmLldyuJFVBhPlnomiOhM7RTDTzRUOgetPsGqkbQB9W8BK
f5fQJycxrj5y8XT+ho4pqUIjJiLbCIvf6dODjYEQNtczaF7Lk8seEsjLvl5cjliw
HQZeyZi38Un+xAPetCvanSx9mDccbKiVIW8j4y4JvGzl7eJQ2PfaRTnYNIpXDE/Q
DYAG1jRjcifRUNHSISyXVxeYApU6lu11n8zzuPE6hflYCJugdf2b/THdCNwUPfRo
qtQw
-----END CERTIFICATE-----
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIi6DixMpf5PQCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECLgvCsIAjjXRBIIEyDAss0V4NrI5
W7KXPRgRJ1tqvQrWZQTIFu4Crzs9Inb4TtSv5mATI9ZU2RMFF6MYXlhIxJng861P
5IWcU6VeOjRFej8wcB3uTvD2z7NB2cyA5BZSojrZfX5OIEKL9sBzn0vinqmm5N1z
oXhLMgf0FZssA3+zjIf04vtvmk5pxTCE6dq6vlsEIJyQ0xGc39bStIwk2a4E9wvi
XayKNJnRFSrTahuI3DvQJPd9TdmM6sBKnrcJrDa6LvH51SrGzW8bBEjDAmC7yJdi
mBckFTjZ6rGrSxOI6HdnF9RP+y9OiLE4ou5OJ9MbBtngq9OrTImAMZ5ftWowaqX/
Do1UTqixi4ecWrr5fr1+A2Vch0I1drds2e/mmLR+5GEQXQXZPZKjtPMwxM/AYnTZ
w/M3T9KtwSj0s5G6Saz4WpzaUL7wATb/UNqMr8Ifl8mHEVoFZhvoRpMWA7Yj1oPb
cHz8lsfoSrTnK+zLR8ZK3HRu4MtpdCNVwQIJ67T6Feb8YLwYSccTNHBSqUmWRD95
wOOqY33xcfplaQ2Y+/8+mHScGSEPNmC4f7EggDeUnG3ow0f4n95DENO+aYqGLjSF
+XdCjhD+NTNdlV0z50B6P2XWUoBZOOnPfgFf4nAgn7xQbkZZOk5bQDKo2Zu9jW9/
uJyTHqI58tcopI3cjd5iQXOJUrM4OWpPIu3p+VWaIAA8JzJIfDN6fyQ3qsMr305L
30JcjrH6if/6J+2g+DpMAK928JY2hfE8VNWH7096ZnArp42/hLYsNuYnSrL1eS+g
F/4mvyZyLLTFyB0Frnic4I1QTuNkNmwSrm/B5wIWLqkS7XAyyDDXAcaTHdZCN6nM
O2OuF7DfBsFcNMM4VagG5adPS9CYkvz0EEh6ho5XiP2yL3tZfsHyuB4njAsV3aFi
D0Yq7QiCf5iA2d4KsYO6yr1wPfVhlsmPi3++mrHulBwwCQWHTlPgRZnjj1xgmPcQ
00KsUVh+CMWlf20O5sKhzjvkzbwUj1K+ZfMDuuq1RbzFRSx+Gx8vIaThFg9kVyoP
zuvzsT6qc3BGNHmaGZ3d5Re25AuGRTF4cTpDfjW0UL7Wnvnis7iMrUasDhyF7CFn
/KG7eKzxqS08o6D4AM5S/fzZEtszoEgAga6DS2R75FVskDweWuEIsar9UGg3UlmW
q3+rRPRf1CzrLtyYenkkLg/ajr8JOnFGqZVaLmMnegZQH6rF3aEzlQLNgbNepcuA
ObSmAO6MR3MlQgdsH/lNzOPdj1gKcE25hOjGfmwgbOXSJv9Cz0bcBLFEyLZSNpRk
HhNejj6BEz/Cmqg1wm7SOBHsXJGcOTnLO1Y3FBt0I7heWvWmj9rOLG9tvvx9dtrP
pQyDbIcWonuXLrXYSPyOjmeWoQSzdH3NsCswBV4G+iOiLCJDkElR+mrwKbhtS88T
3YeZnsCTsmH+jZpxGgPTObjIG91U4UE4Pnkwc2df355VuOKrf0rB/NK0A9hZGqsV
nxtodjn92P6UFzmfEdt95pMcmurK9wkm1kRkP7cIyAs2lCOIdbgGsszz6Mk16Xqy
49RdhLxJrJ4gkYZIAbY+KNGVc7uPhm/T9xGrIstbEsoUM7jy7nHMOCCDgdbwX/4X
caMeSMZcZ+RvraDDBEbSbg==
-----END ENCRYPTED PRIVATE KEY-----
+254 -70
View File
@@ -3,37 +3,38 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)] #![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
use lazy_static::lazy_static; use cipher::common::{getrandom::SysRng, Generate};
use log::trace; use log::trace;
use rand_core::{OsRng, RngCore}; use once_cell::sync::Lazy;
use rsa::{hash::Hash::SHA2_256, PaddingScheme, PublicKey}; use rsa::{pkcs1v15, RsaPublicKey};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{env, sync::Mutex}; use signature::hazmat::PrehashVerifier;
use x509::RelativeDistinguishedName; use std::{env, str::FromStr, sync::Mutex, time::Duration};
use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
use yubikey::{ use yubikey::{
certificate::{Certificate, PublicKeyInfo}, certificate::{yubikey_signer, Certificate},
piv::{self, AlgorithmId, Key, RetiredSlotId, SlotId}, piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
Error, MgmKey, PinPolicy, TouchPolicy, YubiKey, Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
}; };
lazy_static! { static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
/// Provide thread-safe access to a YubiKey
static ref YUBIKEY: Mutex<YubiKey> = init_yubikey();
}
/// One-time test initialization and setup
fn init_yubikey() -> Mutex<YubiKey> {
// Only show logs if `RUST_LOG` is set // Only show logs if `RUST_LOG` is set
if env::var("RUST_LOG").is_ok() { if env::var("RUST_LOG").is_ok() {
env_logger::builder().format_timestamp(None).init(); env_logger::builder().format_timestamp(None).init();
} }
let yubikey = YubiKey::open().unwrap(); let yubikey = if let Ok(serial) = env::var("YUBIKEY_SERIAL") {
let serial = Serial::from_str(&serial).unwrap();
YubiKey::open_by_serial(serial).unwrap()
} else {
YubiKey::open().unwrap()
};
trace!("serial: {}", yubikey.serial()); trace!("serial: {}", yubikey.serial());
trace!("version: {}", yubikey.version()); trace!("version: {}", yubikey.version());
Mutex::new(yubikey) Mutex::new(yubikey)
} });
// //
// CCCID support // CCCID support
@@ -42,12 +43,15 @@ fn init_yubikey() -> Mutex<YubiKey> {
#[test] #[test]
#[ignore] #[ignore]
fn test_get_cccid() { fn test_get_cccid() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
match yubikey.cccid() { match yubikey.cccid() {
Ok(cccid) => trace!("CCCID: {:?}", cccid), Ok(cccid) => trace!("CCCID: {:?}", cccid),
Err(Error::NotFound) => trace!("CCCID not found"), Err(Error::NotFound) => trace!("CCCID not found"),
Err(err) => panic!("error getting CCCID: {:?}", err), Err(err) => panic!("error getting CCCID: {err:?}"),
} }
} }
@@ -58,12 +62,15 @@ fn test_get_cccid() {
#[test] #[test]
#[ignore] #[ignore]
fn test_get_chuid() { fn test_get_chuid() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
match yubikey.chuid() { match yubikey.chuid() {
Ok(chuid) => trace!("CHUID: {:?}", chuid), Ok(chuid) => trace!("CHUID: {:?}", chuid),
Err(Error::NotFound) => trace!("CHUID not found"), Err(Error::NotFound) => trace!("CHUID not found"),
Err(err) => panic!("error getting CHUID: {:?}", err), Err(err) => panic!("error getting CHUID: {err:?}"),
} }
} }
@@ -74,7 +81,10 @@ fn test_get_chuid() {
#[test] #[test]
#[ignore] #[ignore]
fn test_get_config() { fn test_get_config() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
let config_result = yubikey.config(); let config_result = yubikey.config();
assert!(config_result.is_ok()); assert!(config_result.is_ok());
trace!("config: {:?}", config_result.unwrap()); trace!("config: {:?}", config_result.unwrap());
@@ -87,7 +97,10 @@ fn test_get_config() {
#[test] #[test]
#[ignore] #[ignore]
fn test_list_keys() { fn test_list_keys() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
let keys_result = Key::list(&mut yubikey); let keys_result = Key::list(&mut yubikey);
assert!(keys_result.is_ok()); assert!(keys_result.is_ok());
trace!("keys: {:?}", keys_result.unwrap()); trace!("keys: {:?}", keys_result.unwrap());
@@ -100,7 +113,10 @@ fn test_list_keys() {
#[test] #[test]
#[ignore] #[ignore]
fn test_verify_pin() { fn test_verify_pin() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
assert!(yubikey.verify_pin(b"000000").is_err()); assert!(yubikey.verify_pin(b"000000").is_err());
assert!(yubikey.verify_pin(b"123456").is_ok()); assert!(yubikey.verify_pin(b"123456").is_ok());
} }
@@ -113,43 +129,52 @@ fn test_verify_pin() {
#[test] #[test]
#[ignore] #[ignore]
fn test_set_mgmkey() { fn test_set_mgmkey() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut rng = SysRng;
let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok()); assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err()); assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_ok()); assert!(yubikey.authenticate(&default_key).is_ok());
// Set a protected management key. // Set a protected management key.
assert!(MgmKey::generate().set_protected(&mut yubikey).is_ok()); assert!(MgmKey::generate_for(&yubikey, &mut rng)
.unwrap()
.set_protected(&mut yubikey)
.is_ok());
let protected = MgmKey::get_protected(&mut yubikey).unwrap(); let protected = MgmKey::get_protected(&mut yubikey).unwrap();
assert!(yubikey.authenticate(MgmKey::default()).is_err()); assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(protected.clone()).is_ok()); assert!(yubikey.authenticate(&protected).is_ok());
// Set a manual management key. // Set a manual management key.
let manual = MgmKey::generate(); let manual = MgmKey::generate_for(&yubikey, &mut rng).unwrap();
assert!(manual.set_manual(&mut yubikey, false).is_ok()); assert!(manual.set_manual(&mut yubikey, false).is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err()); assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_err()); assert!(yubikey.authenticate(&default_key).is_err());
assert!(yubikey.authenticate(protected.clone()).is_err()); assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(manual.clone()).is_ok()); assert!(yubikey.authenticate(&manual).is_ok());
// Set back to the default management key. // Set back to the default management key.
assert!(MgmKey::set_default(&mut yubikey).is_ok()); assert!(MgmKey::set_default(&mut yubikey).is_ok());
assert!(MgmKey::get_protected(&mut yubikey).is_err()); assert!(MgmKey::get_protected(&mut yubikey).is_err());
assert!(yubikey.authenticate(protected).is_err()); assert!(yubikey.authenticate(&protected).is_err());
assert!(yubikey.authenticate(manual).is_err()); assert!(yubikey.authenticate(&manual).is_err());
assert!(yubikey.authenticate(MgmKey::default()).is_ok()); assert!(yubikey.authenticate(&default_key).is_ok());
} }
// //
// Certificate support // Certificate support
// //
fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate { fn generate_self_signed_cert<KT: yubikey_signer::KeyType>() -> Certificate {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok()); assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok()); assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1); let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -157,25 +182,27 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
let generated = piv::generate( let generated = piv::generate(
&mut yubikey, &mut yubikey,
slot, slot,
algorithm, KT::ALGORITHM,
PinPolicy::Default, PinPolicy::Default,
TouchPolicy::Default, TouchPolicy::Default,
) )
.unwrap(); .unwrap();
let mut serial = [0u8; 20]; // 0x80 0x00 ... (20bytes) is invalid because of high MSB (serial will keep the sign)
OsRng.fill_bytes(&mut serial); // we'll limit ourselves to 19 bytes serial.
let serial = <[u8; 19]>::generate();
let serial = SerialNumber::new(&serial[..]).expect("serial can't be more than 20 bytes long");
let validity = Validity::from_now(Duration::new(500000, 0)).unwrap();
// Generate a self-signed certificate for the new key. // Generate a self-signed certificate for the new key.
let extensions: &[x509::Extension<'_, &[u64]>] = &[]; let cert_result = Certificate::generate_self_signed::<_, KT>(
let cert_result = Certificate::generate_self_signed(
&mut yubikey, &mut yubikey,
slot, slot,
serial, serial,
None, validity,
&[RelativeDistinguishedName::common_name("testSubject")], Name::from_str("CN=testSubject").expect("parse name"),
generated, generated,
extensions, |_builder| Ok(()),
); );
assert!(cert_result.is_ok()); assert!(cert_result.is_ok());
@@ -187,61 +214,100 @@ fn generate_self_signed_cert(algorithm: AlgorithmId) -> Certificate {
#[test] #[test]
#[ignore] #[ignore]
fn generate_self_signed_rsa_cert() { fn generate_self_signed_rsa_cert() {
let cert = generate_self_signed_cert(AlgorithmId::Rsa1024); let cert = generate_self_signed_cert::<yubikey_signer::YubiRsa<yubikey_signer::Rsa1024>>();
// //
// Verify that the certificate is signed correctly // Verify that the certificate is signed correctly
// //
let pubkey = match cert.subject_pki() { let pubkey = RsaPublicKey::try_from(cert.subject_pki()).expect("valid rsa key");
PublicKeyInfo::Rsa { pubkey, .. } => pubkey, let pubkey = pkcs1v15::VerifyingKey::<Sha256>::new(pubkey);
_ => unreachable!(),
};
let data = cert.as_ref(); let data = cert.cert.to_der().expect("serialize certificate");
let tbs_cert_len = u16::from_be_bytes(data[6..8].try_into().unwrap()) as usize; let tbs_cert_len = u16::from_be_bytes(data[6..8].try_into().unwrap()) as usize;
let msg = &data[4..8 + tbs_cert_len]; let msg = &data[4..8 + tbs_cert_len];
let sig = &data[data.len() - 128..]; let sig = pkcs1v15::Signature::try_from(&data[data.len() - 128..]).unwrap();
let hash = Sha256::digest(msg); let hash = Sha256::digest(msg);
assert!(pubkey assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
.verify( }
PaddingScheme::PKCS1v15Sign {
hash: Some(SHA2_256) #[test]
}, #[ignore]
&hash, fn generate_rsa3072() {
sig let mut yubikey = YUBIKEY.lock().unwrap();
) let version = yubikey.version();
.is_ok()); let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
// Generate a new key in the selected slot.
let generated = piv::generate(
&mut yubikey,
slot,
AlgorithmId::Rsa3072,
PinPolicy::Default,
TouchPolicy::Default,
);
match generated {
Ok(key) => {
let pubkey = key.subject_public_key;
assert!(pubkey.bit_len() > 3072)
}
Err(e) => assert!((version.major, version.minor) < (5, 7) && e == Error::AlgorithmError),
}
} }
#[test] #[test]
#[ignore] #[ignore]
fn generate_self_signed_ec_cert() { fn generate_self_signed_ec_cert() {
let cert = generate_self_signed_cert(AlgorithmId::EccP256); let cert = generate_self_signed_cert::<p256::NistP256>();
// //
// Verify that the certificate is signed correctly // Verify that the certificate is signed correctly
// //
let pubkey = match cert.subject_pki() { let vk = p256::ecdsa::VerifyingKey::try_from(cert.subject_pki()).expect("ecdsa key expected");
PublicKeyInfo::EcP256(pubkey) => pubkey,
_ => unreachable!(),
};
let data = cert.as_ref(); let data = cert.cert.to_der().expect("serialize certificate");
let tbs_cert_len = data[6] as usize; let tbs_cert_len = data[6] as usize;
let sig_algo_len = data[7 + tbs_cert_len + 1] as usize; let sig_algo_len = data[7 + tbs_cert_len + 1] as usize;
let sig_start = 7 + tbs_cert_len + 2 + sig_algo_len + 3; let sig_start = 7 + tbs_cert_len + 2 + sig_algo_len + 3;
let msg = &data[4..7 + tbs_cert_len]; let msg = &data[4..7 + tbs_cert_len];
let sig = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap(); let sig = p256::ecdsa::Signature::from_der(&data[sig_start..]).unwrap();
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(pubkey.as_bytes()).unwrap();
use p256::ecdsa::signature::Verifier; use p256::ecdsa::signature::Verifier;
assert!(vk.verify(msg, &sig).is_ok()); assert!(vk.verify(msg, &sig).is_ok());
} }
#[test]
#[ignore]
fn generate_self_signed_cv_cert() {
let cert = generate_self_signed_cert::<ed25519_dalek::SigningKey>();
//
// Verify that the certificate is signed correctly
//
let pubkey =
ed25519_dalek::VerifyingKey::try_from(cert.subject_pki()).expect("ed25519 key expected");
let data = cert.cert.to_der().expect("serialize certificate");
let cert_len = data[2] as usize;
let tbs_cert_len = data[5] as usize;
let sig_algo_len: usize = 64;
let sig_start = cert_len - sig_algo_len + 3;
let msg = &data[3..6 + tbs_cert_len];
let sig =
ed25519_dalek::Signature::from_slice(&data[sig_start..sig_start + sig_algo_len]).unwrap();
use ed25519_dalek::Verifier;
assert!(pubkey.verify(msg, &sig).is_ok());
}
#[test] #[test]
#[ignore] #[ignore]
fn test_slot_id_display() { fn test_slot_id_display() {
@@ -273,4 +339,122 @@ fn test_slot_id_display() {
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R18)), "R18"); assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R18)), "R18");
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R19)), "R19"); assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R19)), "R19");
assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R20)), "R20"); assert_eq!(format!("{}", SlotId::Retired(RetiredSlotId::R20)), "R20");
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Pin)),
"Pin"
);
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Puk)),
"Puk"
);
assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Management)),
"Management"
);
}
//
// Metadata
//
#[test]
#[ignore]
fn test_read_metadata() {
let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
// Generate a new key in the selected slot.
let generated = piv::generate(
&mut yubikey,
slot,
AlgorithmId::EccP256,
PinPolicy::Default,
TouchPolicy::Default,
)
.unwrap();
match piv::metadata(&mut yubikey, slot) {
Ok(metadata) => assert_eq!(metadata.public, Some(generated)),
Err(Error::NotSupported) => {
// Some YubiKeys don't support metadata
eprintln!("metadata not supported by this YubiKey");
}
Err(err) => panic!("{}", err),
}
}
#[test]
#[ignore]
fn test_read_metadata_missing_key() {
let mut yubikey = YUBIKEY.lock().unwrap();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
// we assume that at least one of these slots is empty
let slots_to_check = [
RetiredSlotId::R10,
RetiredSlotId::R11,
RetiredSlotId::R12,
RetiredSlotId::R13,
RetiredSlotId::R14,
RetiredSlotId::R15,
RetiredSlotId::R16,
RetiredSlotId::R17,
RetiredSlotId::R18,
RetiredSlotId::R19,
RetiredSlotId::R20,
];
for slot in slots_to_check {
let slot = SlotId::Retired(slot);
match piv::metadata(&mut yubikey, slot) {
Ok(_) => {
eprintln!("Key {} exists", slot);
}
Err(Error::NotSupported) => {
// Some YubiKeys don't support metadata
eprintln!("metadata not supported by this YubiKey");
return;
}
Err(Error::NotFound) => {
eprintln!("Key {} doesn't exist, ok.", slot);
return;
}
Err(err) => panic!("{}", err),
}
}
panic!("No empty slots to check");
}
#[test]
#[ignore]
fn test_parse_cert_from_der() {
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
let cert = Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
assert_eq!(
cert.subject(),
"CN=Bob",
"Subject is {} should be CN=Bob",
cert.subject()
);
assert_eq!(
cert.issuer(),
"CN=Ferdinand Linnenberg CA",
"Issuer is {} should be {}",
cert.issuer(),
"CN=Ferdinand Linnenberg CA"
);
} }