Compare commits

...

80 Commits

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