Compare commits

...

76 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
32 changed files with 3292 additions and 2007 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", ...]
+12 -1
View File
@@ -1,8 +1,19 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: cargo - package-ecosystem: cargo
versioning-strategy: lockfile-only
directory: "/"
allow:
- dependency-type: "all"
groups:
all-deps:
patterns:
- "*"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/" directory: "/"
schedule: schedule:
interval: weekly interval: weekly
time: '13:00'
open-pull-requests-limit: 10 open-pull-requests-limit: 10
+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.60.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.60.0 # MSRV toolchain: 1.85.0 # MSRV
deps: true deps: true
- platform: macos-latest - platform: macos-latest
toolchain: 1.60.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.65.0 toolchain: 1.85.0 # MSRV
components: clippy components: clippy
override: true override: true
- run: sudo apt-get install libpcsclite-dev - run: sudo apt-get install libpcsclite-dev
+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
+87
View File
@@ -4,6 +4,93 @@ 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) ## 0.7.0 (2022-11-14)
### Added ### Added
- Display inner PC/SC errors ([#420]) - Display inner PC/SC errors ([#420])
Generated
+864 -674
View File
File diff suppressed because it is too large Load Diff
+35 -28
View File
@@ -1,10 +1,10 @@
[package] [package]
name = "yubikey" name = "yubikey"
version = "0.7.0" version = "0.9.0-pre.0"
description = """ description = """
Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with Pure Rust cross-platform host-side driver for YubiKey devices from Yubico with
support for hardware-backed public-key decryption and digital signatures using support for hardware-backed public-key decryption and digital signatures using
the Personal Identity Verification (PIV) application. Supports RSA (1024/2048) the Personal Identity Verification (PIV) application. Supports RSA (1024/2048/3072/4096)
or ECC (NIST P-256/P-384) algorithms e.g, PKCS#1v1.5, ECDSA or ECC (NIST P-256/P-384) algorithms e.g, PKCS#1v1.5, ECDSA
""" """
authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"] authors = ["Tony Arcieri <tony@iqlusion.io>", "Yubico AB"]
@@ -14,48 +14,55 @@ readme = "README.md"
categories = ["api-bindings", "authentication", "cryptography", "hardware-support"] categories = ["api-bindings", "authentication", "cryptography", "hardware-support"]
keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"] keywords = ["ecdsa", "encryption", "rsa", "piv", "signature"]
edition = "2021" edition = "2021"
rust-version = "1.60" 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.23" 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"
hex = { package = "base16ct", version = "0.1", features = ["alloc"] } des = "0.9.0-rc.3"
hmac = "0.12" 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.7" sha2 = { workspace = true, features = ["oid"] }
secrecy = "0.8" signature = "3.0.0-rc.10"
sha1 = { version = "0.10", features = ["oid"] }
sha2 = { version = "0.10", features = ["oid"] }
subtle = "2" subtle = "2"
uuid = { version = "1.2", features = ["v4"] } uuid = { version = "1.2", features = ["v4"] }
x509 = "0.2" x25519-dalek = "3.0.0-pre.6"
x509-parser = "0.14" x509-cert.workspace = true
zeroize = "1" zeroize = "1"
[dev-dependencies] [dev-dependencies]
env_logger = "0.9" env_logger = "0.11"
once_cell = "1" once_cell = "1"
rsa = { version = "0.7.1", features = ["hazmat"] }
signature = { version = "1.6.4", features = ["hazmat-preview"] }
[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"]
+16 -13
View File
@@ -22,7 +22,7 @@ access provided by the [`pcsc` crate].
## About ## About
YubiKeys are versatile devices and through their PIV support, you can use them YubiKeys are versatile devices and through their PIV support, you can use them
to store a number of RSA (2048/1024) and ECC (NIST P-256/P-384) private keys to store a number of RSA (1024/2048/3072/4096) and ECC (NIST P-256/P-384) private keys
with configurable access control policies. Both the signing (RSASSA/ECDSA) and with configurable access control policies. Both the signing (RSASSA/ECDSA) and
encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type. encryption (PKCS#1v1.5/ECIES) use cases are supported for either key type.
@@ -54,15 +54,19 @@ See [Yubico's guide to PIV-enabled YubiKeys][yk-guide] for more information
on which devices support PIV and the available functionality. on which devices support PIV and the available functionality.
### Supported Algorithms ### Supported Algorithms
- **Authentication**: `3DES` - **Authentication**: `3DES`
- **Encryption**: - **Encryption**:
- RSA: `RSA1024`, `RSA2048` - RSA: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384) - ECC: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
- **Signatures**: - **Signatures**:
- RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048` - RSASSA-PKCS#1v1.5: `RSA1024`, `RSA2048`, `RSA3072`, `RSA4096`
- ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384) - ECDSA: `ECCP256`, `ECCP384` (NIST curves: P-256, P-384)
NOTE: RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD) NOTE:
- RSASSA-PSS signatures and RSA-OAEP encryption may be supportable (TBD)
- `RSA3072` and `RSA4096` require a YubiKey with firmware 5.7 or newer.
## Minimum Supported Rust Version ## Minimum Supported Rust Version
@@ -73,8 +77,7 @@ Rust **1.60** or newer.
- [YubiKey 4] series - [YubiKey 4] series
- [YubiKey 5] series - [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported. NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
## Supported Operating Systems ## Supported Operating Systems
@@ -102,8 +105,8 @@ We would appreciate any help testing this functionality and removing the
## Testing ## Testing
To run the full test suite, you'll need a connected YubiKey NEO/4/5 device in To run the full test suite, you'll need a supported YubiKey device connected
the default state (i.e. default PIN/PUK). which is in the default state (i.e. default PIN/PUK).
Tests which run live against a YubiKey device are marked as `#[ignore]` by Tests which run live against a YubiKey device are marked as `#[ignore]` by
default in order to pass when running in a CI environment. To run these default in order to pass when running in a CI environment. To run these
@@ -172,7 +175,7 @@ Yubico's [yubico-piv-tool], a C library/CLI program. The original library
was licensed under a [2-Clause BSD License][BSDL], which this library inherits was licensed under a [2-Clause BSD License][BSDL], which this library inherits
as a derived work. as a derived work.
Copyright (c) 2014-2022 Yubico AB, Tony Arcieri Copyright (c) 2014-2025 Yubico AB, Tony Arcieri
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@@ -208,16 +211,16 @@ or conditions.
[//]: # (badges) [//]: # (badges)
[crate-image]: https://buildstats.info/crate/yubikey [crate-image]: https://img.shields.io/crates/v/yubikey?logo=rust
[crate-link]: https://crates.io/crates/yubikey [crate-link]: https://crates.io/crates/yubikey
[docs-image]: https://docs.rs/yubikey/badge.svg [docs-image]: https://docs.rs/yubikey/badge.svg
[docs-link]: https://docs.rs/yubikey/ [docs-link]: https://docs.rs/yubikey/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg [license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING [license-link]: https://github.com/iqlusioninc/yubikey.rs/blob/main/COPYING
[msrv-image]: https://img.shields.io/badge/rustc-1.60+-blue.svg [msrv-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/ [safety-link]: https://github.com/rust-secure-code/safety-dance/
[build-image]: https://github.com/iqlusioninc/yubikey.rs/workflows/CI/badge.svg?branch=main&event=push [build-image]: https://github.com/iqlusioninc/yubikey.rs/actions/workflows/ci.yml/badge.svg
[build-link]: https://github.com/iqlusioninc/yubikey.rs/actions [build-link]: https://github.com/iqlusioninc/yubikey.rs/actions
[deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg [deps-image]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs/status.svg
[deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs [deps-link]: https://deps.rs/repo/github/iqlusioninc/yubikey.rs
@@ -231,10 +234,10 @@ or conditions.
[PC/SC]: https://en.wikipedia.org/wiki/PC/SC [PC/SC]: https://en.wikipedia.org/wiki/PC/SC
[`pcsc` crate]: https://github.com/bluetech/pcsc-rust [`pcsc` crate]: https://github.com/bluetech/pcsc-rust
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4 [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/ [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
[piv-tool-guide]: https://docs.yubico.com/software/yubikey/tools/pivtool/Introduction.html
[Corrode]: https://github.com/jameysharp/corrode [Corrode]: https://github.com/jameysharp/corrode
[cc-web]: https://contributor-covenant.org/ [cc-web]: https://contributor-covenant.org/
[cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md [cc-md]: https://github.com/iqlusioninc/yubikey.rs/blob/main/CODE_OF_CONDUCT.md
+4
View File
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- MSRV is now 1.81.
## 0.7.0 (2022-11-14) ## 0.7.0 (2022-11-14)
### Changed ### Changed
- Bump `clap` to v4.0 ([#438]) - Bump `clap` to v4.0 ([#438])
+7 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "yubikey-cli" name = "yubikey-cli"
version = "0.7.0" version = "0.9.0-pre"
description = """ description = """
Command-line interface for performing encryption and signing using RSA/ECC keys Command-line interface for performing encryption and signing using RSA/ECC keys
stored on YubiKey devices. stored on YubiKey devices.
@@ -12,15 +12,15 @@ readme = "README.md"
categories = ["command-line-utilities", "cryptography", "hardware-support"] categories = ["command-line-utilities", "cryptography", "hardware-support"]
keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"] keywords = ["ecdsa", "rsa", "piv", "pcsc", "yubikey"]
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = "1.85"
[dependencies] [dependencies]
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
env_logger = "0.9" env_logger = "0.11"
hex = { package = "base16ct", version = "0.1", features = ["alloc"] } hex = { package = "base16ct", version = "0.2", features = ["alloc"] }
log = "0.4" log = "0.4"
once_cell = "1" once_cell = "1"
sha2 = "0.10" sha2.workspace = true
termcolor = "1" termcolor = "1"
x509-parser = "0.14" x509-cert.workspace = true
yubikey = { version = "0.7", path = ".." } yubikey = { version = "=0.9.0-pre.0", path = ".." }
+3 -5
View File
@@ -25,8 +25,7 @@ Rust **1.60** or newer.
- [YubiKey 4] series - [YubiKey 4] series
- [YubiKey 5] series - [YubiKey 5] series
NOTE: Nano and USB-C variants of the above are also supported. NOTE: Nano and USB-C variants of the above are also supported. NEO series is NOT supported.
Pre-YK4 [YubiKey NEO] series is **NOT** supported (see [#18]).
## Security Warning ## Security Warning
@@ -43,7 +42,7 @@ For more information, please see [CODE_OF_CONDUCT.md][cc-md].
## License ## License
Copyright (c) 2014-2022 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
@@ -84,7 +83,7 @@ or conditions.
[docs-image]: https://docs.rs/yubikey-cli/badge.svg [docs-image]: https://docs.rs/yubikey-cli/badge.svg
[docs-link]: https://docs.rs/yubikey-cli/ [docs-link]: https://docs.rs/yubikey-cli/
[license-image]: https://img.shields.io/badge/license-BSD-blue.svg [license-image]: https://img.shields.io/badge/license-BSD-blue.svg
[rustc-image]: https://img.shields.io/badge/rustc-1.60+-blue.svg [rustc-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg
[maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg [maintenance-image]: https://img.shields.io/badge/maintenance-experimental-blue.svg
[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg [safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg
[safety-link]: https://github.com/rust-secure-code/safety-dance/ [safety-link]: https://github.com/rust-secure-code/safety-dance/
@@ -98,7 +97,6 @@ or conditions.
[PIV]: https://piv.idmanagement.gov/ [PIV]: https://piv.idmanagement.gov/
[yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html [yk-guide]: https://developers.yubico.com/PIV/Introduction/YubiKey_and_PIV.html
[Yubico]: https://www.yubico.com/ [Yubico]: https://www.yubico.com/
[YubiKey NEO]: https://support.yubico.com/support/solutions/articles/15000006494-yubikey-neo
[YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4 [YubiKey 4]: https://support.yubico.com/support/solutions/articles/15000006486-yubikey-4
[YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/ [YubiKey 5]: https://www.yubico.com/products/yubikey-5-overview/
[yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/ [yubico-piv-tool]: https://github.com/Yubico/yubico-piv-tool/
+12 -27
View File
@@ -9,7 +9,7 @@ use std::{
sync::Mutex, sync::Mutex,
}; };
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)
@@ -180,52 +180,37 @@ pub fn print_cert_info(
return Ok(()); return Ok(());
} }
}; };
let buf = cert.into_buffer(); let cert = &cert.cert;
if !buf.is_empty() { let fingerprint = Sha256::digest(cert.to_der().unwrap());
let fingerprint = Sha256::digest(&buf);
let slot_id: u8 = slot.into(); let slot_id: u8 = slot.into();
print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?; print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
match parse_x509_certificate(&buf) {
Ok((_rem, cert)) => {
print_cert_attr( print_cert_attr(
stream, stream,
"Algorithm", "Algorithm",
cert.tbs_certificate.subject_pki.algorithm.algorithm, cert.tbs_certificate()
.subject_public_key_info()
.algorithm
.oid,
)?; )?;
print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?; print_cert_attr(stream, "Subject", cert.tbs_certificate().subject())?;
print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?; print_cert_attr(stream, "Issuer", cert.tbs_certificate().issuer())?;
print_cert_attr( print_cert_attr(
stream, stream,
"Fingerprint", "Fingerprint",
&hex::upper::encode_string(&fingerprint), hex::upper::encode_string(&fingerprint),
)?; )?;
print_cert_attr( print_cert_attr(
stream, stream,
"Not Before", "Not Before",
cert.tbs_certificate cert.tbs_certificate().validity().not_before,
.validity
.not_before
.to_rfc2822()
.unwrap(),
)?; )?;
print_cert_attr( print_cert_attr(
stream, stream,
"Not After", "Not After",
cert.tbs_certificate cert.tbs_certificate().validity().not_after,
.validity
.not_after
.to_rfc2822()
.unwrap(),
)?; )?;
}
_ => {
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();
}
+80 -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;
@@ -198,6 +204,15 @@ pub enum Ins {
/// Get slot metadata /// Get slot metadata
GetMetadata, GetMetadata,
/// Management // Read Config
ReadConfig,
/// Management // Write Config
WriteConfig,
/// Management // DeviceReset
DeviceReset,
/// Other/unrecognized instruction codes /// Other/unrecognized instruction codes
Other(u8), Other(u8),
} }
@@ -223,6 +238,12 @@ impl Ins {
Ins::Attest => 0xf9, Ins::Attest => 0xf9,
Ins::GetSerial => 0xf8, Ins::GetSerial => 0xf8,
Ins::GetMetadata => 0xf7, Ins::GetMetadata => 0xf7,
// Management
Ins::ReadConfig => 0x1d,
Ins::WriteConfig => 0x1c,
Ins::DeviceReset => 0x1f,
Ins::Other(code) => code, Ins::Other(code) => code,
} }
} }
@@ -231,6 +252,11 @@ impl Ins {
impl From<u8> for Ins { impl From<u8> for Ins {
fn from(code: u8) -> Self { fn from(code: u8) -> Self {
match code { match code {
// Management
0x1d => Ins::ReadConfig,
0x1c => Ins::WriteConfig,
0x1f => Ins::DeviceReset,
0x20 => Ins::Verify, 0x20 => Ins::Verify,
0x24 => Ins::ChangeReference, 0x24 => Ins::ChangeReference,
0x2c => Ins::ResetRetry, 0x2c => Ins::ResetRetry,
@@ -318,7 +344,7 @@ impl From<Vec<u8>> for Response {
} }
let sw = StatusWords::from( let sw = StatusWords::from(
(bytes[bytes.len() - 2] as u16) << 8 | (bytes[bytes.len() - 1] as u16), ((bytes[bytes.len() - 2] as u16) << 8) | (bytes[bytes.len() - 1] as u16),
); );
let len = bytes.len() - 2; let len = bytes.len() - 2;
@@ -353,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,
@@ -389,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
// //
@@ -410,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,
@@ -421,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,
@@ -439,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,
@@ -452,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,
@@ -466,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));
}
}
+11 -12
View File
@@ -30,8 +30,9 @@
// (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 rand_core::CryptoRng;
use std::fmt::{self, Debug, Display}; use std::fmt::{self, Debug, Display};
/// CCCID offset /// CCCID offset
@@ -48,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,
@@ -65,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))
} }
} }
@@ -90,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);
+315 -499
View File
@@ -33,90 +33,27 @@
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::Display; spki::{SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef},
use std::{fmt, ops::DerefMut}; time::Validity,
use x509::{der::Oid, RelativeDistinguishedName}; };
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()
}
/// Returns itself formatted as x509 compatible hex string
pub fn as_x509_hex(&self) -> String {
let data = self.to_bytes();
let raw_hex_string = format!("{:02X?}", data);
raw_hex_string
.replace(", ", ":")
.replace([']', '['], "")
.to_lowercase()
}
/// Returns itself formatted as x509 compatible int string
pub fn as_x509_int(&self) -> String {
let Serial(buint) = self;
format!("{}", buint)
}
}
impl Display for Serial {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_x509_hex())
}
}
/// Information about how a [`Certificate`] is stored within a YubiKey. /// Information about how a [`Certificate`] is stored within a YubiKey.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CertInfo { pub enum CertInfo {
@@ -148,210 +85,11 @@ impl From<CertInfo> for u8 {
} }
} }
impl x509::AlgorithmIdentifier for AlgorithmId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
// RSA encryption
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => &[1, 2, 840, 113_549, 1, 1, 1],
// EC Public Key
AlgorithmId::EccP256 | AlgorithmId::EccP384 => &[1, 2, 840, 10045, 2, 1],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
use x509::der::write::der_oid;
// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1):
// ```text
// ECParameters ::= CHOICE {
// namedCurve OBJECT IDENTIFIER
// -- implicitCurve NULL
// -- specifiedCurve SpecifiedECDomain
// }
// ```
match self {
AlgorithmId::EccP256 => der_oid(&[1, 2, 840, 10045, 3, 1, 7][..])(w),
AlgorithmId::EccP384 => der_oid(&[1, 3, 132, 0, 34][..])(w),
_ => Ok(w),
}
}
}
/// Information about a public key within a [`Certificate`].
#[derive(Clone, Eq, PartialEq)]
pub enum PublicKeyInfo {
/// RSA keys
Rsa {
/// RSA algorithm
algorithm: AlgorithmId,
/// Public key
pubkey: RsaPublicKey,
},
/// EC P-256 keys
EcP256(EcPublicKey<NistP256>),
/// EC P-384 keys
EcP384(EcPublicKey<NistP384>),
}
impl fmt::Debug for PublicKeyInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKeyInfo({:?})", self.algorithm())
}
}
impl PublicKeyInfo {
fn parse(subject_pki: &SubjectPublicKeyInfo<'_>) -> Result<Self> {
match subject_pki.algorithm.algorithm.to_string().as_str() {
OID_RSA_ENCRYPTION => {
let pubkey = read_pki::rsa_pubkey(&subject_pki.subject_public_key.data)?;
Ok(PublicKeyInfo::Rsa {
algorithm: match pubkey.n().bits() {
1024 => AlgorithmId::Rsa1024,
2048 => AlgorithmId::Rsa2048,
_ => return Err(Error::AlgorithmError),
},
pubkey,
})
}
OID_EC_PUBLIC_KEY => {
let key_bytes = &subject_pki.subject_public_key.data;
let algorithm_parameters = subject_pki
.algorithm
.parameters
.as_ref()
.ok_or(Error::InvalidObject)?;
match read_pki::ec_parameters(algorithm_parameters)? {
AlgorithmId::EccP256 => EcPublicKey::<NistP256>::from_bytes(key_bytes)
.map(PublicKeyInfo::EcP256)
.map_err(|_| Error::InvalidObject),
AlgorithmId::EccP384 => EcPublicKey::<NistP384>::from_bytes(key_bytes)
.map(PublicKeyInfo::EcP384)
.map_err(|_| Error::InvalidObject),
_ => Err(Error::AlgorithmError),
}
}
_ => Err(Error::InvalidObject),
}
}
/// Returns the algorithm that this public key can be used with.
pub fn algorithm(&self) -> AlgorithmId {
match self {
PublicKeyInfo::Rsa { algorithm, .. } => *algorithm,
PublicKeyInfo::EcP256(_) => AlgorithmId::EccP256,
PublicKeyInfo::EcP384(_) => AlgorithmId::EccP384,
}
}
}
impl x509::SubjectPublicKeyInfo for PublicKeyInfo {
type AlgorithmId = AlgorithmId;
type SubjectPublicKey = Vec<u8>;
fn algorithm_id(&self) -> AlgorithmId {
self.algorithm()
}
fn public_key(&self) -> Vec<u8> {
match self {
PublicKeyInfo::Rsa { pubkey, .. } => {
cookie_factory::gen_simple(write_pki::rsa_pubkey(pubkey), vec![])
.expect("can write to Vec")
}
PublicKeyInfo::EcP256(pubkey) => pubkey.as_bytes().to_vec(),
PublicKeyInfo::EcP384(pubkey) => pubkey.as_bytes().to_vec(),
}
}
}
/// Digest algorithms.
///
/// See RFC 4055 and RFC 8017.
enum DigestId {
/// Secure Hash Algorithm 256 (SHA256)
Sha256,
}
impl x509::AlgorithmIdentifier for DigestId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
// See https://tools.ietf.org/html/rfc4055#section-2.1
DigestId::Sha256 => &[2, 16, 840, 1, 101, 3, 4, 2, 1],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
// Parameters are an explicit NULL
// See https://tools.ietf.org/html/rfc8017#appendix-A.2.4
x509::der::write::der_null()(w)
}
}
enum SignatureId {
/// Public-Key Cryptography Standards (PKCS) #1 version 1.5 signature algorithm with
/// Secure Hash Algorithm 256 (SHA256) and Rivest, Shamir and Adleman (RSA) encryption
///
/// See RFC 4055 and RFC 8017.
Sha256WithRsaEncryption,
/// Elliptic Curve Digital Signature Algorithm (DSA) coupled with the Secure Hash
/// Algorithm 256 (SHA256) algorithm
///
/// See RFC 5758.
EcdsaWithSha256,
}
impl x509::AlgorithmIdentifier for SignatureId {
type AlgorithmOid = &'static [u64];
fn algorithm(&self) -> Self::AlgorithmOid {
match self {
SignatureId::Sha256WithRsaEncryption => &[1, 2, 840, 113_549, 1, 1, 11],
SignatureId::EcdsaWithSha256 => &[1, 2, 840, 10045, 4, 3, 2],
}
}
fn parameters<W: std::io::Write>(
&self,
w: cookie_factory::WriteContext<W>,
) -> cookie_factory::GenResult<W> {
// No parameters for any SignatureId
Ok(w)
}
}
/// Certificates /// 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 {
@@ -360,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)
@@ -480,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)
@@ -506,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.issuer 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![])
} }
} }
@@ -590,12 +250,7 @@ pub(crate) fn write_certificate(
) -> Result<()> { ) -> Result<()> {
let object_id = slot.object_id(); let object_id = slot.object_id();
if data.is_none() { if let Some(data) = data {
return txn.save_object(object_id, &[]);
}
let data = data.unwrap();
let mut buf = [0u8; CB_OBJ_MAX]; let mut buf = [0u8; CB_OBJ_MAX];
let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?; let mut offset = Tlv::write(&mut buf, TAG_CERT, data)?;
@@ -604,99 +259,260 @@ pub(crate) fn write_certificate(
offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?; offset += Tlv::write(&mut buf[offset..], TAG_CERT_LRC, &[])?;
txn.save_object(object_id, &buf[..offset]) txn.save_object(object_id, &buf[..offset])
} else {
txn.save_object(object_id, &[])
}
} }
mod read_pki { 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 nom::{combinator, sequence::pair, IResult}; use der::{
use rsa::{BigUint, RsaPublicKey}; asn1::{Any, OctetString},
oid::db::rfc5912,
use super::{OID_NIST_P256, OID_NIST_P384}; Encode, Sequence,
use crate::{piv::AlgorithmId, Error, Result};
/// From [RFC 8017](https://tools.ietf.org/html/rfc8017#appendix-A.1.1):
/// ```text
/// RSAPublicKey ::= SEQUENCE {
/// modulus INTEGER, -- n
/// publicExponent INTEGER -- e
/// }
/// ```
pub(super) fn rsa_pubkey(encoded: &[u8]) -> Result<RsaPublicKey> {
fn parse_rsa_pubkey(i: &[u8]) -> IResult<&[u8], (DerObject<'_>, DerObject<'_>), BerError> {
parse_der_sequence_defined_g(|i, _| pair(parse_der_integer, parse_der_integer)(i))(i)
}
fn rsa_pubkey_parts(i: &[u8]) -> IResult<&[u8], (BigUint, BigUint), BerError> {
combinator::map(parse_rsa_pubkey, |(modulus, public_exponent)| {
let n = match modulus.content {
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s),
_ => panic!("expected DER integer"),
}; };
let e = match public_exponent.content { use sha2::{Digest, Sha256, Sha384, Sha512};
BerObjectContent::Integer(s) => BigUint::from_bytes_be(s), use signature::Keypair;
_ => panic!("expected DER integer"), use std::{cell::RefCell, fmt, io::Write, marker::PhantomData};
use x509_cert::spki::{
self, AlgorithmIdentifierOwned, DynSignatureAlgorithmIdentifier, EncodePublicKey,
SignatureBitStringEncoding, SubjectPublicKeyInfoRef,
}; };
(n, e) type SigResult<T> = core::result::Result<T, signature::Error>;
})(i)
/// Key to be used to sign certificates
pub trait KeyType {
/// Error returned when working with signature
type Error: Into<signature::Error> + fmt::Debug;
/// The signature type returned by the signer
type Signature: SignatureBitStringEncoding
+ for<'s> TryFrom<&'s [u8], Error = Self::Error>
+ fmt::Debug;
/// The public key used to verify signature
type VerifyingKey: EncodePublicKey
+ DynSignatureAlgorithmIdentifier
+ Clone
+ From<Self::PublicKey>;
/// Public key type used to load the SPKI formatted key
type PublicKey: for<'a> TryFrom<SubjectPublicKeyInfoRef<'a>, Error = spki::Error>;
/// The algorithm used when talking with the yubikey
const ALGORITHM: AlgorithmId;
/// Prepare buffer before submitting it for signature
fn prepare(input: &[u8]) -> SigResult<Vec<u8>>;
/// Read back the signature from the device
fn read_signature(input: &[u8]) -> SigResult<Self::Signature>;
} }
let (n, e) = match rsa_pubkey_parts(encoded) { impl KeyType for ed25519_dalek::SigningKey {
Ok((_, res)) => res, const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519;
_ => return Err(Error::InvalidObject), type Error = ed25519_dalek::SignatureError;
type Signature = ed25519_dalek::Signature;
type VerifyingKey = ed25519_dalek::VerifyingKey;
type PublicKey = ed25519_dalek::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha512::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::try_from(input)
}
}
impl KeyType for p256::NistP256 {
const ALGORITHM: AlgorithmId = AlgorithmId::EccP256;
type Error = ecdsa::Error;
type Signature = p256::ecdsa::DerSignature;
type VerifyingKey = p256::ecdsa::VerifyingKey;
type PublicKey = p256::ecdsa::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha256::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::from_bytes(input)
}
}
impl KeyType for p384::NistP384 {
const ALGORITHM: AlgorithmId = AlgorithmId::EccP384;
type Error = ecdsa::Error;
type Signature = p384::ecdsa::DerSignature;
type VerifyingKey = p384::ecdsa::VerifyingKey;
type PublicKey = p384::ecdsa::VerifyingKey;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
Ok(Sha384::digest(input).to_vec())
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::from_bytes(input)
}
}
/// Trait used to handle subtypes of RSA keys
pub trait RsaLength {
/// The length of the RSA key in bits
const BIT_LENGTH: usize;
/// The algorithm to use when talking with the Yubikey.
const ALGORITHM: AlgorithmId;
}
/// RSA 1024 bits key
pub struct Rsa1024;
impl RsaLength for Rsa1024 {
const BIT_LENGTH: usize = 1024;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa1024;
}
/// RSA 2048 bits key
pub struct Rsa2048;
impl RsaLength for Rsa2048 {
const BIT_LENGTH: usize = 2048;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa2048;
}
/// RSA 3072 bits key
pub struct Rsa3072;
impl RsaLength for Rsa3072 {
const BIT_LENGTH: usize = 3072;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa3072;
}
/// RSA 4096 bits key
pub struct Rsa4096;
impl RsaLength for Rsa4096 {
const BIT_LENGTH: usize = 4096;
const ALGORITHM: AlgorithmId = AlgorithmId::Rsa4096;
}
/// RSA keys used to sign certificates
pub struct YubiRsa<N: RsaLength> {
_len: PhantomData<N>,
}
impl<N: RsaLength> KeyType for YubiRsa<N> {
type Error = signature::Error;
type Signature = rsa::pkcs1v15::Signature;
type VerifyingKey = rsa::pkcs1v15::VerifyingKey<Sha256>;
type PublicKey = rsa::RsaPublicKey;
const ALGORITHM: AlgorithmId = N::ALGORITHM;
fn prepare(input: &[u8]) -> SigResult<Vec<u8>> {
let hashed = Sha256::digest(input).to_vec();
OctetString::new(hashed)
.map_err(|e| e.into())
.and_then(Self::emsa_pkcs1_1_5)
.map_err(signature::Error::from_source)
}
fn read_signature(input: &[u8]) -> SigResult<Self::Signature> {
Self::Signature::try_from(input)
}
}
impl<N: RsaLength> YubiRsa<N> {
/// https://www.rfc-editor.org/rfc/rfc8017#section-9.2
fn emsa_pkcs1_1_5(digest: OctetString) -> Result<Vec<u8>> {
/// https://www.rfc-editor.org/rfc/rfc8017#appendix-A.2.4
#[derive(Debug, Sequence)]
struct DigestInfo {
digest_algorithm: AlgorithmIdentifierOwned,
digest: OctetString,
}
let em_len = N::BIT_LENGTH / 8;
let null = Any::null();
let t = DigestInfo {
digest_algorithm: AlgorithmIdentifierOwned {
oid: rfc5912::ID_SHA_256,
parameters: Some(null),
},
digest,
}; };
let t = t.to_der()?;
RsaPublicKey::new(n, e).map_err(|_| Error::InvalidObject) let ps = vec![0xff; em_len - t.len() - 3];
} assert!(ps.len() >= 8, "spec violation");
/// From [RFC 5480](https://tools.ietf.org/html/rfc5480#section-2.1.1): let mut out = Vec::with_capacity(em_len);
/// ```text out.write(&[0x00, 0x01]).map_err(|_| Error::MemoryError)?;
/// ECParameters ::= CHOICE { out.write(&ps).map_err(|_| Error::MemoryError)?;
/// namedCurve OBJECT IDENTIFIER out.write(&[0x00]).map_err(|_| Error::MemoryError)?;
/// -- implicitCurve NULL out.write(&t).map_err(|_| Error::MemoryError)?;
/// -- specifiedCurve SpecifiedECDomain Ok(out)
/// }
/// ```
pub(super) fn ec_parameters(parameters: &Any<'_>) -> Result<AlgorithmId> {
let curve_oid = parameters.as_oid().map_err(|_| Error::InvalidObject)?;
match curve_oid.to_string().as_str() {
OID_NIST_P256 => Ok(AlgorithmId::EccP256),
OID_NIST_P384 => Ok(AlgorithmId::EccP384),
_ => Err(Error::AlgorithmError),
}
} }
} }
mod write_pki { /// The entrypoint to sign data with the yubikey.
use cookie_factory::{SerializeFn, WriteContext}; pub struct Signer<'y, KT: KeyType> {
use rsa::{BigUint, PublicKeyParts, RsaPublicKey}; yubikey: RefCell<&'y mut YubiKey>,
use std::io::Write; key: SlotId,
use x509::der::write::{der_integer, der_sequence}; public_key: KT::VerifyingKey,
/// 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): impl<'y, KT: KeyType> Signer<'y, KT> {
/// ```text /// Create new Signer
/// RSAPublicKey ::= SEQUENCE { pub fn new(
/// modulus INTEGER, -- n yubikey: &'y mut YubiKey,
/// publicExponent INTEGER -- e key: SlotId,
/// } subject_pki: SubjectPublicKeyInfoRef<'_>,
/// ``` ) -> Result<Self> {
pub(super) fn rsa_pubkey<'a, W: Write + 'a>( let public_key = KT::PublicKey::try_from(subject_pki).map_err(|_| Error::ParseError)?;
pubkey: &'a RsaPublicKey, let public_key = public_key.into();
) -> impl SerializeFn<W> + 'a {
der_sequence(( Ok(Self {
der_integer_biguint(pubkey.n()), yubikey: RefCell::new(yubikey),
der_integer_biguint(pubkey.e()), key,
)) public_key,
})
}
}
impl<KT: KeyType> Keypair for Signer<'_, KT> {
type VerifyingKey = KT::VerifyingKey;
fn verifying_key(&self) -> <Self as Keypair>::VerifyingKey {
self.public_key.clone()
}
}
impl<KT: KeyType> DynSignatureAlgorithmIdentifier for Signer<'_, KT> {
fn signature_algorithm_identifier(&self) -> spki::Result<AlgorithmIdentifierOwned> {
self.verifying_key().signature_algorithm_identifier()
}
}
impl<KT: KeyType> signature::Signer<KT::Signature> for Signer<'_, KT> {
fn try_sign(&self, msg: &[u8]) -> SigResult<KT::Signature> {
let data = KT::prepare(msg)?;
let out = sign_data(
&mut self.yubikey.borrow_mut(),
&data,
KT::ALGORITHM,
self.key,
)
.map_err(signature::Error::from_source)?;
let out = KT::read_signature(&out)?;
Ok(out)
}
} }
} }
+7 -11
View File
@@ -30,7 +30,7 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (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::fmt::{self, Debug, Display}; use std::fmt::{self, Debug, Display};
use uuid::Uuid; use uuid::Uuid;
@@ -61,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,
@@ -86,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
@@ -99,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);
+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;
+38 -1
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,
@@ -125,8 +134,12 @@ impl Error {
match self { match self {
Error::AlgorithmError => f.write_str("algorithm error"), Error::AlgorithmError => f.write_str("algorithm error"),
Error::AppletError => f.write_str("applet error"), Error::AppletError => f.write_str("applet error"),
Error::AppletNotFound { applet_name } => {
f.write_str(&format!("{applet_name} applet not found"))
}
Error::ArgumentError => f.write_str("argument error"), Error::ArgumentError => f.write_str("argument error"),
Error::AuthenticationError => f.write_str("authentication error"), Error::AuthenticationError => f.write_str("authentication error"),
Error::CertificateBuilder => f.write_str("certificate builder error"),
Error::GenericError => f.write_str("generic error"), Error::GenericError => f.write_str("generic error"),
Error::InvalidObject => f.write_str("invalid object"), Error::InvalidObject => f.write_str("invalid object"),
Error::KeyError => f.write_str("key error"), Error::KeyError => f.write_str("key error"),
@@ -137,7 +150,7 @@ impl Error {
Error::PcscError { Error::PcscError {
inner: Some(pcsc_error), inner: Some(pcsc_error),
} => f.write_fmt(format_args!("PC/SC error: {}", pcsc_error)), } => f.write_fmt(format_args!("PC/SC error: {pcsc_error}")),
Error::PcscError { .. } => f.write_str("PC/SC error"), Error::PcscError { .. } => f.write_str("PC/SC error"),
@@ -155,6 +168,18 @@ impl Display for Error {
} }
} }
impl From<std::array::TryFromSliceError> for Error {
fn from(_: std::array::TryFromSliceError) -> Error {
Error::SizeError
}
}
impl From<std::time::SystemTimeError> for Error {
fn from(_: std::time::SystemTimeError) -> Error {
Error::GenericError
}
}
impl From<pcsc::Error> for Error { impl From<pcsc::Error> for Error {
fn from(err: pcsc::Error) -> Error { fn from(err: pcsc::Error) -> Error {
Error::PcscError { inner: Some(err) } Error::PcscError { inner: Some(err) }
@@ -170,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
}
}
+12 -4
View File
@@ -2,9 +2,16 @@
#![doc( #![doc(
html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png" html_logo_url = "https://raw.githubusercontent.com/iqlusioninc/yubikey.rs/main/img/logo-sq.png"
)] )]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)] #![warn(
clippy::mod_module_files,
clippy::unwrap_used,
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
// Adapted from yubico-piv-tool: // Adapted from yubico-piv-tool:
// <https://github.com/Yubico/yubico-piv-tool/> // <https://github.com/Yubico/yubico-piv-tool/>
@@ -44,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;
@@ -63,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
+789 -180
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];
+224 -114
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},
consts::CB_OBJ_MAX,
error::{Error, Result}, error::{Error, Result},
mgm::MgmAlgorithmId,
policy::{PinPolicy, TouchPolicy}, policy::{PinPolicy, TouchPolicy},
serialization::*, serialization::*,
setting, setting,
yubikey::YubiKey, yubikey::YubiKey,
Buffer, ObjectId, Buffer, ObjectId,
}; };
use elliptic_curve::sec1::EncodedPoint as EcPublicKey; 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},
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;
@@ -165,9 +177,9 @@ impl From<SlotId> for u8 {
impl Display for SlotId { impl Display for SlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
SlotId::Management(r) => write!(f, "{:?}", r), SlotId::Management(r) => write!(f, "{r:?}"),
SlotId::Retired(r) => write!(f, "{:?}", r), SlotId::Retired(r) => write!(f, "{r:?}"),
_ => write!(f, "{:?}", self), _ => write!(f, "{self:?}"),
} }
} }
} }
@@ -320,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:?}")
} }
} }
@@ -403,7 +415,7 @@ impl From<ManagementSlotId> for u8 {
impl Display for ManagementSlotId { impl Display for ManagementSlotId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self) write!(f, "{self:?}")
} }
} }
@@ -435,10 +447,11 @@ impl ManagementSlotId {
} }
/// Personal Identity Verification (PIV) key slots /// Personal Identity Verification (PIV) key slots
pub const SLOTS: [SlotId; 27] = [ 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),
@@ -474,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 {
@@ -488,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),
} }
} }
@@ -500,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,
} }
} }
} }
@@ -513,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,
} }
} }
} }
@@ -585,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";
@@ -600,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))
@@ -695,7 +738,6 @@ pub fn generate(
} }
#[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,
@@ -744,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,
@@ -760,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 {
@@ -804,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,
@@ -816,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),
} }
@@ -841,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,
@@ -866,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()?;
@@ -906,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],
@@ -922,34 +1000,21 @@ pub fn decrypt_data(
/// Read metadata /// Read metadata
pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> { pub fn metadata(yubikey: &mut YubiKey, slot: SlotId) -> Result<SlotMetadata> {
let txn = yubikey.begin_transaction()?; let txn = yubikey.begin_transaction()?;
let templ = [0, Ins::GetMetadata.code(), 0, slot.into()];
let response = txn.transfer_data(&templ, &[], CB_OBJ_MAX)?; txn.get_metadata(slot)
if !response.is_success() {
if response.status_words() == StatusWords::NotSupportedError {
return Err(Error::NotSupported); // Requires firmware 5.2.3
} else {
return Err(Error::GenericError);
}
}
let buf = Buffer::new(response.data().into());
SlotMetadata::try_from(buf)
} }
/// Metadata from a slot /// Metadata from a slot
#[derive(Debug)] #[derive(Debug)]
pub struct SlotMetadata { pub struct SlotMetadata {
/// Algorithm / Type of key /// Algorithm / Type of key
pub algorithm: ManagementAlgorithmId, pub algorithm: SlotAlgorithmId,
/// PIN and touch policy /// PIN and touch policy
pub policy: Option<(PinPolicy, TouchPolicy)>, pub policy: Option<(PinPolicy, TouchPolicy)>,
/// Imported or generated key /// Imported or generated key
pub origin: Option<Origin>, pub origin: Option<Origin>,
/// Pub key of the key /// Pub key of the key
pub public: Option<PublicKeyInfo>, pub public: Option<SubjectPublicKeyInfoOwned>,
/// Whether PIN PUK and management key are default /// Whether PIN PUK and management key are default
pub default: Option<bool>, pub default: Option<bool>,
/// Number of retries left /// Number of retries left
@@ -964,13 +1029,14 @@ impl TryFrom<Buffer> for SlotMetadata {
combinator::{eof, map_res}, combinator::{eof, map_res},
multi::fold_many1, multi::fold_many1,
number::complete::u8, number::complete::u8,
Parser,
}; };
let out = fold_many1( let out = fold_many1(
|input| Tlv::parse(input).map_err(|_| nom::Err::Error(())), |input| Tlv::parse(input).map_err(|_| nom::Err::Error(())),
|| { || {
Ok(SlotMetadata { Ok(SlotMetadata {
algorithm: ManagementAlgorithmId::PinPuk, algorithm: SlotAlgorithmId::PinPuk,
policy: None, policy: None,
origin: None, origin: None,
public: None, public: None,
@@ -981,15 +1047,15 @@ impl TryFrom<Buffer> for SlotMetadata {
|acc: Result<SlotMetadata>, tlv| match acc { |acc: Result<SlotMetadata>, tlv| match acc {
Ok(mut metadata) => match tlv.tag { Ok(mut metadata) => match tlv.tag {
1 => { 1 => {
metadata.algorithm = ManagementAlgorithmId::try_from(tlv.value[0])?; metadata.algorithm = SlotAlgorithmId::try_from(tlv.value[0])?;
Ok(metadata) Ok(metadata)
} }
2 => { 2 => {
fn policy_parser( fn policy_parser(
i: &[u8], i: &[u8],
) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> { ) -> nom::IResult<&[u8], (PinPolicy, TouchPolicy)> {
let (i, pin) = map_res(u8, PinPolicy::try_from)(i)?; let (i, pin) = map_res(u8, PinPolicy::try_from).parse(i)?;
let (i, touch) = map_res(u8, TouchPolicy::try_from)(i)?; let (i, touch) = map_res(u8, TouchPolicy::try_from).parse(i)?;
let (i, _) = eof(i)?; let (i, _) = eof(i)?;
Ok((i, (pin, touch))) Ok((i, (pin, touch)))
@@ -1001,7 +1067,7 @@ impl TryFrom<Buffer> for SlotMetadata {
} }
3 => { 3 => {
fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> { fn origin_parser(i: &[u8]) -> nom::IResult<&[u8], Origin> {
let (i, origin) = map_res(u8, Origin::try_from)(i)?; let (i, origin) = map_res(u8, Origin::try_from).parse(i)?;
let (i, _) = eof(i)?; let (i, _) = eof(i)?;
Ok((i, origin)) Ok((i, origin))
@@ -1013,7 +1079,7 @@ impl TryFrom<Buffer> for SlotMetadata {
} }
4 => { 4 => {
match metadata.algorithm { match metadata.algorithm {
ManagementAlgorithmId::Asymmetric(alg) => { SlotAlgorithmId::Asymmetric(alg) => {
metadata.public = Some(read_public_key(alg, tlv.value, false)?); metadata.public = Some(read_public_key(alg, tlv.value, false)?);
} }
_ => Err(Error::ParseError)?, _ => Err(Error::ParseError)?,
@@ -1061,7 +1127,8 @@ impl TryFrom<Buffer> for SlotMetadata {
}, },
err => err, err => err,
}, },
)(buf.as_ref()); )
.parse(buf.as_ref());
match out { match out {
Ok((_, res)) => res, Ok((_, res)) => res,
@@ -1104,12 +1171,45 @@ fn read_public_key(
algorithm: AlgorithmId, algorithm: AlgorithmId,
input: &[u8], input: &[u8],
skip_asn1_tag: bool, skip_asn1_tag: bool,
) -> Result<PublicKeyInfo> { ) -> Result<SubjectPublicKeyInfoOwned> {
// TODO(str4d): Response is wrapped in an ASN.1 TLV: // TODO(str4d): Response is wrapped in an ASN.1 TLV:
// //
// 0x7f 0x49 -> Application | Constructed | 0x49 // 0x7f 0x49 -> Application | Constructed | 0x49
match algorithm { match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { AlgorithmId::X25519 | AlgorithmId::Ed25519 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
// are shorter than 128 bytes, fitting into a definite short ASN.1 length).
let data = if skip_asn1_tag { &input[3..] } else { input };
let (_, tlv) = Tlv::parse(data)?;
let pk_data: [u8; 32] = tlv.value.try_into().map_err(|_| Error::InvalidObject)?;
match algorithm {
AlgorithmId::Ed25519 => SubjectPublicKeyInfoOwned::from_der(
ed25519_dalek::VerifyingKey::from_bytes(&pk_data)
.map_err(|_| Error::InvalidObject)?
.to_public_key_der()
.map_err(|_| Error::InvalidObject)?
.as_bytes(),
)
.map_err(|_| Error::InvalidObject),
AlgorithmId::X25519 => Ok(SubjectPublicKeyInfoOwned {
algorithm: AlgorithmIdentifier {
oid: OID_X25519,
parameters: None,
},
subject_public_key: BitString::from_bytes(
x25519_dalek::PublicKey::from(pk_data).as_bytes(),
)
.map_err(|_| Error::InvalidObject)?,
}),
_ => Err(Error::AlgorithmError),
}
}
AlgorithmId::Rsa1024
| AlgorithmId::Rsa2048
| AlgorithmId::Rsa3072
| AlgorithmId::Rsa4096 => {
// It appears that the inner application-specific value returned by the // It appears that the inner application-specific value returned by the
// YubiKey is constructed such that RSA pubkeys can be parsed in two ways: // YubiKey is constructed such that RSA pubkeys can be parsed in two ways:
// //
@@ -1153,14 +1253,17 @@ fn read_public_key(
} }
let exp = exp_tlv.value.to_vec(); let exp = exp_tlv.value.to_vec();
Ok(PublicKeyInfo::Rsa { let pubkey = RsaPublicKey::new(
algorithm, BoxedUint::from_be_slice_vartime(&modulus),
pubkey: RsaPublicKey::new( BoxedUint::from_be_slice_vartime(&exp),
BigUint::from_bytes_be(&modulus),
BigUint::from_bytes_be(&exp),
) )
.map_err(|_| Error::InvalidObject)?, .map_err(|_| Error::InvalidObject)?;
}) Ok(SubjectPublicKeyInfoOwned::from_der(
pubkey
.to_public_key_der()
.map_err(|_| Error::ParseError)?
.as_bytes(),
)?)
} }
AlgorithmId::EccP256 | AlgorithmId::EccP384 => { AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
// 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths // 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths
@@ -1188,49 +1291,56 @@ fn read_public_key(
let point = tlv.value.to_vec(); let point = tlv.value.to_vec();
match algorithm { let pubkey = match algorithm {
AlgorithmId::EccP256 => { AlgorithmId::EccP256 => PublicKey::<NistP256>::try_from(
EcPublicKey::<NistP256>::from_bytes(point).map(PublicKeyInfo::EcP256) EcPublicKey::<NistP256>::from_bytes(point).map_err(|_| Error::InvalidObject)?,
} )
AlgorithmId::EccP384 => { .map_err(|_| Error::InvalidObject)?
EcPublicKey::<NistP384>::from_bytes(point).map(PublicKeyInfo::EcP384) .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), _ => return Err(Error::AlgorithmError),
} }
.map_err(|_| Error::InvalidObject) .map_err(|_| Error::InvalidObject)?;
Ok(SubjectPublicKeyInfoOwned::from_der(pubkey.as_bytes())?)
} }
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Algorithms as reported by the metadata command. /// Algorithms as reported by the metadata command.
pub enum ManagementAlgorithmId { pub enum SlotAlgorithmId {
/// Used on PIN and PUK slots. /// Used on PIN and PUK slots.
PinPuk, PinPuk,
/// Used on the key management slot. /// Used on the key management slot.
ThreeDes, Management(MgmAlgorithmId),
/// Used on all other slots. /// Used on all other slots.
Asymmetric(AlgorithmId), Asymmetric(AlgorithmId),
} }
impl TryFrom<u8> for ManagementAlgorithmId { impl TryFrom<u8> for SlotAlgorithmId {
type Error = Error; type Error = Error;
fn try_from(value: u8) -> Result<Self> { fn try_from(value: u8) -> Result<Self> {
match value { match value {
0xff => Ok(ManagementAlgorithmId::PinPuk), 0xff => Ok(SlotAlgorithmId::PinPuk),
0x03 => Ok(ManagementAlgorithmId::ThreeDes), oth => MgmAlgorithmId::try_from(oth)
oth => AlgorithmId::try_from(oth).map(ManagementAlgorithmId::Asymmetric), .map(SlotAlgorithmId::Management)
.or_else(|_| AlgorithmId::try_from(oth).map(SlotAlgorithmId::Asymmetric)),
} }
} }
} }
impl From<ManagementAlgorithmId> for u8 { impl From<SlotAlgorithmId> for u8 {
fn from(id: ManagementAlgorithmId) -> u8 { fn from(id: SlotAlgorithmId) -> u8 {
match id { match id {
ManagementAlgorithmId::PinPuk => 0xff, SlotAlgorithmId::PinPuk => 0xff,
ManagementAlgorithmId::ThreeDes => 0x03, SlotAlgorithmId::Management(oth) => oth.into(),
ManagementAlgorithmId::Asymmetric(oth) => oth.into(), SlotAlgorithmId::Asymmetric(oth) => oth.into(),
} }
} }
} }
+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,
+186 -91
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);
}
let resp = Apdu::new(0x01).p1(0x10).transmit(self, 0xFF)?; if !response.is_success() {
// TODO(tarcieri): still reselect the PIV applet in this case?
if !resp.is_success() {
error!( error!(
"failed retrieving serial number: {:04x}", "failed retrieving serial number: {:04x}",
resp.status_words().code() response.status_words().code()
); );
return Err(Error::GenericError); return Err(Error::GenericError);
} }
// reselect the PIV applet // reselect the PIV applet
let sw = Apdu::new(Ins::SelectApplication) self.select_application(
.p1(0x04) piv::APPLET_ID,
.data(PIV_AID) piv::APPLET_NAME,
.transmit(self, 0xFF)? "failed selecting application",
.status_words(); )?;
if !sw.is_success() { response.data().try_into()
error!("failed selecting application: {:04x}", sw.code());
return Err(Error::GenericError);
} }
resp
} else {
// YK5 implements getting the serial as a PIV applet command (0xf8) // YK5 implements getting the serial as a PIV applet command (0xf8)
let resp = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?; 5 => {
let response = Apdu::new(Ins::GetSerial).transmit(self, 0xFF)?;
if !resp.is_success() { if !response.is_success() {
error!( error!(
"failed retrieving serial number: {:04x}", "failed retrieving serial number: {:04x}",
resp.status_words().code() response.status_words().code()
); );
return Err(Error::GenericError); return Err(Error::GenericError);
} }
resp response.data().try_into()
}; }
response.data()[..4] // Other versions unsupported
.try_into() _ => Err(Error::NotSupported),
.map(|serial| Serial::from(u32::from_be_bytes(serial))) }
.map_err(|_| Error::SizeError) }
/// Read metadata
pub(crate) fn get_metadata(&self, slot: SlotId) -> Result<piv::SlotMetadata> {
let response = Apdu::new(Ins::GetMetadata)
.p2(slot.into())
.transmit(self, CB_OBJ_MAX)?;
match response.status_words() {
StatusWords::Success => {
let buf = Buffer::new(response.data().into());
piv::SlotMetadata::try_from(buf)
}
StatusWords::ReferenceDataNotFoundError => Err(Error::NotFound),
// Requires firmware 5.2.3
StatusWords::NotSupportedError => Err(Error::NotSupported),
_ => Err(Error::GenericError),
}
} }
/// Verify device PIN. /// Verify device PIN.
@@ -177,7 +191,7 @@ impl<'tx> Transaction<'tx> {
if !pin.is_empty() { if !pin.is_empty() {
let mut data = Zeroizing::new([0xff; CB_PIN_MAX]); let mut data = Zeroizing::new([0xff; CB_PIN_MAX]);
data[0..pin.len()].copy_from_slice(pin); data[0..pin.len()].copy_from_slice(pin);
query.data(data.as_ref()); query.data(data.as_slice());
} }
let response = query.transmit(self, 261)?; let response = query.transmit(self, 261)?;
@@ -234,15 +248,14 @@ impl<'tx> Transaction<'tx> {
} }
/// Set the management key (MGM). /// Set the management key (MGM).
#[cfg(feature = "untested")]
pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> { pub fn set_mgm_key(&self, new_key: &MgmKey, require_touch: bool) -> Result<()> {
let p2 = if require_touch { 0xfe } else { 0xff }; let p2 = if require_touch { 0xfe } else { 0xff };
let mut data = [0u8; DES_LEN_3DES + 3]; let mut data = Vec::with_capacity(usize::from(new_key.key_size()) + 3);
data[0] = ALGO_3DES; data.push(new_key.algorithm_id().into());
data[1] = KEY_CARDMGM; data.push(KEY_CARDMGM);
data[2] = DES_LEN_3DES as u8; data.push(new_key.key_size());
data[3..3 + DES_LEN_3DES].copy_from_slice(new_key.as_ref()); data.extend_from_slice(new_key.as_ref());
let status_words = Apdu::new(Ins::SetMgmKey) let status_words = Apdu::new(Ins::SetMgmKey)
.params(0xff, p2) .params(0xff, p2)
@@ -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)
}
} }
+238 -100
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), let mut readers = Context::open()?;
} => Error::NotFound, for reader in readers.iter()? {
other => other, if let Ok(yk_found) = reader.open() {
})?; if let Some(yk_stored) = yubikey {
let mut reader_iter = readers.iter()?; // We found two YubiKeys, so we won't use either.
// Don't reset them.
let _ = yk_stored.disconnect(pcsc::Disposition::LeaveCard);
let _ = yk_found.disconnect(pcsc::Disposition::LeaveCard);
if let Some(reader) = reader_iter.next() {
if reader_iter.next().is_some() {
error!("multiple YubiKeys detected!"); error!("multiple YubiKeys detected!");
return Err(Error::PcscError { inner: None }); return Err(Error::PcscError { inner: None });
} else {
yubikey = Some(yk_found);
}
}
} }
return reader.open(); if let Some(yubikey) = yubikey {
} // We found exactly one YubiKey that we could open, so we return it.
Ok(yubikey)
} else {
error!("no YubiKey detected!"); error!("no YubiKey detected!");
Err(Error::NotFound) Err(Error::NotFound)
} }
}
/// Open a YubiKey with a specific serial number. /// Open a YubiKey with a specific serial number.
pub fn open_by_serial(serial: Serial) -> Result<Self> { pub fn open_by_serial(serial: Serial) -> Result<Self> {
let mut readers = Context::open().map_err(|e| match e { let mut readers = Context::open()?;
Error::PcscError {
inner: Some(pcsc::Error::NoReadersAvailable), let mut open_error = None;
} => Error::NotFound,
other => other,
})?;
for reader in readers.iter()? { for reader in readers.iter()? {
let yubikey = match reader.open() { let yubikey = match reader.open() {
Ok(yk) => yk, Ok(yk) => yk,
Err(_) => continue, Err(e) => {
// Save the first error we see that indicates we might have been able
// to find a matching YubiKey.
if open_error.is_none() {
if let Error::PcscError {
inner: Some(pcsc::Error::SharingViolation),
} = e
{
open_error = Some(e);
}
}
continue;
}
}; };
if serial == yubikey.serial() { if serial == yubikey.serial() {
return Ok(yubikey); return Ok(yubikey);
} else {
// We didn't want this YubiKey; don't reset it.
let _ = yubikey.disconnect(pcsc::Disposition::LeaveCard);
} }
} }
Err(if let Some(e) = open_error {
e
} else {
error!("no YubiKey detected with serial: {}", serial); error!("no YubiKey detected with serial: {}", serial);
Err(Error::NotFound) Error::NotFound
})
} }
/// Reconnect to a YubiKey. /// Reconnect to a YubiKey.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reconnect(&mut self) -> Result<()> { pub fn reconnect(&mut self) -> Result<()> {
info!("trying to reconnect to current reader"); info!("trying to reconnect to current reader");
@@ -223,13 +321,10 @@ impl YubiKey {
pcsc::Disposition::ResetCard, pcsc::Disposition::ResetCard,
)?; )?;
let pin = self let pin = self.pin.as_ref().map(|p| Buffer::new(p.clone()));
.pin
.as_ref()
.map(|p| Buffer::new(p.expose_secret().clone()));
let txn = Transaction::new(&mut self.card)?; let txn = Transaction::new(&mut self.card)?;
txn.select_application()?; txn.select_piv_application()?;
if let Some(p) = &pin { if let Some(p) = &pin {
txn.verify_pin(p)?; txn.verify_pin(p)?;
@@ -238,6 +333,41 @@ impl YubiKey {
Ok(()) Ok(())
} }
/// Disconnect from the YubiKey.
///
/// In case of error, ownership of the YubiKey is returned to the caller.
///
/// # Note
///
/// `YubiKey` implements `Drop` which automatically disconnects the card using
/// `Disposition::ResetCard`; you only need to call this function if you want to
/// handle errors or use a different disposition method.
pub fn disconnect(
self,
disposition: pcsc::Disposition,
) -> core::result::Result<(), (Self, Error)> {
let Self {
card,
name,
pin,
version,
serial,
} = self;
card.disconnect(disposition).map_err(|(card, e)| {
(
Self {
card,
name,
pin,
version,
serial,
},
e.into(),
)
})
}
/// Begin a transaction. /// Begin a transaction.
pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> { pub(crate) fn begin_transaction(&mut self) -> Result<Transaction<'_>> {
// TODO(tarcieri): reconnect support // TODO(tarcieri): reconnect support
@@ -279,37 +409,46 @@ impl YubiKey {
} }
/// Authenticate to the card using the provided management key (MGM). /// Authenticate to the card using the provided management key (MGM).
pub fn authenticate(&mut self, mgm_key: MgmKey) -> Result<()> { pub fn authenticate(&mut self, mgm_key: &MgmKey) -> Result<()> {
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
// get a challenge from the card // get a challenge from the card
let challenge = Apdu::new(Ins::Authenticate) let card_response = Apdu::new(Ins::Authenticate)
.params(ALGO_3DES, KEY_CARDMGM) .params(mgm_key.algorithm_id().into(), KEY_CARDMGM)
.data([TAG_DYN_AUTH, 0x02, 0x80, 0x00]) .data([TAG_DYN_AUTH, 0x02, 0x80, 0x00])
.transmit(&txn, 261)?; .transmit(&txn, 261)?;
if !challenge.is_success() || challenge.data().len() < 12 { if !card_response.is_success() || card_response.data().len() < 5 {
return Err(Error::AuthenticationError); return Err(Error::AuthenticationError);
} }
// send a response to the cards challenge and a challenge of our own. // send a response to the cards challenge and a challenge of our own.
let response = mgm_key.decrypt(challenge.data()[4..12].try_into().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)?;
@@ -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,7 +683,6 @@ 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()?;
@@ -575,12 +695,15 @@ impl YubiKey {
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;
@@ -611,7 +734,6 @@ impl YubiKey {
/// ///
/// The reset function is only available when both pins are blocked. /// The reset function is only available when both pins are blocked.
#[cfg(feature = "untested")] #[cfg(feature = "untested")]
#[cfg_attr(docsrs, doc(cfg(feature = "untested")))]
pub fn reset_device(&mut self) -> Result<()> { pub fn reset_device(&mut self) -> Result<()> {
let templ = [0, Ins::Reset.code(), 0, 0]; let templ = [0, Ins::Reset.code(), 0, 0];
let txn = self.begin_transaction()?; let txn = self.begin_transaction()?;
@@ -629,22 +751,36 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
type Error = Error; type Error = Error;
fn try_from(reader: &'a Reader<'_>) -> Result<Self> { fn try_from(reader: &'a Reader<'_>) -> Result<Self> {
let mut card = reader.connect().map_err(|e| { let mut card = reader
error!("error connecting to reader '{}': {}", reader.name(), e); .connect()
e .inspect_err(|e| error!("error connecting to reader '{}': {}", reader.name(), e))?;
})?;
info!("connected to reader: {}", reader.name()); info!("connected to reader: {}", reader.name());
let (version, serial) = { let mut app_version_serial = || -> Result<(Version, Serial)> {
let txn = Transaction::new(&mut card)?; let txn = Transaction::new(&mut card)?;
txn.select_application()?; txn.select_piv_application()?;
let v = txn.get_version()?; let v = txn.get_version()?;
let s = txn.get_serial(v)?; let s = txn.get_serial(v)?;
(v, s) Ok((v, s))
}; };
match app_version_serial() {
Err(e) => {
error!("Could not use reader: {}", e);
// We were unable to use the card, so we've effectively only connected as
// a side-effect of determining this. Avoid disrupting its internal state
// any further (e.g. preserve the PIN cache of whatever applet is selected
// currently).
if let Err((_, e)) = card.disconnect(pcsc::Disposition::LeaveCard) {
error!("Failed to disconnect gracefully from card: {}", e);
}
Err(e)
}
Ok((version, serial)) => {
let yubikey = YubiKey { let yubikey = YubiKey {
card, card,
name: String::from(reader.name()), name: String::from(reader.name()),
@@ -656,3 +792,5 @@ impl<'a> TryFrom<&'a Reader<'_>> for YubiKey {
Ok(yubikey) Ok(yubikey)
} }
} }
}
}
+179 -95
View File
@@ -3,17 +3,16 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)] #![warn(missing_docs, rust_2018_idioms, trivial_casts, unused_qualifications)]
use cipher::common::{getrandom::SysRng, Generate};
use log::trace; use log::trace;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand_core::{OsRng, RngCore}; use rsa::{pkcs1v15, RsaPublicKey};
use rsa::pkcs1v15;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use signature::{hazmat::PrehashVerifier, Signature as _}; use signature::hazmat::PrehashVerifier;
use std::{env, str::FromStr, sync::Mutex}; use std::{env, str::FromStr, sync::Mutex, time::Duration};
use x509::RelativeDistinguishedName; use x509_cert::{der::Encode, name::Name, serial_number::SerialNumber, time::Validity};
use yubikey::{ use yubikey::{
certificate, certificate::{yubikey_signer, Certificate},
certificate::{Certificate, PublicKeyInfo},
piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId}, piv::{self, AlgorithmId, Key, ManagementSlotId, RetiredSlotId, SlotId},
Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey, Error, MgmKey, PinPolicy, Serial, TouchPolicy, YubiKey,
}; };
@@ -44,12 +43,15 @@ static YUBIKEY: Lazy<Mutex<YubiKey>> = Lazy::new(|| {
#[test] #[test]
#[ignore] #[ignore]
fn test_get_cccid() { fn test_get_cccid() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
match yubikey.cccid() { match yubikey.cccid() {
Ok(cccid) => trace!("CCCID: {:?}", cccid), Ok(cccid) => trace!("CCCID: {:?}", cccid),
Err(Error::NotFound) => trace!("CCCID not found"), Err(Error::NotFound) => trace!("CCCID not found"),
Err(err) => panic!("error getting CCCID: {:?}", err), Err(err) => panic!("error getting CCCID: {err:?}"),
} }
} }
@@ -60,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:?}"),
} }
} }
@@ -76,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());
@@ -89,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());
@@ -102,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());
} }
@@ -115,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);
@@ -159,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());
@@ -189,52 +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, .. } => pkcs1v15::VerifyingKey::<Sha256>::from(pubkey.clone()), 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 = pkcs1v15::Signature::from_bytes(&data[data.len() - 128..]).unwrap(); let sig = pkcs1v15::Signature::try_from(&data[data.len() - 128..]).unwrap();
let hash = Sha256::digest(msg); let hash = Sha256::digest(msg);
assert!(pubkey.verify_prehash(&hash, &sig).is_ok()); assert!(pubkey.verify_prehash(&hash, &sig).is_ok());
} }
#[test]
#[ignore]
fn generate_rsa3072() {
let mut yubikey = YUBIKEY.lock().unwrap();
let version = yubikey.version();
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1);
// Generate a new key in the selected slot.
let generated = piv::generate(
&mut yubikey,
slot,
AlgorithmId::Rsa3072,
PinPolicy::Default,
TouchPolicy::Default,
);
match generated {
Ok(key) => {
let pubkey = key.subject_public_key;
assert!(pubkey.bit_len() > 3072)
}
Err(e) => assert!((version.major, version.minor) < (5, 7) && e == Error::AlgorithmError),
}
}
#[test] #[test]
#[ignore] #[ignore]
fn generate_self_signed_ec_cert() { fn generate_self_signed_ec_cert() {
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() {
@@ -269,11 +342,11 @@ fn test_slot_id_display() {
assert_eq!( assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Pin)), format!("{}", SlotId::Management(ManagementSlotId::Pin)),
"PIN" "Pin"
); );
assert_eq!( assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Puk)), format!("{}", SlotId::Management(ManagementSlotId::Puk)),
"PUK" "Puk"
); );
assert_eq!( assert_eq!(
format!("{}", SlotId::Management(ManagementSlotId::Management)), format!("{}", SlotId::Management(ManagementSlotId::Management)),
@@ -288,10 +361,14 @@ fn test_slot_id_display() {
#[test] #[test]
#[ignore] #[ignore]
fn test_read_metadata() { fn test_read_metadata() {
let mut yubikey = YUBIKEY.lock().unwrap(); let mut yubikey = match YUBIKEY.lock() {
Ok(yubikey) => yubikey,
Err(poison) => poison.into_inner(),
};
let default_key = MgmKey::get_default(&yubikey).unwrap();
assert!(yubikey.verify_pin(b"123456").is_ok()); assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(MgmKey::default()).is_ok()); assert!(yubikey.authenticate(&default_key).is_ok());
let slot = SlotId::Retired(RetiredSlotId::R1); let slot = SlotId::Retired(RetiredSlotId::R1);
@@ -305,61 +382,68 @@ fn test_read_metadata() {
) )
.unwrap(); .unwrap();
let metadata = piv::metadata(&mut yubikey, slot).unwrap(); match piv::metadata(&mut yubikey, slot) {
Ok(metadata) => assert_eq!(metadata.public, Some(generated)),
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] #[test]
#[ignore] #[ignore]
fn test_serial_string_conversions() { fn test_read_metadata_missing_key() {
//2^152+1 let mut yubikey = YUBIKEY.lock().unwrap();
let serial: [u8; 20] = [ let default_key = MgmKey::get_default(&yubikey).unwrap();
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, assert!(yubikey.verify_pin(b"123456").is_ok());
assert!(yubikey.authenticate(&default_key).is_ok());
// we assume that at least one of these slots is empty
let slots_to_check = [
RetiredSlotId::R10,
RetiredSlotId::R11,
RetiredSlotId::R12,
RetiredSlotId::R13,
RetiredSlotId::R14,
RetiredSlotId::R15,
RetiredSlotId::R16,
RetiredSlotId::R17,
RetiredSlotId::R18,
RetiredSlotId::R19,
RetiredSlotId::R20,
]; ];
let s = certificate::Serial::from(serial); for slot in slots_to_check {
assert_eq!( let slot = SlotId::Retired(slot);
s.as_x509_int(),
"5708990770823839524233143877797980545530986497"
);
assert_eq!(
s.as_x509_hex(),
"01:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01"
);
let serial2: [u8; 20] = [ match piv::metadata(&mut yubikey, slot) {
0xA1, 0xF3, 0x02, 0x30, 0x76, 0x01, 0x32, 0x48, 0x09, 0x9C, 0x10, 0xAA, 0x3F, 0xA0, 0x54, Ok(_) => {
0x0D, 0xC0, 0xB7, 0x65, 0x01, eprintln!("Key {} exists", slot);
]; }
Err(Error::NotSupported) => {
// Some YubiKeys don't support metadata
eprintln!("metadata not supported by this YubiKey");
return;
}
Err(Error::NotFound) => {
eprintln!("Key {} doesn't exist, ok.", slot);
return;
}
Err(err) => panic!("{}", err),
}
}
let s2 = certificate::Serial::from(serial2); panic!("No empty slots to check");
assert_eq!(
s2.as_x509_int(),
"924566785900861696177829411010986812227211191553"
);
assert_eq!(
s2.as_x509_hex(),
"a1:f3:02:30:76:01:32:48:09:9c:10:aa:3f:a0:54:0d:c0:b7:65:01"
);
let serial3: [u8; 20] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0x3F, 0xA0, 0x54,
0x0D, 0xC0, 0xB7, 0x65, 0x01,
];
let s3 = certificate::Serial::from(serial3);
assert_eq!(s3.as_x509_int(), "3140531249369331492097");
assert_eq!(s3.as_x509_hex(), "aa:3f:a0:54:0d:c0:b7:65:01");
} }
#[test] #[test]
#[ignore] #[ignore]
fn test_parse_cert_from_der() { fn test_parse_cert_from_der() {
let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found"); let bob_der = std::fs::read("tests/assets/Bob.der").expect(".der file not found");
let cert = let cert = Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
certificate::Certificate::from_bytes(bob_der).expect("Failed to parse valid certificate");
assert_eq!( assert_eq!(
cert.subject(), cert.subject(),
"CN=Bob", "CN=Bob",