diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7d4e1f3..626612d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,12 @@ -# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-FileCopyrightText: 2021-2022 Nora Widdecke # SPDX-License-Identifier: CC0-1.0 stages: - lint - test - - virtual-test + - virtual-cards + - udeps - hw-builddeps - hw-import - hw-keygen @@ -46,6 +47,44 @@ cargo-fmt: - cargo +nightly fmt -- --check cache: [ ] +cargo-clippy: + stage: lint + image: rust:latest + before_script: + - mkdir -p /run/user/$UID + - apt update -y -qq + - apt install -y -qq --no-install-recommends git clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev + - apt clean + - *report-rust + script: + - rustup component add clippy + - cargo clippy --verbose --tests -- -D warnings + allow_failure: true + cache: + # inherit all general cache settings + <<: *general_cache_config + # override the key + key: "rust-latest" + +semver-checks: + stage: lint + image: rust:latest + before_script: + - mkdir -p /run/user/$UID + - apt update -y -qq + - apt install -y -qq --no-install-recommends git clang make cmake pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev + - apt clean + - *report-rust + script: + - cargo install cargo-semver-checks + - cargo semver-checks -p card-backend -p card-backend-pcsc -p card-backend-scdc -p openpgp-card -p openpgp-card-sequoia + allow_failure: true + cache: + # inherit all general cache settings + <<: *general_cache_config + # override the key + key: "rust-latest" + cargo-deny: stage: lint image: rust:latest @@ -62,26 +101,9 @@ cargo-deny: - cargo/bin/cargo-deny key: "deny" -cargo-clippy: - stage: lint - image: rust:latest - before_script: - - mkdir -p /run/user/$UID - - apt update -y -qq - - apt install -y -qq --no-install-recommends git clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev - - apt clean - - *report-rust - script: - - rustup component add clippy - - cargo clippy --verbose --tests -- -D warnings - cache: - # inherit all general cache settings - <<: *general_cache_config - # override the key - key: "rust-latest" - udeps: - stage: lint + stage: udeps + needs: [ ] image: rustlang/rust:nightly-slim before_script: - mkdir -p /run/user/$UID @@ -121,52 +143,33 @@ cargo-test-debian-bookworm: - apt install -y -qq --no-install-recommends git rustc cargo clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev - apt clean - *report-rust + - cargo update + - cargo update -p sequoia-openpgp --precise 1.15.0 + - cargo update -p buffered-reader --precise 1.1.5 + - cargo update -p regex --precise 1.9.6 + - cargo update -p lalrpop@0.20.0 --precise 0.19.12 # hack to work with Rust 1.63 + - cargo update -p petgraph --precise 0.6.3 # hack to work with Rust 1.63 script: # there is no virtual card in this image, so subplot does not generate tests # that would require one. - cargo test + allow_failure: true cache: # inherit all general cache settings <<: *general_cache_config # override the key key: "bookworm" -subplot: - stage: virtual-test - image: registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps - before_script: - - mkdir -p /run/user/$UID - - apt update -y -qq - - > - apt install -y -qq --no-install-recommends - git clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates - libpcsclite-dev - sq - - apt clean - - /etc/init.d/pcscd start - - su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim - - *report-rust - script: - # make sure a virtual card is available, so that the subplot tests are - # generated - - CARD_BASED_TESTS=true cargo test -- --test-threads 1 - cache: - # inherit all general cache settings - <<: *general_cache_config - # subplot uses tests/virtual-card-available to indicate that tests which use - # virtual cards should be created. The cache with this file should not be - # shared. - key: "subplot" - run_cardtest_smartpgp: - stage: virtual-test + stage: virtual-cards image: registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps before_script: + - export PATH="$HOME/.cargo/bin:$PATH" - *report-rust script: - sh /start.sh - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin import -- $CONFIG - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin keygen -- $CONFIG variables: @@ -179,14 +182,15 @@ run_cardtest_smartpgp: key: "bookworm" run_cardtest_opcard_rs: - stage: virtual-test - image: registry.gitlab.com/openpgp-card/virtual-cards/opcard-rs-builddeps + stage: virtual-cards + image: registry.gitlab.com/openpgp-card/virtual-cards/opcard-rs-tools before_script: + - export PATH="$HOME/.cargo/bin:$PATH" - *report-rust script: - sh /start.sh - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin import -- $CONFIG - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin keygen -- $CONFIG variables: @@ -195,18 +199,46 @@ run_cardtest_opcard_rs: # inherit all general cache settings <<: *general_cache_config # override the key - # (the base image of run_cardtest uses bookworm) - key: "bookworm" + # (the base image is separate from the other virtual cards) + key: "opcard_rs" -run_cardtest_ykneo: - stage: virtual-test - image: registry.gitlab.com/openpgp-card/virtual-cards/ykneo-builddeps +run_cardtest_canokey: + stage: virtual-cards + image: registry.gitlab.com/openpgp-card/virtual-cards/canokey-builddeps before_script: + - export PATH="$HOME/.cargo/bin:$PATH" - *report-rust + script: + - sh /start.sh && sleep 10 + # - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status + # - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info + - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin import -- $CONFIG + - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin keygen -- $CONFIG + variables: + CONFIG: "card-functionality/ci/virt-canokey.toml" + cache: + # inherit all general cache settings + <<: *general_cache_config + # override the key + # (the base image of canokey-builddeps is distinct from the other virtual card images) + key: "canokey" + +run_cardtest_ykneo: + stage: virtual-cards + image: registry.gitlab.com/openpgp-card/virtual-cards/ykneo-builddeps + before_script: + - export PATH="$HOME/.cargo/bin:$PATH" + - *report-rust + - cargo update + - cargo update -p sequoia-openpgp --precise 1.15.0 + - cargo update -p buffered-reader --precise 1.1.5 + - cargo update -p regex --precise 1.9.6 + - cargo update -p lalrpop@0.20.0 --precise 0.19.12 # hack to work with Rust 1.63 + - cargo update -p petgraph --precise 0.6.3 # hack to work with Rust 1.63 script: - sh /start.sh - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin import -- $CONFIG - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin keygen -- $CONFIG variables: @@ -219,14 +251,21 @@ run_cardtest_ykneo: key: "bookworm" run_cardtest_fluffypgp: - stage: virtual-test + stage: virtual-cards image: registry.gitlab.com/openpgp-card/virtual-cards/fluffypgp-builddeps before_script: + - export PATH="$HOME/.cargo/bin:$PATH" - *report-rust + - cargo update + - cargo update -p sequoia-openpgp --precise 1.15.0 + - cargo update -p buffered-reader --precise 1.1.5 + - cargo update -p regex --precise 1.9.6 + - cargo update -p lalrpop@0.20.0 --precise 0.19.12 # hack to work with Rust 1.63 + - cargo update -p petgraph --precise 0.6.3 # hack to work with Rust 1.63 script: - sh /start.sh - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status - - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- status +# - RUST_BACKTRACE=1 cargo run -p openpgp-card-tools --bin opgpcard -- info - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin import -- $CONFIG - RUST_BACKTRACE=1 cargo run -p openpgp-card-tests --bin keygen -- $CONFIG variables: @@ -238,7 +277,8 @@ run_cardtest_fluffypgp: # (the base image of run_cardtest uses bookworm) key: "bookworm" -hardware-builddeps: +# disabled for now +.hardware-builddeps: stage: hw-builddeps needs: [ ] image: docker:stable @@ -291,15 +331,16 @@ hardware-builddeps: # so use a different key for clarity key: "cookiejar" -import: +# disabled for now +.import: extends: .hw-test-template stage: hw-import variables: ARG: import -keygen: +# disabled for now +.keygen: extends: .hw-test-template stage: hw-keygen - timeout: 2h variables: ARG: keygen diff --git a/.reuse/dep5 b/.reuse/dep5 index a762701..fda4287 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -10,7 +10,3 @@ License: CC0-1.0 Files: card-functionality/data/* Copyright: 2021 Heiko Schaefer License: CC0-1.0 - -Files: tools/debian/* -Copyright: 2022 Lars Wirzenius -License: CC0-1.0 diff --git a/Cargo.toml b/Cargo.toml index dd33793..02b48d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-License-Identifier: MIT OR Apache-2.0 [workspace] members = [ "openpgp-card", "openpgp-card-sequoia", + "card-backend", "pcsc", "scdc", "openpgp-card-examples", - "tools", "card-functionality", ] diff --git a/README.md b/README.md index 6cc2c99..10df6d3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ @@ -12,13 +12,15 @@ standard, in Rust. This project consists of the following library crates: - [openpgp-card](https://crates.io/crates/openpgp-card), which offers a - relatively low level OpenPGP card client API. + relatively low-level OpenPGP card client API. It is PGP implementation agnostic. -- [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc), - a backend to communicate with smartcards via +- [card-backend](https://crates.io/crates/card-backend), + a shared trait for Smart Card backends +- [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc), + a backend implementation to communicate with smartcards via [pcsc](https://pcsclite.apdu.fr/). -- [openpgp-card-scdc](https://crates.io/crates/openpgp-card-scdc), - a backend to communicate with smartcards via an +- [card-backend-scdc](https://crates.io/crates/card-backend-scdc), + a backend implementation to communicate with smartcards via an [scdaemon](https://www.gnupg.org/documentation/manuals/gnupg/Invoking-SCDAEMON.html#Invoking-SCDAEMON) instance. - [openpgp-card-sequoia](https://crates.io/crates/openpgp-card-sequoia), @@ -29,8 +31,10 @@ This is how the libraries relate to each other (and to applications): ```mermaid graph BT - OP["openpgp-card-pcsc
(pcsclite backend)"] --> OC - OS["openpgp-card-scdc
(scdaemon backend)"] --> OC["openpgp-card
(low level API)"] + CB["card-backend
(shared trait)"] --> OP + CB --> OS + OP["card-backend-pcsc
(pcsclite backend)"] --> OC + OS["card-backend-scdc
(scdaemon backend)"] --> OC["openpgp-card
(low level API)"] OC --> OCS["openpgp-card-sequoia
(high level Sequoia PGP-based API)"] OC -.-> U1[Applications based on low level API] OCS -.-> U2[Sequoia PGP-based applications] @@ -43,9 +47,9 @@ Additionally, there are the following non-library crates that are built on top of the libraries described above: - [openpgp-card-tools](https://crates.io/crates/openpgp-card-sequoia), - a CLI tool to inspect, manage and use OpenPGP cards, aimed at end users. + the `opgpcard` CLI tool to inspect, manage and use OpenPGP cards, aimed at end users. - [openpgp-card-tests](https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/card-functionality), - a test-suite that runs OpenPGP card operations on smartcards. + a test-suite that runs OpenPGP card operations on Smart Cards. - [openpgp-card-examples](https://gitlab.com/openpgp-card/openpgp-card/-/tree/main/card-examples), small example applications that demonstrate how you can use these libraries in your own projects to access OpenPGP card functionality. @@ -63,11 +67,11 @@ implementation. ### Backends -Typically, `openpgp-card` will be used with the `openpgp-card-pcsc` backend, -which uses the standard pcsclite library to communicate with cards. +Typically, `openpgp-card` will be used with the `card-backend-pcsc` backend, +which uses the standard pcsc-lite library to communicate with cards. However, alternative backends can be used and may be useful. -The experimental, alternative `openpgp-card-scdc` backend uses scdaemon from +The experimental, alternative `card-backend-scdc` backend uses scdaemon from the GnuPG project as a low-level transport layer to interact with OpenPGP cards. @@ -84,7 +88,7 @@ Backends implement: All higher level and/or OpenPGP card-specific logic (including command chaining) is handled in the `openpgp-card` layer. -### The **openpgp-card-sequoia** crate +### The openpgp-card-sequoia crate Offers a higher level interface, based around Sequoia PGP datastructures. @@ -100,8 +104,8 @@ library against OpenPGP cards. However, OpenPGP cards are, usually, physical devices that you plug into your computer, e.g. as USB sticks, or Smart cards (this is, of course, the usual point of these cards: they are independent devices, which are only loosely -coupled with your regular computing environment. However, for automated -testing, such as CI, this can be a complication.) +coupled with your regular computing environment). +For automated testing, such as CI, this is a complication. There are at least two approaches for running tests against software-based OpenPGP cards: diff --git a/card-backend/Cargo.toml b/card-backend/Cargo.toml new file mode 100644 index 0000000..4afb3f5 --- /dev/null +++ b/card-backend/Cargo.toml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Heiko Schaefer +# SPDX-License-Identifier: MIT OR Apache-2.0 + +[package] +name = "card-backend" +description = "Card backend trait, for use with the openpgp-card crate" +authors = ["Heiko Schaefer "] +license = "MIT OR Apache-2.0" +version = "0.2.0" +edition = "2018" +repository = "https://gitlab.com/openpgp-card/openpgp-card" +documentation = "https://docs.rs/crate/card-backend" + +[dependencies] +thiserror = "1" \ No newline at end of file diff --git a/card-backend/README.md b/card-backend/README.md new file mode 100644 index 0000000..69a9591 --- /dev/null +++ b/card-backend/README.md @@ -0,0 +1,12 @@ + + +# Backend trait for Smart Card crates + +This crate defines the `CardBackend` and `CardTransactions` traits. + +The initial target for this abstraction layer was the +[openpgp-card](https://gitlab.com/openpgp-card/openpgp-card) set of client libraries +for OpenPGP card. This trait offers an implementation-agnostic means to access cards. diff --git a/card-backend/src/lib.rs b/card-backend/src/lib.rs new file mode 100644 index 0000000..ef534c4 --- /dev/null +++ b/card-backend/src/lib.rs @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! A thin abstraction layer for accessing smart cards, including, but not +//! limited to, [openpgp-card](https://gitlab.com/openpgp-card/openpgp-card) +//! devices. + +/// This trait defines a connection with a smart card via a +/// backend implementation (e.g. via the pcsc backend in the crate +/// [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)). +/// +/// A [CardBackend] is only used to get access to a [CardTransaction] object, +/// which supports transmitting commands to the card. +pub trait CardBackend { + /// If a CardBackend introduces a additional (possibly backend-specific) + /// limits for any fields in CardCaps, this fn can indicate that limit by + /// returning an amended [`CardCaps`]. + fn limit_card_caps(&self, card_caps: CardCaps) -> CardCaps; + + fn transaction( + &mut self, + reselect_application: Option<&[u8]>, + ) -> Result, SmartcardError>; +} + +/// The CardTransaction trait defines communication with a smart card via a +/// backend implementation (e.g. the pcsc backend in the crate +/// [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc)), +/// after opening a transaction from a CardBackend. +pub trait CardTransaction { + /// Transmit the command data in `cmd` to the card. + /// + /// `buf_size` is a hint to the backend (the backend may ignore it) + /// indicating the expected maximum response size. + fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result, SmartcardError>; + + /// Select `application` on the card + fn select(&mut self, application: &[u8]) -> Result, SmartcardError> { + let mut cmd = vec![0x00, 0xa4, 0x04, 0x00]; // CLA, INS, P1, P2 + cmd.push(application.len() as u8); // Lc + cmd.extend_from_slice(application); // Data + cmd.push(0x00); // Le + + self.transmit(&cmd, 254) + } + + /// If a CardTransaction implementation introduces an additional, + /// backend-specific limit for maximum number of bytes per command, + /// this fn can indicate that limit by returning `Some(max_cmd_len)`. + fn max_cmd_len(&self) -> Option { + None + } + + /// Does the reader support FEATURE_VERIFY_PIN_DIRECT? + fn feature_pinpad_verify(&self) -> bool; + + /// Does the reader support FEATURE_MODIFY_PIN_DIRECT? + fn feature_pinpad_modify(&self) -> bool; + + /// Verify the PIN `pin` via the reader pinpad + fn pinpad_verify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, SmartcardError>; + + /// Modify the PIN `pin` via the reader pinpad + fn pinpad_modify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, SmartcardError>; + + /// Has a reset been detected while starting this transaction? + /// + /// (Backends may choose to always return false) + fn was_reset(&self) -> bool; +} + +/// Information about the capabilities of a card. +/// +/// CardCaps is used to signal capabilities (chaining, extended length support, max +/// command/response sizes, max PIN lengths) of the current card to backends. +/// +/// CardCaps is not intended for users of this library. +/// +/// (The information is gathered from the "Card Capabilities", "Extended length information" and +/// "PWStatus" DOs) +#[derive(Clone, Copy, Debug)] +pub struct CardCaps { + ext_support: bool, + chaining_support: bool, + max_cmd_bytes: u16, + max_rsp_bytes: u16, + pw1_max_len: u8, + pw3_max_len: u8, +} + +impl CardCaps { + pub fn new( + ext_support: bool, + chaining_support: bool, + max_cmd_bytes: u16, + max_rsp_bytes: u16, + pw1_max_len: u8, + pw3_max_len: u8, + ) -> Self { + Self { + ext_support, + chaining_support, + max_cmd_bytes, + max_rsp_bytes, + pw1_max_len, + pw3_max_len, + } + } + + /// Does the card support extended Lc and Le fields? + pub fn ext_support(&self) -> bool { + self.ext_support + } + + /// Does the card support command chaining? + pub fn chaining_support(&self) -> bool { + self.chaining_support + } + + /// Maximum number of bytes in a command APDU + pub fn max_cmd_bytes(&self) -> u16 { + self.max_cmd_bytes + } + + /// Maximum number of bytes in a response APDU + pub fn max_rsp_bytes(&self) -> u16 { + self.max_rsp_bytes + } + + /// Maximum length of PW1 + pub fn pw1_max_len(&self) -> u8 { + self.pw1_max_len + } + + /// Maximum length of PW3 + pub fn pw3_max_len(&self) -> u8 { + self.pw3_max_len + } +} + +/// Specify a PIN to *verify* (distinguishes between `Sign`, `User` and `Admin`). +/// +/// (Note that for PIN *management*, in particular changing a PIN, "signing and user" are +/// not distinguished. They always share the same PIN value `PW1`) +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum PinType { + /// Verify PW1 in mode P2=81 (for the PSO:CDS operation) + Sign, + + /// Verify PW1 in mode P2=82 (for all other User operations) + User, + + /// Verify PW3 (for Admin operations) + Admin, +} + +impl PinType { + pub fn id(&self) -> u8 { + match self { + PinType::Sign => 0x81, + PinType::User => 0x82, + PinType::Admin => 0x83, + } + } +} + +/// Errors on the smartcard/reader layer +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum SmartcardError { + #[error("Failed to create a pcsc smartcard context {0}")] + ContextError(String), + + #[error("Failed to list readers: {0}")] + ReaderError(String), + + #[error("No reader found.")] + NoReaderFoundError, + + #[error("The requested card '{0}' was not found.")] + CardNotFound(String), + + #[error("Failed to connect to the card: {0}")] + SmartCardConnectionError(String), + + #[error("Smart card status: [{0}, {1}]")] + SmartCardStatus(u8, u8), + + #[error("NotTransacted (SCARD_E_NOT_TRANSACTED)")] + NotTransacted, + + #[error("Generic SmartCard Error: {0}")] + Error(String), +} diff --git a/card-functionality/Cargo.toml b/card-functionality/Cargo.toml index c26a162..7279371 100644 --- a/card-functionality/Cargo.toml +++ b/card-functionality/Cargo.toml @@ -31,8 +31,8 @@ path = "src/list-cards.rs" [dependencies] openpgp-card = { path = "../openpgp-card" } openpgp-card-sequoia = { path = "../openpgp-card-sequoia" } -openpgp-card-scdc = { path = "../scdc" } -openpgp-card-pcsc = { path = "../pcsc" } +card-backend-scdc = { path = "../scdc" } +card-backend-pcsc = { path = "../pcsc" } pcsc = "2.7" sequoia-openpgp = "1.3" anyhow = "1" diff --git a/card-functionality/ci/virt-canokey.toml b/card-functionality/ci/virt-canokey.toml new file mode 100644 index 0000000..2d26780 --- /dev/null +++ b/card-functionality/ci/virt-canokey.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-License-Identifier: CC0-1.0 + +[card.smartpgp] +backend.pcsc = "F1D0:00000000" +config.keygen = [ + "RSA2k", "RSA3k", "RSA4k", + "NIST256", "NIST384", "Curve25519" +] +config.import = [ + "card-functionality/data/rsa2k.sec", + "card-functionality/data/rsa3k.sec", + "card-functionality/data/rsa4k.sec", + "card-functionality/data/nist256.sec", + "card-functionality/data/nist384.sec", + "card-functionality/data/25519.sec", +] diff --git a/card-functionality/ci/virt-opcard-rs.toml b/card-functionality/ci/virt-opcard-rs.toml index bba5f72..5e12c0c 100644 --- a/card-functionality/ci/virt-opcard-rs.toml +++ b/card-functionality/ci/virt-opcard-rs.toml @@ -4,11 +4,12 @@ [card.opcard-rs] backend.pcsc = "0000:00000000" config.keygen = [ - "RSA2k", "RSA4k", + "RSA2k", "RSA3k", "RSA4k", "NIST256", "Curve25519" ] config.import = [ "card-functionality/data/rsa2k.sec", + "card-functionality/data/rsa3k.sec", "card-functionality/data/rsa4k.sec", "card-functionality/data/nist256.sec", "card-functionality/data/25519.sec", diff --git a/card-functionality/src/cards.rs b/card-functionality/src/cards.rs index ea551df..d4d22b8 100644 --- a/card-functionality/src/cards.rs +++ b/card-functionality/src/cards.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! Wrapping of cards for tests. Open a list of cards, based on a @@ -7,13 +7,13 @@ use std::collections::BTreeMap; use anyhow::Result; -use openpgp_card::{CardBackend, Error}; -use openpgp_card_pcsc::PcscBackend; -use openpgp_card_scdc::ScdBackend; -use pcsc::ShareMode; +use card_backend_pcsc::PcscBackend; +use card_backend_scdc::ScdBackend; +use openpgp_card::Error; +use openpgp_card_sequoia::state::Open; use serde_derive::Deserialize; -const SHARE_MODE: Option = Some(ShareMode::Shared); +// const SHARE_MODE: Option = Some(ShareMode::Shared); #[derive(Debug, Deserialize)] pub struct TestConfig { @@ -41,7 +41,7 @@ pub struct TestCardData { } impl TestCardData { - pub(crate) fn get_card(&self) -> Result> { + pub fn get_card(&self) -> Result> { self.tc.open() } @@ -92,7 +92,7 @@ pub enum TestCard { } impl TestCard { - pub fn open(&self) -> Result> { + pub fn open(&self) -> Result> { match self { Self::Pcsc(ident) => { // Attempt to shutdown SCD, if it is running. @@ -103,24 +103,39 @@ impl TestCard { // Make three attempts to open the card before failing // (this can be useful in ShareMode::Exclusive) let mut i = 1; - let card: Result, Error> = loop { - let res = PcscBackend::open_by_ident(ident, SHARE_MODE); + let card: Result, Error> = loop { + i += 1; - if i == 3 { - if let Ok(res) = res { - break Ok(Box::new(res)); - } + let cards = PcscBackend::card_backends(None)?; + let res = openpgp_card_sequoia::Card::::open_by_ident(cards, ident); + + println!("Got result for card: {}", ident); + + if let Err(e) = &res { + println!("Result is an error: {:x?}", e); + } else { + println!("Result is a happy card"); + } + + if let Ok(res) = res { + break Ok(res); + } + + if i > 3 { + break Err(Error::NotFound(format!("Couldn't open card {}", ident))); } // sleep for 100ms + println!("Will sleep for 100ms"); std::thread::sleep(std::time::Duration::from_millis(100)); - - i += 1; }; Ok(card?) } - Self::Scdc(serial) => Ok(Box::new(ScdBackend::open_by_serial(None, serial)?)), + Self::Scdc(serial) => { + let backend = ScdBackend::open_by_serial(None, serial)?; + Ok(openpgp_card_sequoia::Card::::new(backend)?) + } } } } diff --git a/card-functionality/src/import.rs b/card-functionality/src/import.rs index da2346e..07de36f 100644 --- a/card-functionality/src/import.rs +++ b/card-functionality/src/import.rs @@ -7,6 +7,8 @@ use anyhow::Result; use card_functionality::cards::TestConfig; use card_functionality::tests::*; use card_functionality::util; +use openpgp_card_sequoia::state::Open; +use openpgp_card_sequoia::Card; use sequoia_openpgp::Cert; fn main() -> Result<()> { @@ -21,16 +23,25 @@ fn main() -> Result<()> { let cards = config.into_cardapps(); - for mut card in cards { + for card in cards { println!("** Run tests on card '{}' **", card.get_name()); + let mut c: Card = card.get_card()?; + println!(" -> Card opened"); + let mut tx = c.transaction()?; + println!(" started transaction"); + println!("Reset"); - let _ = run_test(&mut card, test_reset, &[])?; + let _ = run_test(&mut tx, test_reset, &[])?; print!("Set user data"); - let userdata_out = run_test(&mut card, test_set_user_data, &[])?; + let userdata_out = run_test(&mut tx, test_set_user_data, &[])?; println!(" {userdata_out:x?}"); + println!("Set login data"); + let login_data_out = run_test(&mut tx, test_set_login_data, &[])?; + println!(" {login_data_out:x?}"); + let key_files = { let config = card.get_config(); if let Some(import) = &config.import { @@ -43,7 +54,7 @@ fn main() -> Result<()> { for key_file in &key_files { // upload keys print!("Upload key '{key_file}'"); - let upload_res = run_test(&mut card, test_upload_keys, &[key_file]); + let upload_res = run_test(&mut tx, test_upload_keys, &[key_file]); if let Err(TestError::KeyUploadError(_file, err)) = &upload_res { // The card doesn't support this key type, so skip to the @@ -62,16 +73,16 @@ fn main() -> Result<()> { // decrypt print!(" Decrypt"); - let c = Cert::from_str(&key)?; - let ciphertext = util::encrypt_to("Hello world!\n", &c)?; + let cert = Cert::from_str(&key)?; + let ciphertext = util::encrypt_to("Hello world!\n", &cert)?; - let dec_out = run_test(&mut card, test_decrypt, &[&key, &ciphertext])?; + let dec_out = run_test(&mut tx, test_decrypt, &[&key, &ciphertext])?; println!(" {dec_out:x?}"); // sign print!(" Sign"); - let sign_out = run_test(&mut card, test_sign, &[&key])?; + let sign_out = run_test(&mut tx, test_sign, &[&key])?; println!(" {sign_out:x?}"); } diff --git a/card-functionality/src/keygen.rs b/card-functionality/src/keygen.rs index 2cce5b5..7684c4e 100644 --- a/card-functionality/src/keygen.rs +++ b/card-functionality/src/keygen.rs @@ -7,6 +7,8 @@ use anyhow::Result; use card_functionality::cards::TestConfig; use card_functionality::tests::*; use card_functionality::util; +use openpgp_card_sequoia::state::Open; +use openpgp_card_sequoia::Card; use sequoia_openpgp::Cert; fn main() -> Result<()> { @@ -21,9 +23,14 @@ fn main() -> Result<()> { let cards = config.into_cardapps(); - for mut card in cards { + for card in cards { println!("** Run tests on card {} **", card.get_name()); + let mut c: Card = card.get_card()?; + println!(" -> Card opened"); + let mut tx = c.transaction()?; + println!(" started transaction"); + // println!("Get pubkey"); // let _ = run_test(&mut card, test_get_pub, &[])?; // @@ -34,14 +41,14 @@ fn main() -> Result<()> { // // continue; // only print caps println!("Reset"); - let _ = run_test(&mut card, test_reset, &[])?; + let _ = run_test(&mut tx, test_reset, &[])?; // println!("Algo info"); // let _ = run_test(&mut card, test_print_algo_info, &[])?; // Set user data because keygen expects a name (for the user id) println!("Set user data"); - let _ = run_test(&mut card, test_set_user_data, &[])?; + let _ = run_test(&mut tx, test_set_user_data, &[])?; let algos = { let config = card.get_config(); @@ -55,20 +62,20 @@ fn main() -> Result<()> { for algo in algos { println!("Generate key [{algo}]"); - let res = run_test(&mut card, test_keygen, &[&algo])?; + let res = run_test(&mut tx, test_keygen, &[&algo])?; - if let TestResult::Text(cert) = &res[0] { + if let TestResult::Text(cert_str) = &res[0] { // sign print!(" Sign"); - let sign_out = run_test(&mut card, test_sign, &[cert])?; + let sign_out = run_test(&mut tx, test_sign, &[cert_str])?; println!(" {sign_out:x?}"); // decrypt - let c = Cert::from_str(cert)?; - let ciphertext = util::encrypt_to("Hello world!\n", &c)?; + let cert = Cert::from_str(cert_str)?; + let ciphertext = util::encrypt_to("Hello world!\n", &cert)?; print!(" Decrypt"); - let dec_out = run_test(&mut card, test_decrypt, &[cert, &ciphertext])?; + let dec_out = run_test(&mut tx, test_decrypt, &[cert_str, &ciphertext])?; println!(" {dec_out:x?}"); } else { panic!("Didn't get back a Cert from test_keygen"); diff --git a/card-functionality/src/list-cards.rs b/card-functionality/src/list-cards.rs index c96a72a..94e1e14 100644 --- a/card-functionality/src/list-cards.rs +++ b/card-functionality/src/list-cards.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use anyhow::Result; -use openpgp_card_pcsc::PcscBackend; +use card_backend_pcsc::PcscBackend; use openpgp_card_sequoia::{state::Open, Card}; fn main() -> Result<()> { println!("The following OpenPGP cards are connected to your system:"); for backend in PcscBackend::cards(None)? { - let mut card: Card = backend.into(); + let mut card: Card = Card::::new(backend?)?; + println!(" {}", card.transaction()?.application_identifier()?.ident()); } diff --git a/card-functionality/src/other.rs b/card-functionality/src/other.rs index b061b93..4672a28 100644 --- a/card-functionality/src/other.rs +++ b/card-functionality/src/other.rs @@ -4,6 +4,8 @@ use anyhow::Result; use card_functionality::cards::TestConfig; use card_functionality::tests::*; +use openpgp_card_sequoia::state::Open; +use openpgp_card_sequoia::Card; fn main() -> Result<()> { env_logger::init(); @@ -12,9 +14,12 @@ fn main() -> Result<()> { let cards = config.into_cardapps(); - for mut card in cards { + for card in cards { println!("** Run tests on card '{}' **", card.get_name()); + let mut c: Card = card.get_card()?; + let mut tx = c.transaction()?; + // println!("Caps"); // let _ = run_test(&mut card, test_print_caps, &[])?; // continue; // only print caps @@ -23,7 +28,7 @@ fn main() -> Result<()> { // let _ = run_test(&mut card, test_print_algo_info, &[])?; println!("Reset"); - let _ = run_test(&mut card, test_reset, &[])?; + let _ = run_test(&mut tx, test_reset, &[])?; // --- diff --git a/card-functionality/src/tests.rs b/card-functionality/src/tests.rs index ca9f53b..2eba414 100644 --- a/card-functionality/src/tests.rs +++ b/card-functionality/src/tests.rs @@ -8,20 +8,19 @@ use std::string::FromUtf8Error; use anyhow::Result; use openpgp_card::algorithm::AlgoSimple; use openpgp_card::card_do::{KeyGenerationTime, Sex}; -use openpgp_card::{Error, KeyType, OpenPgp, OpenPgpTransaction, StatusBytes}; +use openpgp_card::{Error, KeyType, StatusBytes}; use openpgp_card_sequoia::sq_util; +use openpgp_card_sequoia::state::{Admin, Open, Transaction}; use openpgp_card_sequoia::util::{ make_cert, public_key_material_and_fp_to_key, public_key_material_to_key, }; -use openpgp_card_sequoia::{state::Transaction, Card}; +use openpgp_card_sequoia::Card; use sequoia_openpgp::parse::Parse; use sequoia_openpgp::policy::StandardPolicy; use sequoia_openpgp::serialize::SerializeInto; -use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; use sequoia_openpgp::Cert; use thiserror; -use crate::cards::TestCardData; use crate::util; #[derive(Debug)] @@ -52,9 +51,7 @@ pub enum TestError { } /// Run after each "upload keys", if key *was* uploaded (?) -pub fn test_decrypt(pgp: &mut OpenPgp, param: &[&str]) -> Result { - let mut pgpt = pgp.transaction()?; - +pub fn test_decrypt(tx: &mut Card, param: &[&str]) -> Result { assert_eq!( param.len(), 2, @@ -63,13 +60,9 @@ pub fn test_decrypt(pgp: &mut OpenPgp, param: &[&str]) -> Result::new(pgpt)?; - - let mut user = transaction.user_card().unwrap(); + let mut user = tx.to_user_card("123456")?; let d = user.decryptor(&|| {})?; let res = sq_util::decrypt(d, msg.into_bytes(), &p)?; @@ -81,18 +74,12 @@ pub fn test_decrypt(pgp: &mut OpenPgp, param: &[&str]) -> Result Result { - let mut pgpt = pgp.transaction()?; - +pub fn test_sign(tx: &mut Card, param: &[&str]) -> Result { assert_eq!(param.len(), 1, "test_sign needs a filename for 'cert'"); - pgpt.verify_pw1_sign(b"123456")?; - let cert = Cert::from_str(param[0])?; - let mut transaction = Card::::new(pgpt)?; - - let mut sign = transaction.signing_card().unwrap(); + let mut sign = tx.to_signing_card("123456").unwrap(); let s = sign.signer(&|| {})?; let msg = "Hello world, I am signed."; @@ -105,13 +92,13 @@ pub fn test_sign(pgp: &mut OpenPgp, param: &[&str]) -> Result, meta: &[(String, KeyGenerationTime)], ) -> Result<()> { - let ard = pgpt.application_related_data()?; + admin.as_transaction().reload_ard()?; // check fingerprints - let card_fp = ard.fingerprints()?; + let card_fp = admin.as_transaction().fingerprints()?; let sig = card_fp.signature().expect("signature fingerprint"); assert_eq!(format!("{sig:X}"), meta[0].0); @@ -125,7 +112,7 @@ fn check_key_upload_metadata( assert_eq!(format!("{auth:X}"), meta[2].0); // get_key_generation_times - let card_kg = ard.key_generation_times()?; + let card_kg = admin.as_transaction().key_generation_times()?; let sig = card_kg.signature().expect("signature creation time"); assert_eq!(sig, &meta[0].1); @@ -148,7 +135,10 @@ fn check_key_upload_algo_attrs() -> Result<()> { Ok(()) } -pub fn test_print_caps(pgp: &mut OpenPgp, _param: &[&str]) -> Result { +pub fn test_print_caps( + pgp: &mut openpgp_card::Card, + _param: &[&str], +) -> Result { let mut pgpt = pgp.transaction()?; let ard = pgpt.application_related_data()?; @@ -168,7 +158,10 @@ pub fn test_print_caps(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result { +pub fn test_print_algo_info( + pgp: &mut openpgp_card::Card, + _param: &[&str], +) -> Result { let mut pgpt = pgp.transaction()?; let ard = pgpt.application_related_data()?; @@ -186,25 +179,25 @@ pub fn test_print_algo_info(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result { - let mut pgpt = pgp.transaction()?; - +pub fn test_upload_keys( + tx: &mut Card, + param: &[&str], +) -> Result { assert_eq!( param.len(), 1, "test_upload_keys needs a filename for 'cert'" ); - pgpt.verify_pw3(b"12345678")?; - let cert = Cert::from_file(param[0])?; - let p = StandardPolicy::new(); - let meta = util::upload_subkeys(&mut pgpt, &cert, &p) + let mut admin = tx.to_admin_card("12345678")?; + + let meta = util::upload_subkeys(&mut admin, &cert, &p) .map_err(|e| TestError::KeyUploadError(param[0].to_string(), e))?; - check_key_upload_metadata(&mut pgpt, &meta)?; + check_key_upload_metadata(&mut admin, &meta)?; // FIXME: implement check_key_upload_algo_attrs()?; @@ -213,12 +206,10 @@ pub fn test_upload_keys(pgp: &mut OpenPgp, param: &[&str]) -> Result Result { - let pgpt = pgp.transaction()?; - - let mut transaction = Card::::new(pgpt)?; - transaction.verify_admin(b"12345678")?; - let mut admin = transaction.admin_card().expect("Couldn't get Admin card"); +pub fn test_keygen(tx: &mut Card, param: &[&str]) -> Result { + let mut admin = tx + .to_admin_card("12345678") + .expect("Couldn't get Admin card"); // Generate all three subkeys on card let algo = param[0]; @@ -226,30 +217,29 @@ pub fn test_keygen(pgp: &mut OpenPgp, param: &[&str]) -> Result Result Result { - let mut pgpt = pgp.transaction()?; +pub fn test_get_pub(mut card: Card, _param: &[&str]) -> Result { + let mut transaction = card.transaction()?; - let ard = pgpt.application_related_data()?; - let times = ard.key_generation_times()?; - let fps = ard.fingerprints()?; + let times = transaction.key_generation_times()?; + let fps = transaction.fingerprints()?; // -- - let sig = pgpt.public_key(KeyType::Signing)?; + let sig = transaction.public_key_material(KeyType::Signing)?; let ts = times.signature().unwrap().get().into(); let key = public_key_material_and_fp_to_key(&sig, KeyType::Signing, &ts, fps.signature().unwrap())?; @@ -280,7 +269,7 @@ pub fn test_get_pub(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result Result Result { - let mut pgpt = pgp.transaction()?; - - pgpt.factory_reset()?; +pub fn test_reset(tx: &mut Card, _param: &[&str]) -> Result { + tx.factory_reset()?; Ok(vec![]) } @@ -319,25 +306,29 @@ pub fn test_reset(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result { - let mut pgpt = pgp.transaction()?; - - pgpt.verify_pw3(b"12345678")?; +pub fn test_set_user_data( + tx: &mut Card, + _param: &[&str], +) -> Result { + let mut admin = tx.to_admin_card("12345678")?; // name - pgpt.set_name(b"Bar< Result Result { - let mut pgpt = pgp.transaction()?; +pub fn test_set_login_data( + tx: &mut Card, + _params: &[&str], +) -> std::result::Result { + let mut admin = tx.to_admin_card("12345678")?; - let out = vec![]; + let test_login = "someone@somewhere.com"; + admin.set_login_data(test_login.as_bytes())?; - println!(); + // Read the previously set login data + let read_login_data = tx.login_data()?; - let d = pgpt.private_use_do(1)?; - println!("data 1 {d:?}"); + assert_eq!(&read_login_data, test_login.as_bytes()); - pgpt.verify_pw1_user(b"123456")?; - - pgpt.set_private_use_do(1, "Foo bar1!".as_bytes().to_vec())?; - pgpt.set_private_use_do(3, "Foo bar3!".as_bytes().to_vec())?; - - pgpt.verify_pw3(b"12345678")?; - - pgpt.set_private_use_do(2, "Foo bar2!".as_bytes().to_vec())?; - pgpt.set_private_use_do(4, "Foo bar4!".as_bytes().to_vec())?; - - let d = pgpt.private_use_do(1)?; - println!("data 1 {d:?}"); - let d = pgpt.private_use_do(2)?; - println!("data 2 {d:?}"); - let d = pgpt.private_use_do(3)?; - println!("data 3 {d:?}"); - let d = pgpt.private_use_do(4)?; - println!("data 4 {d:?}"); - - Ok(out) + Ok(vec![]) } +// pub fn test_private_data(mut card: Card, _param: &[&str]) -> Result { +// let mut transaction = card.transaction()?; +// +// let out = vec![]; +// +// println!(); +// +// let d = transaction.private_use_do(1)?; +// println!("data 1 {d:?}"); +// +// transaction.verify_pw1_user("123456")?; +// +// transaction.set_private_use_do(1, "Foo bar1!".as_bytes().to_vec())?; +// transaction.set_private_use_do(3, "Foo bar3!".as_bytes().to_vec())?; +// +// transaction.verify_pw3("12345678")?; +// +// transaction.set_private_use_do(2, "Foo bar2!".as_bytes().to_vec())?; +// transaction.set_private_use_do(4, "Foo bar4!".as_bytes().to_vec())?; +// +// let d = transaction.private_use_do(1)?; +// println!("data 1 {d:?}"); +// let d = transaction.private_use_do(2)?; +// println!("data 2 {d:?}"); +// let d = transaction.private_use_do(3)?; +// println!("data 3 {d:?}"); +// let d = transaction.private_use_do(4)?; +// println!("data 4 {d:?}"); +// +// Ok(out) +// } + // pub fn test_cardholder_cert( // card_tx: &mut CardApp, // _param: &[&str], @@ -444,25 +452,25 @@ pub fn test_private_data(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result { - let mut pgpt = pgp.transaction()?; +pub fn test_pw_status(mut card: Card, _param: &[&str]) -> Result { + let mut transaction = card.transaction()?; let out = vec![]; - let ard = pgpt.application_related_data()?; - let mut pws = ard.pw_status_bytes()?; + let mut pws = transaction.pw_status_bytes()?; println!("pws {pws:?}"); - pgpt.verify_pw3(b"12345678")?; + let mut admin = transaction.to_admin_card("12345678")?; pws.set_pw1_cds_valid_once(false); pws.set_pw1_pin_block(true); - pgpt.set_pw_status_bytes(&pws, false)?; + admin.set_pw_status_bytes(&pws, false)?; - let ard = pgpt.application_related_data()?; - let pws = ard.pw_status_bytes()?; + transaction.reload_ard()?; + + let pws = transaction.pw_status_bytes()?; println!("pws {pws:?}"); Ok(out) @@ -471,8 +479,8 @@ pub fn test_pw_status(pgp: &mut OpenPgp, _param: &[&str]) -> Result Status /// - verify pw1 (check) -> Status -pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result { - let mut pgpt = pgp.transaction()?; +pub fn test_verify(mut card: Card, _param: &[&str]) -> Result { + let mut transaction = card.transaction()?; // Steps: // @@ -489,7 +497,8 @@ pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result Result { // e.g. yubikey5 returns an error status! out.push(TestResult::Status(s)); @@ -510,14 +519,18 @@ pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result out.push(TestResult::StatusOk), } - pgpt.set_name(b"Admin< { // e.g. yubikey5 returns an error status! out.push(TestResult::Status(s)); @@ -528,36 +541,39 @@ pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result out.push(TestResult::StatusOk), } - pgpt.set_name(b"There< Result { - let mut pgpt = pgp.transaction()?; +pub fn test_change_pw(mut card: Card, _param: &[&str]) -> Result { + let mut transaction = card.transaction()?; let out = vec![]; // first do admin-less pw1 on gnuk // (NOTE: Gnuk requires a key to be loaded before allowing pw changes!) println!("change pw1"); - pgpt.change_pw1(b"123456", b"abcdef00")?; + transaction.change_user_pin("123456", "abcdef00")?; // also set admin pw, which means pw1 is now only user-pw again, on gnuk println!("change pw3"); // ca.change_pw3("abcdef00", "abcdefgh")?; // gnuk - pgpt.change_pw3(b"12345678", b"abcdefgh")?; + transaction.change_admin_pin("12345678", "abcdefgh")?; println!("change pw1"); - pgpt.change_pw1(b"abcdef00", b"abcdef")?; // gnuk + transaction.change_user_pin("abcdef00", "abcdef")?; // gnuk // ca.change_pw1("123456", "abcdef")?; println!("verify bad pw1"); - match pgpt.verify_pw1_user(b"123456ab") { + match transaction.verify_user_pin("123456ab") { Err(Error::CardStatus(StatusBytes::SecurityStatusNotSatisfied)) => { // this is expected } @@ -568,10 +584,10 @@ pub fn test_change_pw(pgp: &mut OpenPgp, _param: &[&str]) -> Result { // this is expected } @@ -582,36 +598,36 @@ pub fn test_change_pw(pgp: &mut OpenPgp, _param: &[&str]) -> Result, _param: &[&str], ) -> Result { - let mut pgpt = pgp.transaction()?; + let mut transaction = card.transaction()?; let out = vec![]; // set pw3, then pw1 (to bring gnuk into non-admin mode) println!("set pw3"); - pgpt.change_pw3(b"12345678", b"12345678")?; + transaction.change_admin_pin("12345678", "12345678")?; println!("set pw1"); - pgpt.change_pw1(b"123456", b"123456")?; + transaction.change_user_pin("123456", "123456")?; println!("break pw1"); - let _ = pgpt.verify_pw1_user(b"wrong0"); - let _ = pgpt.verify_pw1_user(b"wrong0"); - let _ = pgpt.verify_pw1_user(b"wrong0"); - let res = pgpt.verify_pw1_user(b"wrong0"); + let _ = transaction.verify_user_pin("wrong0"); + let _ = transaction.verify_user_pin("wrong0"); + let _ = transaction.verify_user_pin("wrong0"); + let res = transaction.verify_user_pin("wrong0"); match res { Err(Error::CardStatus(StatusBytes::AuthenticationMethodBlocked)) => { @@ -630,20 +646,21 @@ pub fn test_reset_retry_counter( } println!("verify pw3"); - pgpt.verify_pw3(b"12345678")?; + transaction.verify_admin_pin("12345678")?; println!("set resetting code"); - pgpt.set_resetting_code(b"abcdefgh")?; + let mut admin = transaction.to_admin_card(None)?; + admin.set_resetting_code("abcdefgh")?; println!("reset retry counter"); // ca.reset_retry_counter_pw1("abcdef".as_bytes().to_vec(), None)?; - let _res = pgpt.reset_retry_counter_pw1(b"abcdef", Some(b"abcdefgh")); + let _res = transaction.reset_user_pin("abcdef", "abcdefgh"); println!("verify good pw1"); - pgpt.verify_pw1_user(b"abcdef")?; + transaction.verify_user_pin("abcdef")?; println!("verify bad pw1"); - match pgpt.verify_pw1_user(b"00000000") { + match transaction.verify_user_pin("00000000") { Err(Error::CardStatus(StatusBytes::SecurityStatusNotSatisfied)) => { // this is expected } @@ -657,11 +674,9 @@ pub fn test_reset_retry_counter( } pub fn run_test( - tc: &mut TestCardData, - t: fn(&mut OpenPgp, &[&str]) -> Result, + card: &mut Card, + t: fn(&mut Card, &[&str]) -> Result, param: &[&str], ) -> Result { - let card = tc.get_card()?; - let mut pgp = OpenPgp::new(card); - t(&mut pgp, param) + t(card, param) } diff --git a/card-functionality/src/util.rs b/card-functionality/src/util.rs index c25a4a6..9b4d476 100644 --- a/card-functionality/src/util.rs +++ b/card-functionality/src/util.rs @@ -6,21 +6,22 @@ use std::time::SystemTime; use anyhow::Result; use openpgp_card::card_do::KeyGenerationTime; -use openpgp_card::{KeyType, OpenPgpTransaction}; -use openpgp_card_sequoia::sq_util; -use openpgp_card_sequoia::util::vka_as_uploadable_key; +use openpgp_card::KeyType; +use openpgp_card_sequoia::state::Admin; +use openpgp_card_sequoia::{sq_util, Card}; use sequoia_openpgp::parse::stream::{ DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper, }; use sequoia_openpgp::parse::Parse; use sequoia_openpgp::policy::{Policy, StandardPolicy}; +#[allow(deprecated)] use sequoia_openpgp::serialize::stream::{Armorer, Encryptor, LiteralWriter, Message}; use sequoia_openpgp::Cert; pub const SP: &StandardPolicy = &StandardPolicy::new(); pub(crate) fn upload_subkeys( - pgpt: &mut OpenPgpTransaction, + admin: &mut Card, cert: &Cert, policy: &dyn Policy, ) -> Result> { @@ -44,8 +45,7 @@ pub(crate) fn upload_subkeys( out.push((fp, creation.into())); // upload key - let cuk = vka_as_uploadable_key(vka, None); - pgpt.key_import(cuk, *kt)?; + admin.upload_key(vka, *kt, None)?; } } @@ -125,6 +125,7 @@ pub fn encrypt_to(cleartext: &str, cert: &Cert) -> Result { let message = Message::new(&mut sink); let message = Armorer::new(message).build()?; + #[allow(deprecated)] let message = Encryptor::for_recipients(message, recipients).build()?; let mut w = LiteralWriter::new(message).build()?; w.write_all(cleartext.as_bytes())?; diff --git a/deny.toml b/deny.toml index 3e7ccc7..2ff949c 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,7 @@ notice = "warn" ignore = [ # Ignore time issue for now as there is no solution "RUSTSEC-2020-0071", + "RUSTSEC-2023-0071", ] [licenses] unlicensed = "deny" diff --git a/openpgp-card-examples/Cargo.toml b/openpgp-card-examples/Cargo.toml index 5588278..aa3fc1e 100644 --- a/openpgp-card-examples/Cargo.toml +++ b/openpgp-card-examples/Cargo.toml @@ -14,7 +14,7 @@ documentation = "https://docs.rs/crate/openpgp-card-examples" [dependencies] sequoia-openpgp = "1.3" nettle = "7" -openpgp-card-pcsc = { path = "../pcsc" } +card-backend-pcsc = { path = "../pcsc" } openpgp-card-sequoia = { path = "../openpgp-card-sequoia" } chrono = "0.4" anyhow = "1" diff --git a/openpgp-card-examples/src/bin/decrypt.rs b/openpgp-card-examples/src/bin/decrypt.rs index 5119dfc..7e5c174 100644 --- a/openpgp-card-examples/src/bin/decrypt.rs +++ b/openpgp-card-examples/src/bin/decrypt.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 Wiktor Kwapisiewicz // SPDX-License-Identifier: MIT OR Apache-2.0 -use openpgp_card_pcsc::PcscBackend; +use card_backend_pcsc::PcscBackend; use openpgp_card_sequoia::{state::Open, Card}; use sequoia_openpgp::parse::{stream::DecryptorBuilder, Parse}; use sequoia_openpgp::policy::StandardPolicy; @@ -17,16 +17,14 @@ fn main() -> Result<(), Box> { let card_ident = &args[0]; let pin_file = &args[1]; - let backend = PcscBackend::open_by_ident(card_ident, None)?; + let cards = PcscBackend::card_backends(None)?; - let mut card: Card = backend.into(); + let mut card: Card = Card::::open_by_ident(cards, card_ident)?; let mut transaction = card.transaction()?; let pin = std::fs::read(pin_file)?; - transaction.verify_user(&pin)?; - - let mut user = transaction.user_card().unwrap(); + let mut user = transaction.to_user_card(&pin)?; let p = StandardPolicy::new(); diff --git a/openpgp-card-examples/src/bin/detach-sign.rs b/openpgp-card-examples/src/bin/detach-sign.rs index 4cc87f8..01fc84d 100644 --- a/openpgp-card-examples/src/bin/detach-sign.rs +++ b/openpgp-card-examples/src/bin/detach-sign.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 Wiktor Kwapisiewicz // SPDX-License-Identifier: MIT OR Apache-2.0 -use openpgp_card_pcsc::PcscBackend; +use card_backend_pcsc::PcscBackend; use openpgp_card_sequoia::{state::Open, Card}; use sequoia_openpgp::serialize::stream::{Armorer, Message, Signer}; @@ -16,16 +16,14 @@ fn main() -> Result<(), Box> { let card_ident = &args[0]; let pin_file = &args[1]; - let backend = PcscBackend::open_by_ident(card_ident, None)?; + let cards = PcscBackend::card_backends(None)?; - let mut card: Card = backend.into(); + let mut card: Card = Card::::open_by_ident(cards, card_ident)?; let mut transaction = card.transaction()?; let pin = std::fs::read(pin_file)?; - transaction.verify_user_for_signing(&pin)?; - - let mut sign = transaction.signing_card().unwrap(); + let mut sign = transaction.to_signing_card(&pin)?; let s = sign.signer(&|| println!("Touch confirmation needed for signing"))?; let stdout = std::io::stdout(); diff --git a/openpgp-card-sequoia/Cargo.toml b/openpgp-card-sequoia/Cargo.toml index d19c008..c3a0ad7 100644 --- a/openpgp-card-sequoia/Cargo.toml +++ b/openpgp-card-sequoia/Cargo.toml @@ -1,19 +1,20 @@ -# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-License-Identifier: MIT OR Apache-2.0 [package] name = "openpgp-card-sequoia" description = "Wrapper of openpgp-card for use with Sequoia PGP" license = "MIT OR Apache-2.0" -version = "0.1.1" +version = "0.2.0" authors = ["Heiko Schaefer "] edition = "2018" repository = "https://gitlab.com/openpgp-card/openpgp-card" documentation = "https://docs.rs/crate/openpgp-card-sequoia" [dependencies] +card-backend = { path = "../card-backend", version = "0.2" } sequoia-openpgp = { version = "1.4", default-features = false } -openpgp-card = { path = "../openpgp-card", version = "0.3.3" } +openpgp-card = { path = "../openpgp-card", version = "0.4" } chrono = "0.4" anyhow = "1" thiserror = "1" @@ -21,9 +22,9 @@ log = "0.4" rsa = "0.8.1" [dev-dependencies] -openpgp-card-pcsc = { path = "../pcsc", version = "0.3" } -#openpgp-card-scdc = { path = "../scdc", version = "0.3" } -env_logger = "0.9" +card-backend-pcsc = { path = "../pcsc", version = "0.5" } +#card-backend-scdc = { path = "../scdc", version = "0.5" } +env_logger = "0.10" testresult = "0.3.0" [features] diff --git a/openpgp-card-sequoia/README.md b/openpgp-card-sequoia/README.md index 5417c3b..18cab62 100644 --- a/openpgp-card-sequoia/README.md +++ b/openpgp-card-sequoia/README.md @@ -1,5 +1,5 @@ @@ -12,7 +12,7 @@ It offers convenient access to [OpenPGP card](https://en.wikipedia.org/wiki/OpenPGP_card) functionality using [Sequoia PGP](https://sequoia-pgp.org/). -Note: the current API of this crate is an early draft, reflected by version numbers in the 0.0.x range. +Note: The API of this crate is not finalized yet, please expect occasional breaking changes. **Example code** diff --git a/openpgp-card-sequoia/examples/test.rs b/openpgp-card-sequoia/examples/test.rs index e87b8ba..2d3cdd4 100644 --- a/openpgp-card-sequoia/examples/test.rs +++ b/openpgp-card-sequoia/examples/test.rs @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 use std::env; use std::error::Error; use anyhow::Result; +use card_backend_pcsc::PcscBackend; use openpgp_card::card_do::Sex; use openpgp_card::KeyType; -use openpgp_card_pcsc::PcscBackend; use openpgp_card_sequoia::sq_util; use openpgp_card_sequoia::{state::Open, Card}; use sequoia_openpgp::parse::Parse; @@ -33,162 +33,165 @@ fn main() -> Result<(), Box> { let test_card_ident = env::var("TEST_CARD_IDENT"); if let Ok(test_card_ident) = test_card_ident { - let backend = PcscBackend::open_by_ident(&test_card_ident, None)?; + let cards = PcscBackend::card_backends(None)?; + let mut card = Card::::open_by_ident(cards, &test_card_ident)?; - let mut card: Card = backend.into(); - let mut transaction = card.transaction()?; + { + let mut transaction = card.transaction()?; - // card metadata + // card metadata - let app_id = transaction.application_identifier()?; - println!("{app_id:x?}\n"); + let app_id = transaction.application_identifier()?; + println!("{app_id:x?}\n"); - let eli = transaction.extended_length_information()?; - println!("extended_length_info: {eli:?}\n"); + let eli = transaction.extended_length_information()?; + println!("extended_length_info: {eli:?}\n"); - let hist = transaction.historical_bytes()?; - println!("{hist:#x?}\n"); + let hist = transaction.historical_bytes()?; + println!("{hist:#x?}\n"); - let ext = transaction.extended_capabilities()?; - println!("{ext:#x?}\n"); + let ext = transaction.extended_capabilities()?; + println!("{ext:#x?}\n"); - let pws = transaction.pw_status_bytes()?; - println!("{pws:#x?}\n"); + let pws = transaction.pw_status_bytes()?; + println!("{pws:#x?}\n"); - // cardholder - let ch = transaction.cardholder_related_data()?; - println!("{ch:#x?}\n"); + // cardholder + let ch = transaction.cardholder_related_data()?; + println!("{ch:#x?}\n"); - // crypto-ish metadata - let fp = transaction.fingerprints()?; - println!("Fingerprint {fp:#x?}\n"); + // crypto-ish metadata + let fp = transaction.fingerprints()?; + println!("Fingerprint {fp:#x?}\n"); - match transaction.algorithm_information() { - Ok(Some(ai)) => println!("Algorithm information:\n{ai}"), - Ok(None) => println!("No Algorithm information found"), - Err(e) => println!("Error getting Algorithm information: {e:?}"), + match transaction.algorithm_information() { + Ok(Some(ai)) => println!("Algorithm information:\n{ai}"), + Ok(None) => println!("No Algorithm information found"), + Err(e) => println!("Error getting Algorithm information: {e:?}"), + } + + println!("Current algorithm attributes on card:"); + let algo = transaction.algorithm_attributes(KeyType::Signing)?; + println!("Sig: {algo}"); + let algo = transaction.algorithm_attributes(KeyType::Decryption)?; + println!("Dec: {algo}"); + let algo = transaction.algorithm_attributes(KeyType::Authentication)?; + println!("Aut: {algo}"); + + println!(); + + // --------------------------------------------- + // CAUTION: Write commands ahead! + // Try not to overwrite your production cards. + // --------------------------------------------- + + assert_eq!(app_id.ident(), test_card_ident.to_ascii_uppercase()); + + let check = transaction.check_admin_verified(); + println!("has admin (pw3) been verified yet?\n{check:x?}\n"); + + println!("factory reset\n"); + transaction.factory_reset()?; + + transaction.verify_admin_pin("12345678")?; + println!("verify for admin ok"); + + let check = transaction.check_user_verified(); + println!("has user (pw1/82) been verified yet? {check:x?}"); + + // Use Admin access to card + let mut admin = transaction.to_admin_card(None).expect("just verified"); + + println!(); + + admin.set_cardholder_name("Bar<::open_by_ident(cards, &test_card_ident)?; - let mut card: Card = backend.into(); - let mut transaction = card.transaction()?; + { + let mut transaction = card.transaction()?; - // Check that we're still using the expected card - let app_id = transaction.application_identifier()?; - assert_eq!(app_id.ident(), test_card_ident.to_ascii_uppercase()); + // Check that we're still using the expected card + let app_id = transaction.application_identifier()?; + assert_eq!(app_id.ident(), test_card_ident.to_ascii_uppercase()); - let check = transaction.check_user_verified(); - println!("has user (pw1/82) been verified yet?\n{check:x?}\n"); + let check = transaction.check_user_verified(); + println!("has user (pw1/82) been verified yet?\n{check:x?}\n"); - transaction.verify_user(b"123456")?; - println!("verify for user (pw1/82) ok"); + transaction.verify_user_pin("123456")?; + println!("verify for user (pw1/82) ok"); - let check = transaction.check_user_verified(); - println!("has user (pw1/82) been verified yet?\n{check:x?}\n"); + let check = transaction.check_user_verified(); + println!("has user (pw1/82) been verified yet?\n{check:x?}\n"); - // Use User access to card - let mut user = transaction - .user_card() - .expect("We just validated, this should not fail"); + // Use User access to card + let mut user = transaction + .to_user_card(None) + .expect("We just validated, this should not fail"); - let _cert = Cert::from_file(TEST_KEY_PATH)?; - let msg = std::fs::read_to_string(TEST_ENC_MSG).expect("Unable to read file"); + let _cert = Cert::from_file(TEST_KEY_PATH)?; + let msg = std::fs::read_to_string(TEST_ENC_MSG).expect("Unable to read file"); - println!("Encrypted message:\n{msg}"); + println!("Encrypted message:\n{msg}"); - let sp = StandardPolicy::new(); - let d = user.decryptor(&|| println!("Touch confirmation needed for decryption"))?; - let res = sq_util::decryption_helper(d, msg.into_bytes(), &sp)?; + let sp = StandardPolicy::new(); + let d = user.decryptor(&|| println!("Touch confirmation needed for decryption"))?; + let res = sq_util::decryption_helper(d, msg.into_bytes(), &sp)?; - let plain = String::from_utf8_lossy(&res); - println!("Decrypted plaintext: {plain}"); + let plain = String::from_utf8_lossy(&res); + println!("Decrypted plaintext: {plain}"); - assert_eq!(plain, "Hello world!\n"); + assert_eq!(plain, "Hello world!\n"); + } // ----------------------------- // Open fresh Card for signing // ----------------------------- - let backend = PcscBackend::open_by_ident(&test_card_ident, None)?; + let cards = PcscBackend::card_backends(None)?; + let mut card = Card::::open_by_ident(cards, &test_card_ident)?; - let mut card: Card = backend.into(); let mut transaction = card.transaction()?; // Sign - transaction.verify_user_for_signing(b"123456")?; + transaction.verify_user_signing_pin("123456")?; println!("verify for sign (pw1/81) ok\n"); // Use Sign access to card - let mut sign = transaction.signing_card().expect("just verified"); + let mut sign = transaction.to_signing_card(None).expect("just verified"); let _cert = Cert::from_file(TEST_KEY_PATH)?; @@ -211,7 +214,7 @@ fn main() -> Result<(), Box> { println!("The following OpenPGP cards are connected to your system:"); for backend in PcscBackend::cards(None)? { - let mut card: Card = backend.into(); + let mut card: Card = Card::::new(backend?)?; let open = card.transaction()?; println!(" {}", open.application_identifier()?.ident()); diff --git a/openpgp-card-sequoia/src/decryptor.rs b/openpgp-card-sequoia/src/decryptor.rs index a41a743..0fdbed7 100644 --- a/openpgp-card-sequoia/src/decryptor.rs +++ b/openpgp-card-sequoia/src/decryptor.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use openpgp_card::crypto_data::Cryptogram; -use openpgp_card::OpenPgpTransaction; +use openpgp_card::Transaction; use sequoia_openpgp::crypto::mpi; use sequoia_openpgp::crypto::SessionKey; use sequoia_openpgp::packet; @@ -15,7 +15,7 @@ use crate::PublicKey; pub struct CardDecryptor<'a, 'app> { /// The OpenPGP card (authenticated to allow decryption operations) - ca: &'a mut OpenPgpTransaction<'app>, + ca: &'a mut Transaction<'app>, /// The matching public key for the card's decryption key public: PublicKey, @@ -26,7 +26,7 @@ pub struct CardDecryptor<'a, 'app> { impl<'a, 'app> CardDecryptor<'a, 'app> { pub(crate) fn with_pubkey( - ca: &'a mut OpenPgpTransaction<'app>, + ca: &'a mut Transaction<'app>, public: PublicKey, touch_prompt: &'a (dyn Fn() + Send + Sync), ) -> CardDecryptor<'a, 'app> { @@ -114,6 +114,7 @@ impl<'a, 'app> crypto::Decryptor for CardDecryptor<'a, 'app> { #[allow(non_snake_case)] let S: crypto::mem::Protected = dec.into(); + #[allow(deprecated)] Ok(crypto::ecdh::decrypt_unwrap(&self.public, &S, ciphertext)?) } diff --git a/openpgp-card-sequoia/src/lib.rs b/openpgp-card-sequoia/src/lib.rs index 6928b23..bf41645 100644 --- a/openpgp-card-sequoia/src/lib.rs +++ b/openpgp-card-sequoia/src/lib.rs @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! This crate offers ergonomic abstractions to use -//! [OpenPGP cards](https://en.wikipedia.org/wiki/OpenPGP_card). +//! [OpenPGP card devices](https://en.wikipedia.org/wiki/OpenPGP_card). //! The central abstraction is the [Card] type, which offers access to all card operations. //! //! A [Card] object is always in one of the possible [State]s. The [State] determines which @@ -16,17 +16,17 @@ //! # Backends //! //! To make use of this crate, you need to use a backend for communication -//! with cards. The suggested default backend is `openpgp-card-pcsc`. +//! with cards. The suggested default backend is `card-backend-pcsc`. //! -//! With `openpgp-card-pcsc` you can either open all available cards: +//! With `card-backend-pcsc` you can either open all available cards: //! //! ```no_run -//! use openpgp_card_pcsc::PcscBackend; +//! use card_backend_pcsc::PcscBackend; //! use openpgp_card_sequoia::{state::Open, Card}; //! //! # fn main() -> Result<(), Box> { //! for backend in PcscBackend::cards(None)? { -//! let mut card: Card = backend.into(); +//! let mut card = Card::::new(backend?)?; //! let mut transaction = card.transaction()?; //! println!( //! "Found OpenPGP card with ident '{}'", @@ -40,12 +40,12 @@ //! Or you can open one particular card, by ident: //! //! ```no_run -//! use openpgp_card_pcsc::PcscBackend; +//! use card_backend_pcsc::PcscBackend; //! use openpgp_card_sequoia::{state::Open, Card}; //! //! # fn main() -> Result<(), Box> { -//! let backend = PcscBackend::open_by_ident("abcd:01234567", None)?; -//! let mut card: Card = backend.into(); +//! let cards = PcscBackend::card_backends(None)?; +//! let mut card = Card::::open_by_ident(cards, "abcd:01234567")?; //! let mut transaction = card.transaction()?; //! # Ok(()) //! # } @@ -55,23 +55,22 @@ //! //! ## Decryption //! -//! To use a card for decryption, it needs to be opened, user authorization -//! needs to be available. A `sequoia_openpgp::crypto::Decryptor` -//! implementation can then be obtained: +//! To use a card for decryption, it needs to be opened, and user +//! authorization needs to be available. +//! A [`sequoia_openpgp::crypto::Decryptor`] implementation can then be obtained: //! //! ```no_run -//! use openpgp_card_pcsc::PcscBackend; +//! use card_backend_pcsc::PcscBackend; //! use openpgp_card_sequoia::{state::Open, Card}; //! # fn main() -> Result<(), Box> { +//! //! // Open card via PCSC -//! use sequoia_openpgp::policy::StandardPolicy; -//! let backend = PcscBackend::open_by_ident("abcd:01234567", None)?; -//! let mut card: Card = backend.into(); +//! let cards = PcscBackend::card_backends(None)?; +//! let mut card = Card::::open_by_ident(cards, "abcd:01234567")?; //! let mut transaction = card.transaction()?; //! -//! // Get authorization for user access to the card with password -//! transaction.verify_user(b"123456")?; -//! let mut user = transaction.user_card().expect("This should not fail"); +//! // Get user access to the card (and authorize with the user pin) +//! let mut user = transaction.to_user_card("123456")?; //! //! // Get decryptor //! let decryptor = user.decryptor(&|| println!("Touch confirmation needed for decryption")); @@ -85,9 +84,9 @@ //! //! ## Signing //! -//! To use a card for signing, it needs to be opened, signing authorization -//! needs to be available. A `sequoia_openpgp::crypto::Signer` -//! implementation can then be obtained. +//! To use a card for signing, it needs to be opened, and signing +//! authorization needs to be available. +//! A [`sequoia_openpgp::crypto::Signer`] implementation can then be obtained. //! //! (Note that by default, some OpenPGP Cards will only allow one signing //! operation to be performed after the password has been presented for @@ -95,18 +94,17 @@ //! user password before each signing operation!) //! //! ```no_run -//! use openpgp_card_pcsc::PcscBackend; +//! use card_backend_pcsc::PcscBackend; //! use openpgp_card_sequoia::{state::Open, Card}; //! //! # fn main() -> Result<(), Box> { //! // Open card via PCSC -//! let backend = PcscBackend::open_by_ident("abcd:01234567", None)?; -//! let mut card: Card = backend.into(); +//! let cards = PcscBackend::card_backends(None)?; +//! let mut card = Card::::open_by_ident(cards, "abcd:01234567")?; //! let mut transaction = card.transaction()?; //! -//! // Get authorization for signing access to the card with password -//! transaction.verify_user_for_signing(b"123456")?; -//! let mut user = transaction.signing_card().expect("This should not fail"); +//! // Get signing access to the card (and authorize with the user pin) +//! let mut user = transaction.to_signing_card("123456")?; //! //! // Get signer //! let signer = user.signer(&|| println!("Touch confirmation needed for signing")); @@ -121,46 +119,43 @@ //! # Setting up and configuring a card //! //! ```no_run -//! use openpgp_card_pcsc::PcscBackend; +//! use card_backend_pcsc::PcscBackend; //! use openpgp_card_sequoia::{state::Open, Card}; //! //! # fn main() -> Result<(), Box> { //! // Open card via PCSC -//! let backend = PcscBackend::open_by_ident("abcd:01234567", None)?; -//! let mut card: Card = backend.into(); +//! let cards = PcscBackend::card_backends(None)?; +//! let mut card = Card::::open_by_ident(cards, "abcd:01234567")?; //! let mut transaction = card.transaction()?; //! -//! // Get authorization for admin access to the card with password -//! transaction.verify_admin(b"12345678")?; -//! let mut admin = transaction.admin_card().expect("This should not fail"); +//! // Get admin access to the card (and authorize with the admin pin) +//! let mut admin = transaction.to_admin_card("12345678")?; //! //! // Set the Name and URL fields on the card -//! admin.set_name("Alice Adams")?; +//! admin.set_cardholder_name("Alice Adams")?; //! admin.set_url("https://example.org/openpgp.asc")?; //! //! # Ok(()) //! # } //! ``` -use openpgp_card::algorithm::{Algo, AlgoInfo, AlgoSimple}; +use card_backend::{CardBackend, SmartcardError}; +use openpgp_card::algorithm::{AlgoSimple, AlgorithmAttributes, AlgorithmInformation}; use openpgp_card::card_do::{ ApplicationIdentifier, CardholderRelatedData, ExtendedCapabilities, ExtendedLengthInfo, - Fingerprint, HistoricalBytes, KeyGenerationTime, KeyInformation, Lang, PWStatusBytes, - SecuritySupportTemplate, Sex, TouchPolicy, UIF, + Fingerprint, HistoricalBytes, KeyGenerationTime, KeyInformation, KeySet, Lang, PWStatusBytes, + Sex, TouchPolicy, UserInteractionFlag, }; use openpgp_card::crypto_data::PublicKeyMaterial; -use openpgp_card::{CardBackend, Error, KeySet, KeyType, OpenPgp, OpenPgpTransaction}; +use openpgp_card::{Error, KeyType}; use sequoia_openpgp::cert::prelude::ValidErasedKeyAmalgamation; use sequoia_openpgp::packet::key::SecretParts; use sequoia_openpgp::packet::{key, Key}; -use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; use crate::decryptor::CardDecryptor; use crate::signer::CardSigner; use crate::state::{Admin, Open, Sign, State, Transaction, User}; -use crate::util::{ - public_key_material_and_fp_to_key, public_to_fingerprint, vka_as_uploadable_key, -}; +use crate::util::{public_key_material_and_fp_to_key, vka_as_uploadable_key}; mod decryptor; mod privkey; @@ -173,14 +168,44 @@ pub mod util; /// Shorthand for Sequoia public key data (a single public (sub)key) pub type PublicKey = Key; +/// Optional PIN, used as a parameter to `Card::into_*_card`. +/// +/// Effectively acts like a `Option<&[u8]>`, but with a number of `From` +/// implementations for convenience. +pub struct OptionalPin<'p>(Option<&'p [u8]>); + +impl<'p> From> for OptionalPin<'p> { + fn from(value: Option<&'p [u8]>) -> Self { + OptionalPin(value) + } +} + +impl<'p> From<&'p str> for OptionalPin<'p> { + fn from(value: &'p str) -> Self { + OptionalPin(Some(value.as_bytes())) + } +} + +impl<'p> From<&'p Vec> for OptionalPin<'p> { + fn from(value: &'p Vec) -> Self { + OptionalPin(Some(value)) + } +} + +impl<'p, const S: usize> From<&'p [u8; S]> for OptionalPin<'p> { + fn from(value: &'p [u8; S]) -> Self { + OptionalPin(Some(value)) + } +} + /// Representation of an OpenPGP card. /// -/// A card transitions between `State`s by starting a transaction (that groups together a number +/// A card transitions between [`State`]s by starting a transaction (that groups together a number /// of operations into an atomic sequence) and via PIN presentation. /// -/// Depending on the `State` of the card, and the access privileges that are associated with that +/// Depending on the [`State`] of the card, and the access privileges that are associated with that /// state, different operations can be performed. In many cases, client software will want to -/// transition between states while performing one activity for the user. +/// transition between states while performing one workflow for the user. pub struct Card where S: State, @@ -188,43 +213,73 @@ where state: S, } -impl From for Card -where - B: Into>, -{ - fn from(backend: B) -> Self { - let pgp = OpenPgp::new(backend.into()); - - Card:: { - state: Open { pgp }, - } - } -} - impl Card { + /// Takes an iterator over [`CardBackend`]s, tries to SELECT the OpenPGP card + /// application on each of them, and checks if its application id matches + /// `ident`. + /// Returns a [`Card`] for the first match, if any. + pub fn open_by_ident( + cards: impl Iterator, SmartcardError>>, + ident: &str, + ) -> Result { + for b in cards.filter_map(|c| c.ok()) { + let mut card = Self::new(b)?; + + let aid = { + let tx = card.transaction()?; + tx.state.ard().application_id()? + }; + + if aid.ident() == ident.to_ascii_uppercase() { + return Ok(card); + } + } + + Err(Error::NotFound(format!("Couldn't find card {}", ident))) + } + + /// Returns a [`Card`] based on `backend` (after SELECTing the + /// OpenPGP card application). + pub fn new(backend: B) -> Result + where + B: Into>, + { + let pgp = openpgp_card::Card::new(backend)?; + + Ok(Card:: { + state: Open { pgp }, + }) + } + + /// Starts a transaction on the underlying backend (if the backend + /// implementation supports transactions, otherwise the backend + /// will operate without transaction guarantees). + /// + /// The resulting [`Card`] object allows performing + /// operations on the card. pub fn transaction(&mut self) -> Result, Error> { let opt = self.state.pgp.transaction()?; Card::::new(opt) } + + /// Retrieve the underlying [`CardBackend`]. + /// + /// This is useful to take the card object into a different context + /// (e.g. to perform operations on the card with the `yubikey-management` + /// crate, without closing the connection to the card). + pub fn into_backend(self) -> Box { + self.state.pgp.into_card() + } } impl<'a> Card> { - /// Do not use! - /// - /// FIXME: this interface is currently used in `card-functionality`, for testing. - /// It will be removed. - pub fn new(mut opt: OpenPgpTransaction<'a>) -> Result { + /// Internal constructor + fn new(mut opt: openpgp_card::Transaction<'a>) -> Result { let ard = opt.application_related_data()?; Ok(Self { - state: Transaction { - opt, - ard, - pw1: false, - pw1_sign: false, - pw3: false, - }, + state: Transaction::new(opt, ard), }) } @@ -232,27 +287,37 @@ impl<'a> Card> { /// with the current data on the card. /// /// This is needed e.g. after importing or generating keys on a card, to - /// see these changes reflected in `self.ard`. + /// see these changes reflected in the internal cached + /// [`openpgp_card::card_do::ApplicationRelatedData`]. pub fn reload_ard(&mut self) -> Result<(), Error> { // FIXME: this should be implemented internally, transparent to users - self.state.ard = self.state.opt.application_related_data()?; + + let ard = self.state.opt.application_related_data()?; + + self.state.set_ard(ard); + Ok(()) } + /// True if the reader for this card supports PIN verification with a pin pad. pub fn feature_pinpad_verify(&mut self) -> bool { self.state.opt.feature_pinpad_verify() } + /// True if the reader for this card supports PIN modification with a pin pad. pub fn feature_pinpad_modify(&mut self) -> bool { self.state.opt.feature_pinpad_modify() } - pub fn verify_user(&mut self, pin: &[u8]) -> Result<(), Error> { - self.state.opt.verify_pw1_user(pin)?; + /// Verify the User PIN (for operations such as decryption) + pub fn verify_user_pin(&mut self, pin: &str) -> Result<(), Error> { + self.state.opt.verify_pw1_user(pin.as_bytes())?; self.state.pw1 = true; Ok(()) } + /// Verify the User PIN with a physical PIN pad (if available, + /// see [`Self::feature_pinpad_verify`]). pub fn verify_user_pinpad(&mut self, pinpad_prompt: &dyn Fn()) -> Result<(), Error> { pinpad_prompt(); @@ -261,8 +326,13 @@ impl<'a> Card> { Ok(()) } - pub fn verify_user_for_signing(&mut self, pin: &[u8]) -> Result<(), Error> { - self.state.opt.verify_pw1_sign(pin)?; + /// Verify the User PIN for signing operations. + /// + /// (Note that depending on the configuration of the card, this may enable + /// performing just one signing operation, or an unlimited amount of + /// signing operations). + pub fn verify_user_signing_pin(&mut self, pin: &str) -> Result<(), Error> { + self.state.opt.verify_pw1_sign(pin.as_bytes())?; // FIXME: depending on card mode, pw1_sign is only usable once @@ -270,10 +340,9 @@ impl<'a> Card> { Ok(()) } - pub fn verify_user_for_signing_pinpad( - &mut self, - pinpad_prompt: &dyn Fn(), - ) -> Result<(), Error> { + /// Verify the User PIN for signing operations with a physical PIN pad + /// (if available, see [`Self::feature_pinpad_verify`]). + pub fn verify_user_signing_pinpad(&mut self, pinpad_prompt: &dyn Fn()) -> Result<(), Error> { pinpad_prompt(); self.state.opt.verify_pw1_sign_pinpad()?; @@ -284,12 +353,15 @@ impl<'a> Card> { Ok(()) } - pub fn verify_admin(&mut self, pin: &[u8]) -> Result<(), Error> { - self.state.opt.verify_pw3(pin)?; + /// Verify the Admin PIN. + pub fn verify_admin_pin(&mut self, pin: &str) -> Result<(), Error> { + self.state.opt.verify_pw3(pin.as_bytes())?; self.state.pw3 = true; Ok(()) } + /// Verify the Admin PIN with a physical PIN pad + /// (if available, see [`Self::feature_pinpad_verify`]). pub fn verify_admin_pinpad(&mut self, pinpad_prompt: &dyn Fn()) -> Result<(), Error> { pinpad_prompt(); @@ -300,73 +372,166 @@ impl<'a> Card> { /// Ask the card if the user password has been successfully verified. /// - /// NOTE: on some cards this functionality seems broken. + /// NOTE: on some cards this functionality seems broken and may decrease + /// the pin's error count! pub fn check_user_verified(&mut self) -> Result<(), Error> { self.state.opt.check_pw1_user() } /// Ask the card if the admin password has been successfully verified. /// - /// NOTE: on some cards this functionality seems broken. + /// NOTE: on some cards this functionality seems broken and may decrease + /// the pin's error count! pub fn check_admin_verified(&mut self) -> Result<(), Error> { self.state.opt.check_pw3() } - pub fn change_user_pin(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { - self.state.opt.change_pw1(old, new) + /// Change the User PIN, based on the old User PIN. + pub fn change_user_pin(&mut self, old: &str, new: &str) -> Result<(), Error> { + self.state.opt.change_pw1(old.as_bytes(), new.as_bytes()) } + /// Change the User PIN, based on the old User PIN, with a physical PIN + /// pad (if available, see [`Self::feature_pinpad_modify`]). pub fn change_user_pin_pinpad(&mut self, pinpad_prompt: &dyn Fn()) -> Result<(), Error> { pinpad_prompt(); self.state.opt.change_pw1_pinpad() } - pub fn reset_user_pin(&mut self, rst: &[u8], new: &[u8]) -> Result<(), Error> { - self.state.opt.reset_retry_counter_pw1(new, Some(rst)) + /// Change the User PIN, based on the resetting code `rst`. + pub fn reset_user_pin(&mut self, rst: &str, new: &str) -> Result<(), Error> { + self.state + .opt + .reset_retry_counter_pw1(new.as_bytes(), Some(rst.as_bytes())) } - pub fn change_admin_pin(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { - self.state.opt.change_pw3(old, new) + /// Change the Admin PIN, based on the old Admin PIN. + pub fn change_admin_pin(&mut self, old: &str, new: &str) -> Result<(), Error> { + self.state.opt.change_pw3(old.as_bytes(), new.as_bytes()) } + /// Change the Admin PIN, based on the old Admin PIN, with a physical PIN + /// pad (if available, see [`Self::feature_pinpad_modify`]). pub fn change_admin_pin_pinpad(&mut self, pinpad_prompt: &dyn Fn()) -> Result<(), Error> { pinpad_prompt(); self.state.opt.change_pw3_pinpad() } - /// Get a view of the card authenticated for "User" commands. - pub fn user_card<'b>(&'b mut self) -> Option>> { - Some(Card:: { + /// Get a view of the card in the [`Card`] state, and authenticate + /// for that state with `pin`, if available. + /// + /// If `pin` is not None, `verify_user` is called with that pin. + pub fn to_user_card<'b, 'p, P>(&'b mut self, pin: P) -> Result>, Error> + where + P: Into>, + { + let pin: OptionalPin = pin.into(); + + if let Some(pin) = pin.0 { + self.verify_user_pin(String::from_utf8_lossy(pin).as_ref())?; + } + + Ok(Card:: { state: User { tx: self }, }) } - /// Get a view of the card authenticated for Signing. - pub fn signing_card<'b>(&'b mut self) -> Option>> { - Some(Card:: { + /// Get a view of the card in the [`Card`] state, and authenticate + /// for that state with `pin`, if available. + /// + /// If `pin` is not None, `verify_user_for_signing` is called with that pin. + pub fn to_signing_card<'b, 'p, P>(&'b mut self, pin: P) -> Result>, Error> + where + P: Into>, + { + let pin: OptionalPin = pin.into(); + + if let Some(pin) = pin.0 { + self.verify_user_signing_pin(String::from_utf8_lossy(pin).as_ref())?; + } + + Ok(Card:: { state: Sign { tx: self }, }) } - /// Get a view of the card authenticated for "Admin" commands. - pub fn admin_card<'b>(&'b mut self) -> Option>> { - Some(Card:: { + /// Get a view of the card in the [`Card`] state, and authenticate + /// for that state with `pin`, if available. + /// + /// If `pin` is not None, `verify_admin` is called with that pin. + pub fn to_admin_card<'b, 'p, P>(&'b mut self, pin: P) -> Result>, Error> + where + P: Into>, + { + let pin: OptionalPin = pin.into(); + + if let Some(pin) = pin.0 { + self.verify_admin_pin(String::from_utf8_lossy(pin).as_ref())?; + } + + Ok(Card:: { state: Admin { tx: self }, }) } // --- application data --- + /// The Application Identifier is unique for each card. + /// It includes a manufacturer code and serial number. + /// + /// (This is an immutable field on the card. The value is cached in the + /// underlying Card object. It can be retrieved without incurring a call + /// to the card) pub fn application_identifier(&self) -> Result { - self.state.ard.application_id() + // Use immutable data cache from underlying Card object + self.state.opt.application_identifier() } + /// The "Extended Capabilities" data object describes features of a card + /// to the caller. + /// This includes the availability and length of various data fields. + /// + /// (This is an immutable field on the card. The value is cached in the + /// underlying Card object. It can be retrieved without incurring a call + /// to the card) + pub fn extended_capabilities(&self) -> Result { + // Use immutable data cache from underlying Card object + self.state.opt.extended_capabilities() + } + + /// The "Historical Bytes" data object describes features of a card + /// to the caller. + /// The information in this field is probably not relevant for most + /// users of this library, however, some of it is used for the internal + /// operation of the `openpgp-card` library. + /// + /// (This is an immutable field on the card. The value is cached in the + /// underlying Card object. It can be retrieved without incurring a call + /// to the card) pub fn historical_bytes(&self) -> Result { - self.state.ard.historical_bytes() + // Use immutable data cache from underlying Card object + match self.state.opt.historical_bytes()? { + Some(hb) => Ok(hb), + None => Err(Error::NotFound( + "Card doesn't have historical bytes DO".to_string(), + )), + } } + /// The "Extended Length Information" data object was introduced in + /// version 3.0 of the OpenPGP card standard. + /// + /// The information in this field should not be relevant for + /// users of this library. + /// However, it is used for the internal operation of the `openpgp-card` + /// library. + /// + /// (This is an immutable field on the card. The value is cached in the + /// underlying Card object. It can be retrieved without incurring a call + /// to the card) pub fn extended_length_information(&self) -> Result, Error> { - self.state.ard.extended_length_information() + // Use immutable data cache from underlying Card object + self.state.opt.extended_length_info() } #[allow(dead_code)] @@ -379,58 +544,134 @@ impl<'a> Card> { unimplemented!() } - pub fn extended_capabilities(&self) -> Result { - self.state.ard.extended_capabilities() - } - - pub fn algorithm_attributes(&self, key_type: KeyType) -> Result { - self.state.ard.algorithm_attributes(key_type) - } - - /// PW status Bytes + /// PW Status Bytes pub fn pw_status_bytes(&self) -> Result { - self.state.ard.pw_status_bytes() + self.state.ard().pw_status_bytes() } + /// Get algorithm attributes for a key slot. + pub fn algorithm_attributes(&self, key_type: KeyType) -> Result { + self.state.ard().algorithm_attributes(key_type) + } + + /// Get the Fingerprints for the three basic [`KeyType`]s. + /// + /// (The fingerprints for the three basic key slots are stored in a + /// shared field on the card, thus they can be retrieved in one go) pub fn fingerprints(&self) -> Result, Error> { - self.state.ard.fingerprints() + self.state.ard().fingerprints() } - pub fn ca_fingerprints(&self) -> Result<[Option; 3], Error> { - self.state.ard.ca_fingerprints() + /// Get the Fingerprint for one [`KeyType`]. + /// + /// This function allows retrieval for all slots, including + /// [`KeyType::Attestation`], if available. + pub fn fingerprint(&mut self, key_type: KeyType) -> Result, Error> { + let fp = match key_type { + KeyType::Signing => self.fingerprints()?.signature().cloned(), + KeyType::Decryption => self.fingerprints()?.decryption().cloned(), + KeyType::Authentication => self.fingerprints()?.authentication().cloned(), + KeyType::Attestation => self.state.ard().attestation_key_fingerprint()?, + _ => { + return Err(Error::UnsupportedFeature(format!( + "Can't get fingerprint for key_type {:?}", + key_type, + ))) + } + }; + + Ok(fp) } + /// Get the Key Creation Times for the three basic [`KeyType`]s. + /// + /// (The creation time for the three basic key slots are stored in a + /// shared field on the card, thus they can be retrieved in one go) pub fn key_generation_times(&self) -> Result, Error> { - self.state.ard.key_generation_times() + self.state.ard().key_generation_times() + } + + /// Get the Key Creation Time for one [`KeyType`]. + /// + /// This function allows retrieval for all slots, including + /// [`KeyType::Attestation`], if available. + pub fn key_generation_time( + &mut self, + key_type: KeyType, + ) -> Result, Error> { + let ts = match key_type { + KeyType::Signing => self.key_generation_times()?.signature().cloned(), + KeyType::Decryption => self.key_generation_times()?.decryption().cloned(), + KeyType::Authentication => self.key_generation_times()?.authentication().cloned(), + KeyType::Attestation => self.state.ard().attestation_key_generation_time()?, + _ => { + return Err(Error::UnsupportedFeature(format!( + "Can't get creation time for key_type {:?}", + key_type, + ))) + } + }; + + Ok(ts) } pub fn key_information(&self) -> Result, Error> { - self.state.ard.key_information() + self.state.ard().key_information() } - pub fn uif_signing(&self) -> Result, Error> { - self.state.ard.uif_pso_cds() + /// Get the [`UserInteractionFlag`] for a key slot. + /// This includes the [`TouchPolicy`], if the card supports touch + /// confirmation. + pub fn user_interaction_flag( + &self, + key_type: KeyType, + ) -> Result, Error> { + match key_type { + KeyType::Signing => self.state.ard().uif_pso_cds(), + KeyType::Decryption => self.state.ard().uif_pso_dec(), + KeyType::Authentication => self.state.ard().uif_pso_aut(), + KeyType::Attestation => self.state.ard().uif_attestation(), + _ => Err(Error::UnsupportedFeature(format!( + "Can't get UIF for key_type {:?}", + key_type, + ))), + } } - pub fn uif_decryption(&self) -> Result, Error> { - self.state.ard.uif_pso_dec() + /// List of CA-Fingerprints of “Ultimately Trusted Keys”. + /// May be used to verify Public Keys from servers. + pub fn ca_fingerprints(&self) -> Result<[Option; 3], Error> { + self.state.ard().ca_fingerprints() } - pub fn uif_authentication(&self) -> Result, Error> { - self.state.ard.uif_pso_aut() + /// Get optional "Private use" data from the card. + /// + /// The presence and maximum length of these DOs is announced + /// in [`ExtendedCapabilities`]. + /// + /// If available, there are 4 data fields for private use: + /// + /// - `1`: read accessible without PIN verification + /// - `2`: read accessible without PIN verification + /// - `3`: read accessible with User PIN verification + /// - `4`: read accessible with Admin PIN verification + pub fn private_use_do(&mut self, num: u8) -> Result, Error> { + self.state.opt.private_use_do(num) } - pub fn uif_attestation(&self) -> Result, Error> { - self.state.ard.uif_attestation() + /// Login Data + /// + /// This DO can be used to store any information used for the Log-In + /// process in a client/server authentication (e.g. user name of a + /// network). + /// The maximum length of this DO is announced in Extended Capabilities. + pub fn login_data(&mut self) -> Result, Error> { + self.state.opt.login_data() } - // --- optional private DOs (0101 - 0104) --- - - // --- login data (5e) --- - // --- URL (5f50) --- - /// Get "hardholder" URL from the card. + /// Get "cardholder" URL from the card. /// /// "The URL should contain a link to a set of public keys in OpenPGP format, related to /// the card." @@ -438,7 +679,7 @@ impl<'a> Card> { Ok(String::from_utf8_lossy(&self.state.opt.url()?).to_string()) } - // --- cardholder related data (65) --- + /// Cardholder related data (contains the fields: Name, Language preferences and Sex) pub fn cardholder_related_data(&mut self) -> Result { self.state.opt.cardholder_related_data() } @@ -448,33 +689,40 @@ impl<'a> Card> { s.iter().map(|&c| c as char).collect() } - /// Get cardholder name as a String (this also normalizes the "<" and "<<" filler chars) - pub fn cardholder_name(&mut self) -> Result, Error> { + /// Get cardholder name. + /// + /// This is an ISO 8859-1 (Latin 1) String of up to 39 characters. + /// + /// Note that the standard specifies that this field should be encoded + /// according to ISO/IEC 7501-1: + /// + /// "The data element consists of surname (e. g. family name and given + /// name(s)) and forename(s) (including name suffix, e. g., Jr. and number). + /// Each item is separated by a ´<´ filler character (3C), the family- and + /// fore-name(s) are separated by two ´<<´ filler characters." + /// + /// This library doesn't perform this encoding. + pub fn cardholder_name(&mut self) -> Result { let crd = self.state.opt.cardholder_related_data()?; - if let Some(name) = crd.name() { - let name = Self::latin1_to_string(name); - // re-format name ("last< = name.split("<<").collect(); - let name = name.iter().cloned().rev().collect::>().join(" "); - - // replace item separators with spaces - let name = name.replace('<', " "); - - Ok(Some(name)) - } else { - Ok(None) + match crd.name() { + Some(name) => Ok(Self::latin1_to_string(name)), + None => Ok("".to_string()), } } - // --- security support template (7a) --- - pub fn security_support_template(&mut self) -> Result { - self.state.opt.security_support_template() + /// Get the current digital signature count (how many signatures have been issued by the card) + pub fn digital_signature_count(&mut self) -> Result { + Ok(self + .state + .opt + .security_support_template()? + .signature_count()) } /// SELECT DATA ("select a DO in the current template"). - pub fn select_data(&mut self, num: u8, tag: &[u8], yk_workaround: bool) -> Result<(), Error> { - self.state.opt.select_data(num, tag, yk_workaround) + pub fn select_data(&mut self, num: u8, tag: &[u8]) -> Result<(), Error> { + self.state.opt.select_data(num, tag) } /// Get cardholder certificate. @@ -493,8 +741,8 @@ impl<'a> Card> { self.state.opt.next_cardholder_certificate() } - // DO "Algorithm Information" (0xFA) - pub fn algorithm_information(&mut self) -> Result, Error> { + /// Algorithm Information (list of supported Algorithm attributes). + pub fn algorithm_information(&mut self) -> Result, Error> { // The DO "Algorithm Information" (Tag FA) shall be present if // Algorithm attributes can be changed let ec = self.extended_capabilities()?; @@ -507,8 +755,9 @@ impl<'a> Card> { self.state.opt.algorithm_information() } - /// "MANAGE SECURITY ENVIRONMENT" - /// Make `key_ref` usable for the operation normally done by the key designated by `for_operation` + /// "MANAGE SECURITY ENVIRONMENT". + /// Make `key_ref` usable for the operation normally done by the key + /// designated by `for_operation` pub fn manage_security_environment( &mut self, for_operation: KeyType, @@ -526,18 +775,6 @@ impl<'a> Card> { self.state.opt.attestation_certificate() } - pub fn attestation_key_fingerprint(&mut self) -> Result, Error> { - self.state.ard.attestation_key_fingerprint() - } - - pub fn attestation_key_algorithm_attributes(&mut self) -> Result, Error> { - self.state.ard.attestation_key_algorithm_attributes() - } - - pub fn attestation_key_generation_time(&mut self) -> Result, Error> { - self.state.ard.attestation_key_generation_time() - } - /// Firmware Version, YubiKey specific (?) pub fn firmware_version(&mut self) -> Result, Error> { self.state.opt.firmware_version() @@ -559,34 +796,36 @@ impl<'a> Card> { // ---------- + /// Get the raw public key material for a key slot on the card + /// (also see [`Self::public_key`] for getting a Sequoia PGP key object) pub fn public_key_material(&mut self, key_type: KeyType) -> Result { self.state.opt.public_key(key_type) } - // ---------- - - /// Delete all state on this OpenPGP card - pub fn factory_reset(&mut self) -> Result<(), Error> { - self.state.opt.factory_reset() - } - - // -- higher level abstractions - - /// Get PublicKey representation for a key slot on the card + /// Get a sequoia public key representation + /// ([`Key`]) + /// for a key slot on the card pub fn public_key(&mut self, kt: KeyType) -> Result, Error> { - // FIXME: only read these once, if multiple subkeys are retrieved from the card - let times = self.key_generation_times()?; let fps = self.fingerprints()?; + let ts = self.key_generation_time(kt)?; + + let fp = match kt { + KeyType::Signing => fps.signature(), + KeyType::Decryption => fps.decryption(), + KeyType::Authentication => fps.authentication(), + _ => None, + }; + match kt { KeyType::Signing => { - if let Ok(pkm) = self.public_key_material(KeyType::Signing) { - if let Some(ts) = times.signature() { + if let Ok(pkm) = self.public_key_material(kt) { + if let Some(ts) = ts { return Ok(Some(public_key_material_and_fp_to_key( &pkm, KeyType::Signing, - ts, - fps.signature().expect("Signature fingerprint is unset"), + &ts, + fp.expect("Signature fingerprint is unset"), )?)); } } @@ -594,12 +833,12 @@ impl<'a> Card> { } KeyType::Decryption => { if let Ok(pkm) = self.public_key_material(KeyType::Decryption) { - if let Some(ts) = times.decryption() { + if let Some(ts) = ts { return Ok(Some(public_key_material_and_fp_to_key( &pkm, KeyType::Decryption, - ts, - fps.decryption().expect("Decryption fingerprint is unset"), + &ts, + fp.expect("Decryption fingerprint is unset"), )?)); } } @@ -607,13 +846,12 @@ impl<'a> Card> { } KeyType::Authentication => { if let Ok(pkm) = self.public_key_material(KeyType::Authentication) { - if let Some(ts) = times.authentication() { + if let Some(ts) = ts { return Ok(Some(public_key_material_and_fp_to_key( &pkm, KeyType::Authentication, - ts, - fps.authentication() - .expect("Authentication fingerprint is unset"), + &ts, + fp.expect("Authentication fingerprint is unset"), )?)); } } @@ -622,11 +860,18 @@ impl<'a> Card> { _ => unimplemented!(), } } + + // ---------- + + /// Reset all state on this OpenPGP card + pub fn factory_reset(&mut self) -> Result<(), Error> { + self.state.opt.factory_reset() + } } impl<'app, 'open> Card> { /// Helper fn to easily access underlying openpgp_card object - fn card(&mut self) -> &mut OpenPgpTransaction<'app> { + fn card(&mut self) -> &mut openpgp_card::Transaction<'app> { &mut self.state.tx.state.opt } @@ -674,11 +919,26 @@ impl<'app, 'open> Card> { ) -> CardSigner<'_, 'app> { CardSigner::with_pubkey_for_auth(self.card(), pubkey, touch_prompt) } + + /// Set optional "Private use" data on the card. + /// + /// The presence and maximum length of these DOs is announced + /// in [`ExtendedCapabilities`]. + /// + /// If available, there are 4 data fields for private use: + /// + /// - `1`: write accessible with User PIN verification + /// - `2`: write accessible with Admin PIN verification + /// - `3`: write accessible with User PIN verification + /// - `4`: write accessible with Admin PIN verification + pub fn set_private_use_do(&mut self, num: u8, data: Vec) -> Result<(), Error> { + self.card().set_private_use_do(num, data) + } } impl<'app, 'open> Card> { /// Helper fn to easily access underlying openpgp_card object - fn card(&mut self) -> &mut OpenPgpTransaction<'app> { + fn card(&mut self) -> &mut openpgp_card::Transaction<'app> { &mut self.state.tx.state.opt } @@ -718,7 +978,7 @@ impl<'app, 'open> Card> { // Touch is required if: // - the card supports the feature // - and the policy is set to a value other than 'Off' - if let Some(uif) = self.state.tx.state.ard.uif_attestation()? { + if let Some(uif) = self.state.tx.state.ard().uif_attestation()? { if uif.touch_policy().touch_required() { (touch_prompt)(); } @@ -729,18 +989,31 @@ impl<'app, 'open> Card> { } impl<'app, 'open> Card> { - pub fn as_open(&'_ mut self) -> &mut Card> { + pub fn as_transaction(&'_ mut self) -> &mut Card> { self.state.tx } /// Helper fn to easily access underlying openpgp_card object - fn card(&mut self) -> &mut OpenPgpTransaction<'app> { + fn card(&mut self) -> &mut openpgp_card::Transaction<'app> { &mut self.state.tx.state.opt } } impl Card> { - pub fn set_name(&mut self, name: &str) -> Result<(), Error> { + /// Set cardholder name. + /// + /// This is an ISO 8859-1 (Latin 1) String of max. 39 characters. + /// + /// Note that the standard specifies that this field should be encoded according + /// to ISO/IEC 7501-1: + /// + /// "The data element consists of surname (e. g. family name and given + /// name(s)) and forename(s) (including name suffix, e. g., Jr. and number). + /// Each item is separated by a ´<´ filler character (3C), the family- and + /// fore-name(s) are separated by two ´<<´ filler characters." + /// + /// This library doesn't perform this encoding. + pub fn set_cardholder_name(&mut self, name: &str) -> Result<(), Error> { // All chars must be in ASCII7 if name.chars().any(|c| !c.is_ascii()) { return Err(Error::InternalError("Invalid char in name".into())); @@ -767,6 +1040,25 @@ impl Card> { self.card().set_sex(sex) } + /// Set optional "Private use" data on the card. + /// + /// The presence and maximum length of these DOs is announced + /// in [`ExtendedCapabilities`]. + /// + /// If available, there are 4 data fields for private use: + /// + /// - `1`: write accessible with User PIN verification + /// - `2`: write accessible with Admin PIN verification + /// - `3`: write accessible with User PIN verification + /// - `4`: write accessible with Admin PIN verification + pub fn set_private_use_do(&mut self, num: u8, data: Vec) -> Result<(), Error> { + self.card().set_private_use_do(num, data) + } + + pub fn set_login_data(&mut self, login_data: &[u8]) -> Result<(), Error> { + self.card().set_login(login_data) + } + /// Set "hardholder" URL on the card. /// /// "The URL should contain a link to a set of public keys in OpenPGP format, related to @@ -792,12 +1084,48 @@ impl Card> { } } - pub fn set_uif(&mut self, key: KeyType, policy: TouchPolicy) -> Result<(), Error> { + /// Set PW Status Bytes. + /// + /// According to the spec, length information should not be changed. + /// + /// If `long` is false, sends 1 byte to the card, otherwise 4. + /// + /// So, effectively, with `long == false` the setting `pw1_cds_multi` + /// can be changed. + /// With `long == true`, the settings `pw1_pin_block` and `pw3_pin_block` + /// can also be changed. + pub fn set_pw_status_bytes( + &mut self, + pw_status: &PWStatusBytes, + long: bool, + ) -> Result<(), Error> { + self.card().set_pw_status_bytes(pw_status, long) + } + + /// Configure the "only valid for one PSO:CDS" setting in PW Status Bytes. + /// + /// If `once` is `true`, the User PIN must be verified before each + /// signing operation on the card. + /// If `once` is `false`, one User PIN verification is good for an + /// unlimited number of signing operations. + pub fn set_user_pin_signing_validity(&mut self, once: bool) -> Result<(), Error> { + let mut pws = self.as_transaction().pw_status_bytes()?; + pws.set_pw1_cds_valid_once(once); + + self.set_pw_status_bytes(&pws, false) + } + + /// Set the touch policy for a key slot (if the card supports this + /// feature). + /// + /// Note that the current touch policy setting (if available) can be read + /// via [`Card::user_interaction_flag`]. + pub fn set_touch_policy(&mut self, key: KeyType, policy: TouchPolicy) -> Result<(), Error> { let uif = match key { - KeyType::Signing => self.state.tx.state.ard.uif_pso_cds()?, - KeyType::Decryption => self.state.tx.state.ard.uif_pso_dec()?, - KeyType::Authentication => self.state.tx.state.ard.uif_pso_aut()?, - KeyType::Attestation => self.state.tx.state.ard.uif_attestation()?, + KeyType::Signing => self.state.tx.state.ard().uif_pso_cds()?, + KeyType::Decryption => self.state.tx.state.ard().uif_pso_dec()?, + KeyType::Authentication => self.state.tx.state.ard().uif_pso_aut()?, + KeyType::Attestation => self.state.tx.state.ard().uif_attestation()?, _ => unimplemented!(), }; @@ -820,19 +1148,28 @@ impl Card> { Ok(()) } - pub fn set_resetting_code(&mut self, pin: &[u8]) -> Result<(), Error> { - self.card().set_resetting_code(pin) + /// Set the User PIN on the card (also resets the User PIN error count) + pub fn reset_user_pin(&mut self, new: &str) -> Result<(), Error> { + self.card().reset_retry_counter_pw1(new.as_bytes(), None) } + /// Define the "resetting code" on the card + pub fn set_resetting_code(&mut self, pin: &str) -> Result<(), Error> { + self.card().set_resetting_code(pin.as_bytes()) + } + + /// Set optional AES encryption/decryption key + /// (32 bytes for AES256, or 16 bytes for AES128), + /// if the card supports this feature. + /// + /// The availability of this feature is announced in + /// [`Card::extended_capabilities`]. pub fn set_pso_enc_dec_key(&mut self, key: &[u8]) -> Result<(), Error> { self.card().set_pso_enc_dec_key(key) } - pub fn reset_user_pin(&mut self, new: &[u8]) -> Result<(), Error> { - self.card().reset_retry_counter_pw1(new, None) - } - - /// Upload a ValidErasedKeyAmalgamation to the card as a specific KeyType. + /// Upload a Sequoia PGP [`ValidErasedKeyAmalgamation`] to the card into + /// a specific key slot. /// /// (The caller needs to make sure that `vka` is suitable as `key_type`) pub fn upload_key( @@ -845,32 +1182,45 @@ impl Card> { self.card().key_import(key, key_type) } - /// Wrapper fn for `public_to_fingerprint` that uses SHA256/AES128 as default parameters. + /// Configure the `algorithm_attributes` for key slot `key_type` based on + /// the algorithm `algo`. + /// This can be useful in preparation for [`Self::generate_key`]. /// - /// FIXME: This is a hack. - /// These parameters should probably be automatically determined based on the algorithm used? - fn ptf( - pkm: &PublicKeyMaterial, - time: KeyGenerationTime, - key_type: KeyType, - ) -> Result { - public_to_fingerprint( - pkm, - &time, - key_type, - Some(HashAlgorithm::SHA256), - Some(SymmetricAlgorithm::AES128), - ) + /// This is a convenience wrapper for [`Self::set_algorithm_attributes`] + /// that determines the exact appropriate [`AlgorithmAttributes`] by + /// reading information from the card. + pub fn set_algorithm(&mut self, key_type: KeyType, algo: AlgoSimple) -> Result<(), Error> { + let attr = algo.matching_algorithm_attributes(self.card(), key_type)?; + self.set_algorithm_attributes(key_type, &attr) } - pub fn generate_key_simple( + /// Configure the key slot `key_type` to `algorithm_attributes`. + /// This can be useful in preparation for [`Self::generate_key`]. + /// + /// Note that legal values for [`AlgorithmAttributes`] are card-specific. + /// Different OpenPGP card implementations may support different + /// algorithms, sometimes with differing requirements for the encoding + /// (e.g. field sizes) + /// + /// See [`Self::set_algorithm`] for a convenience function that sets + /// the algorithm attributes based on an [`AlgoSimple`]. + pub fn set_algorithm_attributes( + &mut self, + key_type: KeyType, + algorithm_attributes: &AlgorithmAttributes, + ) -> Result<(), Error> { + self.card() + .set_algorithm_attributes(key_type, algorithm_attributes) + } + + /// Generate a new cryptographic key in slot `key_type`, with the currently + /// configured cryptographic algorithm + /// (see [`Self::set_algorithm`] for changing the algorithm setting). + pub fn generate_key( &mut self, key_type: KeyType, - algo: Option, ) -> Result<(PublicKeyMaterial, KeyGenerationTime), Error> { - match algo { - Some(algo) => self.card().generate_key_simple(Self::ptf, key_type, algo), - None => self.card().generate_key(Self::ptf, key_type, None), - } + self.card() + .generate_key(crate::util::public_to_fingerprint, key_type) } } diff --git a/openpgp-card-sequoia/src/privkey.rs b/openpgp-card-sequoia/src/privkey.rs index 73a6d68..b4e8a73 100644 --- a/openpgp-card-sequoia/src/privkey.rs +++ b/openpgp-card-sequoia/src/privkey.rs @@ -234,11 +234,11 @@ impl EccKey for SqEccKey { } fn private(&self) -> Vec { - // FIXME: padding for 25519? match self.curve { Curve::NistP256 => self.private.value_padded(0x20).to_vec(), Curve::NistP384 => self.private.value_padded(0x30).to_vec(), Curve::NistP521 => self.private.value_padded(0x42).to_vec(), + Curve::Cv25519 | Curve::Ed25519 => self.private.value_padded(0x20).to_vec(), _ => self.private.value().to_vec(), } } diff --git a/openpgp-card-sequoia/src/signer.rs b/openpgp-card-sequoia/src/signer.rs index 079dc70..d377701 100644 --- a/openpgp-card-sequoia/src/signer.rs +++ b/openpgp-card-sequoia/src/signer.rs @@ -5,7 +5,7 @@ use std::convert::TryInto; use anyhow::anyhow; use openpgp_card::crypto_data::Hash; -use openpgp_card::OpenPgpTransaction; +use openpgp_card::Transaction; use sequoia_openpgp::crypto; use sequoia_openpgp::crypto::mpi; use sequoia_openpgp::types::{Curve, PublicKeyAlgorithm}; @@ -14,7 +14,7 @@ use crate::PublicKey; pub struct CardSigner<'a, 'app> { /// The OpenPGP card (authenticated to allow signing operations) - ca: &'a mut OpenPgpTransaction<'app>, + ca: &'a mut Transaction<'app>, /// The matching public key for the card's signing key public: PublicKey, @@ -28,7 +28,7 @@ pub struct CardSigner<'a, 'app> { impl<'a, 'app> CardSigner<'a, 'app> { pub(crate) fn with_pubkey( - ca: &'a mut OpenPgpTransaction<'app>, + ca: &'a mut Transaction<'app>, public: PublicKey, touch_prompt: &'a (dyn Fn() + Send + Sync), ) -> CardSigner<'a, 'app> { @@ -41,7 +41,7 @@ impl<'a, 'app> CardSigner<'a, 'app> { } pub(crate) fn with_pubkey_for_auth( - ca: &'a mut OpenPgpTransaction<'app>, + ca: &'a mut Transaction<'app>, public: PublicKey, touch_prompt: &'a (dyn Fn() + Send + Sync), ) -> CardSigner<'a, 'app> { @@ -84,9 +84,9 @@ impl<'a, 'app> crypto::Signer for CardSigner<'a, 'app> { }; let sig_fn = if !self.auth { - OpenPgpTransaction::signature_for_hash + Transaction::signature_for_hash } else { - OpenPgpTransaction::authenticate_for_hash + Transaction::authenticate_for_hash }; // Delegate a signing (or auth) operation to the OpenPGP card. diff --git a/openpgp-card-sequoia/src/state.rs b/openpgp-card-sequoia/src/state.rs index 2143f3f..db03ac6 100644 --- a/openpgp-card-sequoia/src/state.rs +++ b/openpgp-card-sequoia/src/state.rs @@ -4,7 +4,6 @@ //! States of a card are modeled by the types `Open`, `Transaction`, `User`, `Sign`, `Admin`. use openpgp_card::card_do::ApplicationRelatedData; -use openpgp_card::{OpenPgp, OpenPgpTransaction}; use crate::Card; @@ -19,11 +18,11 @@ impl State for User<'_, '_> {} impl State for Sign<'_, '_> {} impl State for Admin<'_, '_> {} -/// State of an OpenPGP card in its base state, no transaction has been started. +/// An OpenPGP card in its base state, no transaction has been started. /// /// A transaction can be started on the card, in this state. pub struct Open { - pub(crate) pgp: OpenPgp, + pub(crate) pgp: openpgp_card::Card, } /// State of an OpenPGP card once a transaction has been started. @@ -34,7 +33,7 @@ pub struct Open { /// /// (Note that a factory-reset can be performed in this base state.) pub struct Transaction<'a> { - pub(crate) opt: OpenPgpTransaction<'a>, + pub(crate) opt: openpgp_card::Transaction<'a>, // Cache of "application related data". // @@ -43,18 +42,41 @@ pub struct Transaction<'a> { // // This field should probably be an Option<> that gets invalidated when appropriate and // re-fetched lazily. - pub(crate) ard: ApplicationRelatedData, + ard: ApplicationRelatedData, // verify status of pw1 + // FIXME: this mechanism needs more thought pub(crate) pw1: bool, // verify status of pw1 for signing + // FIXME: this mechanism needs more thought pub(crate) pw1_sign: bool, // verify status of pw3 + // FIXME: this mechanism needs more thought pub(crate) pw3: bool, } +impl<'a> Transaction<'a> { + pub(crate) fn new(opt: openpgp_card::Transaction<'a>, ard: ApplicationRelatedData) -> Self { + Transaction { + opt, + ard, + pw1: false, + pw1_sign: false, + pw3: false, + } + } + + pub(crate) fn ard(&self) -> &ApplicationRelatedData { + &self.ard + } + + pub(crate) fn set_ard(&mut self, ard: ApplicationRelatedData) { + self.ard = ard + } +} + /// State of an OpenPGP card after successfully verifying the User PIN /// (this verification allow user operations other than signing). /// diff --git a/openpgp-card-sequoia/src/types.rs b/openpgp-card-sequoia/src/types.rs index dfe0764..589ea19 100644 --- a/openpgp-card-sequoia/src/types.rs +++ b/openpgp-card-sequoia/src/types.rs @@ -3,7 +3,7 @@ //! Re-exports of openpgp-card types to enable standalone-use of openpgp-card-sequoia. -pub use openpgp_card::algorithm::{Algo, AlgoSimple, Curve}; -pub use openpgp_card::card_do::{Sex, TouchPolicy}; +pub use openpgp_card::algorithm::{AlgoSimple, AlgorithmAttributes, Curve}; +pub use openpgp_card::card_do::{Fingerprint, Sex, TouchPolicy}; pub use openpgp_card::crypto_data::{EccType, PublicKeyMaterial}; -pub use openpgp_card::{CardBackend, Error, KeyType, StatusBytes}; +pub use openpgp_card::{Error, KeyType, StatusBytes}; diff --git a/openpgp-card-sequoia/src/util.rs b/openpgp-card-sequoia/src/util.rs index 5583187..2c19caf 100644 --- a/openpgp-card-sequoia/src/util.rs +++ b/openpgp-card-sequoia/src/util.rs @@ -6,8 +6,8 @@ use std::convert::TryFrom; use std::convert::TryInto; -use anyhow::{anyhow, Result}; -use openpgp_card::algorithm::{Algo, Curve}; +use anyhow::Result; +use openpgp_card::algorithm::{AlgorithmAttributes, Curve}; use openpgp_card::card_do::{Fingerprint, KeyGenerationTime}; use openpgp_card::crypto_data::{CardUploadableKey, PublicKeyMaterial}; use openpgp_card::{Error, KeyType}; @@ -42,7 +42,7 @@ pub fn make_cert( key_sig: PublicKey, key_dec: Option, key_aut: Option, - pw1: Option<&[u8]>, + pw1: Option<&str>, pinpad_prompt: &dyn Fn(), touch_prompt: &(dyn Fn() + Send + Sync), user_ids: &[String], @@ -54,20 +54,18 @@ pub fn make_cert( |op: &mut dyn Fn(&mut dyn sequoia_openpgp::crypto::Signer) -> Result| { // Allow signing on the card if let Some(pw1) = pw1 { - open.verify_user_for_signing(pw1)?; + open.verify_user_signing_pin(pw1)?; } else { - open.verify_user_for_signing_pinpad(pinpad_prompt)?; + open.verify_user_signing_pinpad(pinpad_prompt)?; } - if let Some(mut sign) = open.signing_card() { - // Card-backed signer for bindings - let mut card_signer = sign.signer_from_public(key_sig.clone(), touch_prompt); + let mut sign = open.to_signing_card(None)?; - // Make signature, return it - let s = op(&mut card_signer)?; - Ok(s) - } else { - Err(anyhow!("Failed to open card for signing")) - } + // Card-backed signer for bindings + let mut card_signer = sign.signer_from_public(key_sig.clone(), touch_prompt); + + // Make signature, return it + let s = op(&mut card_signer)?; + Ok::(s) }; // 1) use the signing key as primary key @@ -237,7 +235,7 @@ pub fn public_key_material_to_key( } PublicKeyMaterial::E(ecc) => { let algo = ecc.algo().clone(); // FIXME? - if let Algo::Ecc(algo_ecc) = algo { + if let AlgorithmAttributes::Ecc(algo_ecc) = algo { let curve = match algo_ecc.curve() { Curve::NistP256r1 => sequoia_openpgp::types::Curve::NistP256, Curve::NistP384r1 => sequoia_openpgp::types::Curve::NistP384, @@ -249,7 +247,7 @@ pub fn public_key_material_to_key( match key_type { KeyType::Authentication | KeyType::Signing => { - if algo_ecc.curve() == Curve::Ed25519 { + if algo_ecc.curve() == &Curve::Ed25519 { // EdDSA let k4 = Key4::import_public_ed25519(ecc.data(), time).map_err(|e| { @@ -279,7 +277,7 @@ pub fn public_key_material_to_key( } } KeyType::Decryption => { - if algo_ecc.curve() == Curve::Cv25519 { + if algo_ecc.curve() == &Curve::Cv25519 { // EdDSA let k4 = Key4::import_public_cv25519(ecc.data(), hash, sym, time) .map_err(|e| { @@ -323,16 +321,16 @@ pub fn public_key_material_to_key( /// Mapping function to get a fingerprint from "PublicKeyMaterial + /// timestamp + KeyType" (intended for use with `CardApp.generate_key()`). /// -/// For ECC decryption keys, `hash` and `sym` can be optionally specified. +/// For ECC decryption keys, `hash` and `sym` are set by Sequoia. +/// This fingerprint calculation is based on the parameters that get +/// selected in [`public_key_material_to_key`]. pub(crate) fn public_to_fingerprint( pkm: &PublicKeyMaterial, - time: &KeyGenerationTime, + time: KeyGenerationTime, kt: KeyType, - hash: Option, - sym: Option, ) -> Result { // Transform PublicKeyMaterial into a Sequoia Key - let key = public_key_material_to_key(pkm, kt, time, hash, sym)?; + let key = public_key_material_to_key(pkm, kt, &time, None, None)?; // Get fingerprint from the Sequoia Key let fp = key.fingerprint(); diff --git a/openpgp-card/Cargo.toml b/openpgp-card/Cargo.toml index 1d46391..933f098 100644 --- a/openpgp-card/Cargo.toml +++ b/openpgp-card/Cargo.toml @@ -1,18 +1,18 @@ -# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-License-Identifier: MIT OR Apache-2.0 [package] name = "openpgp-card" description = "A client implementation for the OpenPGP card specification" license = "MIT OR Apache-2.0" -version = "0.3.3" +version = "0.4.1" authors = ["Heiko Schaefer "] edition = "2018" repository = "https://gitlab.com/openpgp-card/openpgp-card" documentation = "https://docs.rs/crate/openpgp-card" [dependencies] -blanket = "0.2.0" +card-backend = { path = "../card-backend", version = "0.2" } nom = "7" hex-slice = "0.1" thiserror = "1" @@ -20,4 +20,4 @@ log = "0.4" chrono = "0.4" [dev-dependencies] -hex-literal = "0.3" +hex-literal = "0.4" diff --git a/openpgp-card/README.md b/openpgp-card/README.md index 173946a..7e3a777 100644 --- a/openpgp-card/README.md +++ b/openpgp-card/README.md @@ -1,5 +1,5 @@ @@ -19,8 +19,8 @@ specification. This crate doesn't contain code to talk to cards. Implementations of the traits `CardBackend`/`CardTransaction` need to be provided for access to cards. -The crates [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc) -and the experimental crate [openpgp-card-scdc](https://crates.io/crates/openpgp-card-scdc) +The crates [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc) +and the experimental crate [card-backend-scdc](https://crates.io/crates/card-backend-scdc) provide implementations of these traits for use with this crate. **Sequoia PGP wrapper** diff --git a/openpgp-card/src/algorithm.rs b/openpgp-card/src/algorithm.rs index 47f6d7e..aa1aeae 100644 --- a/openpgp-card/src/algorithm.rs +++ b/openpgp-card/src/algorithm.rs @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 -//! Data structures that define OpenPGP algorithms. +//! Data structures that specify algorithms to use on an OpenPGP card. //! -//! [`Algo`] and its components model "Algorithm Attributes" as described in -//! the OpenPGP card specification. +//! [`AlgorithmAttributes`] (and its components) model "Algorithm Attributes" +//! as described in the OpenPGP card specification. //! //! [`AlgoSimple`] offers a shorthand for specifying an algorithm, //! specifically for key generation on the card. @@ -12,9 +12,8 @@ use std::convert::TryFrom; use std::fmt; -use crate::card_do::ApplicationRelatedData; use crate::crypto_data::EccType; -use crate::{keys, oid, Error, KeyType}; +use crate::{keys, oid, Error, KeyType, Transaction}; /// A shorthand way to specify algorithms (e.g. for key generation). #[derive(Clone, Copy, Debug)] @@ -31,7 +30,7 @@ pub enum AlgoSimple { } impl TryFrom<&str> for AlgoSimple { - type Error = crate::Error; + type Error = Error; fn try_from(algo: &str) -> Result { use AlgoSimple::*; @@ -51,6 +50,25 @@ impl TryFrom<&str> for AlgoSimple { } impl AlgoSimple { + /// Get algorithm attributes for slot `key_type` from this AlgoSimple. + /// + /// AlgoSimple doesn't specify card specific details (such as bit-size + /// of e for RSA, and import format). + /// This function determines these values based on information from the + /// card behind `tx`. + pub fn matching_algorithm_attributes( + &self, + tx: &mut Transaction, + key_type: KeyType, + ) -> Result { + let ard = tx.application_related_data()?; + let algorithm_attributes = ard.algorithm_attributes(key_type)?; + + let algo_info = tx.algorithm_information_cached().ok().flatten(); + + self.determine_algo_attributes(key_type, algorithm_attributes, algo_info) + } + /// Get corresponding EccType by KeyType (except for Curve25519) fn ecc_type(key_type: KeyType) -> EccType { match key_type { @@ -77,39 +95,61 @@ impl AlgoSimple { /// Return the appropriate Algo for this AlgoSimple. /// - /// This mapping differs between cards, based on `ard` and `algo_info` - /// (e.g. the exact Algo variant can have a different size for e, in RSA; - /// also, the import_format can differ). - pub(crate) fn determine_algo( + /// This mapping depends on the actual card in use + /// (e.g.: the size of "e", in RSA can differ; + /// or a different `import_format` can be selected). + /// + /// These card-specific settings are derived from `algorithm_attributes` and `algo_info`. + pub(crate) fn determine_algo_attributes( &self, key_type: KeyType, - ard: &ApplicationRelatedData, - algo_info: Option, - ) -> Result { + algorithm_attributes: AlgorithmAttributes, + algo_info: Option, + ) -> Result { let algo = match self { - Self::RSA1k => Algo::Rsa(keys::determine_rsa_attrs(1024, key_type, ard, algo_info)?), - Self::RSA2k => Algo::Rsa(keys::determine_rsa_attrs(2048, key_type, ard, algo_info)?), - Self::RSA3k => Algo::Rsa(keys::determine_rsa_attrs(3072, key_type, ard, algo_info)?), - Self::RSA4k => Algo::Rsa(keys::determine_rsa_attrs(4096, key_type, ard, algo_info)?), - Self::NIST256 => Algo::Ecc(keys::determine_ecc_attrs( + Self::RSA1k => AlgorithmAttributes::Rsa(keys::determine_rsa_attrs( + 1024, + key_type, + algorithm_attributes, + algo_info, + )?), + Self::RSA2k => AlgorithmAttributes::Rsa(keys::determine_rsa_attrs( + 2048, + key_type, + algorithm_attributes, + algo_info, + )?), + Self::RSA3k => AlgorithmAttributes::Rsa(keys::determine_rsa_attrs( + 3072, + key_type, + algorithm_attributes, + algo_info, + )?), + Self::RSA4k => AlgorithmAttributes::Rsa(keys::determine_rsa_attrs( + 4096, + key_type, + algorithm_attributes, + algo_info, + )?), + Self::NIST256 => AlgorithmAttributes::Ecc(keys::determine_ecc_attrs( Curve::NistP256r1.oid(), Self::ecc_type(key_type), key_type, algo_info, )?), - Self::NIST384 => Algo::Ecc(keys::determine_ecc_attrs( + Self::NIST384 => AlgorithmAttributes::Ecc(keys::determine_ecc_attrs( Curve::NistP384r1.oid(), Self::ecc_type(key_type), key_type, algo_info, )?), - Self::NIST521 => Algo::Ecc(keys::determine_ecc_attrs( + Self::NIST521 => AlgorithmAttributes::Ecc(keys::determine_ecc_attrs( Curve::NistP521r1.oid(), Self::ecc_type(key_type), key_type, algo_info, )?), - Self::Curve25519 => Algo::Ecc(keys::determine_ecc_attrs( + Self::Curve25519 => AlgorithmAttributes::Ecc(keys::determine_ecc_attrs( Self::curve_for_25519(key_type).oid(), Self::ecc_type_25519(key_type), key_type, @@ -121,17 +161,19 @@ impl AlgoSimple { } } -/// 4.4.3.11 Algorithm Information +/// "Algorithm Information" enumerates which algorithms the current card supports +/// [Spec section 4.4.3.11] /// -/// Modern cards (since OpenPGP card v3.4) provide a list of supported -/// algorithms for each key type. This list specifies which "Algorithm -/// Attributes" can be set for key generation or key import. +/// Modern OpenPGP cards (starting with version v3.4) provide a list of +/// algorithms they support for each key slot. +/// The Algorithm Information list specifies which [`AlgorithmAttributes`] +/// can be used on that card (for key generation or key import). #[derive(Debug, Clone, Eq, PartialEq)] -pub struct AlgoInfo(pub(crate) Vec<(KeyType, Algo)>); +pub struct AlgorithmInformation(pub(crate) Vec<(KeyType, AlgorithmAttributes)>); -/// 4.4.3.9 Algorithm Attributes +/// Algorithm Attributes [Spec section 4.4.3.9] /// -/// An `Algo` describes the algorithm settings for a key on the card. +/// [`AlgorithmAttributes`] describes the algorithm settings for a key on the card. /// /// This setting specifies the data format of: /// - Key import @@ -139,13 +181,13 @@ pub struct AlgoInfo(pub(crate) Vec<(KeyType, Algo)>); /// - Export of public key data from the card (e.g. after key generation) #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] -pub enum Algo { - Rsa(RsaAttrs), - Ecc(EccAttrs), +pub enum AlgorithmAttributes { + Rsa(RsaAttributes), + Ecc(EccAttributes), Unknown(Vec), } -impl fmt::Display for Algo { +impl fmt::Display for AlgorithmAttributes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Rsa(rsa) => { @@ -181,19 +223,19 @@ impl fmt::Display for Algo { } } -impl Algo { +impl AlgorithmAttributes { /// Get a DO representation of the Algo, for setting algorithm /// attributes on the card. pub(crate) fn to_data_object(&self) -> Result, Error> { match self { - Algo::Rsa(rsa) => Self::rsa_algo_attrs(rsa), - Algo::Ecc(ecc) => Self::ecc_algo_attrs(ecc.oid(), ecc.ecc_type()), + AlgorithmAttributes::Rsa(rsa) => Self::rsa_algo_attrs(rsa), + AlgorithmAttributes::Ecc(ecc) => Self::ecc_algo_attrs(ecc.oid(), ecc.ecc_type()), _ => Err(Error::UnsupportedAlgo(format!("Unexpected Algo {self:?}"))), } } /// Helper: generate `data` for algorithm attributes with RSA - fn rsa_algo_attrs(algo_attrs: &RsaAttrs) -> Result, Error> { + fn rsa_algo_attrs(algo_attrs: &RsaAttributes) -> Result, Error> { // Algorithm ID (01 = RSA (Encrypt or Sign)) let mut algo_attributes = vec![0x01]; @@ -225,17 +267,17 @@ impl Algo { } } -/// RSA specific attributes of [`Algo`] ("Algorithm Attributes") +/// RSA specific attributes of [`AlgorithmAttributes`] #[derive(Debug, Clone, Eq, PartialEq)] -pub struct RsaAttrs { +pub struct RsaAttributes { len_n: u16, len_e: u16, import_format: u8, } -impl RsaAttrs { +impl RsaAttributes { pub fn new(len_n: u16, len_e: u16, import_format: u8) -> Self { - RsaAttrs { + Self { len_n, len_e, import_format, @@ -255,15 +297,15 @@ impl RsaAttrs { } } -/// ECC specific attributes of [`Algo`] ("Algorithm Attributes") +/// ECC specific attributes of [`AlgorithmAttributes`] #[derive(Debug, Clone, Eq, PartialEq)] -pub struct EccAttrs { +pub struct EccAttributes { ecc_type: EccType, curve: Curve, import_format: Option, } -impl EccAttrs { +impl EccAttributes { pub fn new(ecc_type: EccType, curve: Curve, import_format: Option) -> Self { Self { ecc_type, @@ -276,8 +318,8 @@ impl EccAttrs { self.ecc_type } - pub fn curve(&self) -> Curve { - self.curve + pub fn curve(&self) -> &Curve { + &self.curve } pub fn oid(&self) -> &[u8] { @@ -290,7 +332,7 @@ impl EccAttrs { } /// Enum for naming ECC curves, and mapping them to/from their OIDs. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub enum Curve { NistP256r1, @@ -304,6 +346,8 @@ pub enum Curve { Cv25519, Ed448, X448, + + Unknown(Vec), } impl Curve { @@ -321,12 +365,14 @@ impl Curve { Cv25519 => oid::CV25519, Ed448 => oid::ED448, X448 => oid::X448, + + Unknown(oid) => oid, } } } impl TryFrom<&[u8]> for Curve { - type Error = crate::Error; + type Error = Error; fn try_from(oid: &[u8]) -> Result { use Curve::*; @@ -348,7 +394,7 @@ impl TryFrom<&[u8]> for Curve { oid::ED448 => Ed448, oid::X448 => X448, - _ => return Err(Error::ParseError(format!("Unknown curve OID {oid:?}"))), + _ => Unknown(oid.to_vec()), }; Ok(curve) diff --git a/openpgp-card/src/apdu.rs b/openpgp-card/src/apdu.rs index e14c4f0..5b85bb4 100644 --- a/openpgp-card/src/apdu.rs +++ b/openpgp-card/src/apdu.rs @@ -5,14 +5,16 @@ //! Commands and responses to commands pub(crate) mod command; -pub(crate) mod commands; pub mod response; use std::convert::TryFrom; -use crate::apdu::command::Expect; -use crate::apdu::{command::Command, response::RawResponse}; -use crate::{CardTransaction, Error, StatusBytes}; +use card_backend::{CardCaps, CardTransaction}; + +use crate::apdu::command::{Command, Expect}; +use crate::apdu::response::RawResponse; +use crate::commands; +use crate::{Error, StatusBytes}; /// "Maximum amount of bytes in a short APDU command or response" (from pcsc) const MAX_BUFFER_SIZE: usize = 264; @@ -24,16 +26,18 @@ const MAX_BUFFER_SIZE: usize = 264; pub(crate) fn send_command( card_tx: &mut C, cmd: Command, + card_caps: Option, expect_reply: bool, ) -> Result where C: CardTransaction + ?Sized, { - log::debug!(" -> full APDU command: {:x?}", cmd); + log::debug!(" -> full APDU command: {:02x?}", cmd); let mut resp = RawResponse::try_from(send_command_low_level( card_tx, cmd.clone(), + card_caps, if expect_reply { Expect::Some } else { @@ -42,7 +46,12 @@ where )?)?; if let StatusBytes::UnknownStatus(0x6c, size) = resp.status() { - resp = RawResponse::try_from(send_command_low_level(card_tx, cmd, Expect::Short(size))?)?; + resp = RawResponse::try_from(send_command_low_level( + card_tx, + cmd, + card_caps, + Expect::Short(size), + )?)?; } while let StatusBytes::OkBytesAvailable(bytes) = resp.status() { @@ -53,6 +62,7 @@ where let next = RawResponse::try_from(send_command_low_level( card_tx, commands::get_response(), + card_caps, Expect::Short(bytes), )?)?; @@ -69,7 +79,7 @@ where } log::debug!( - " <- APDU response [len {}]: {:x?}", + " <- APDU response [len {}]: {:02x?}", resp.raw_data().len(), resp ); @@ -85,20 +95,21 @@ where fn send_command_low_level( card_tx: &mut C, cmd: Command, + card_caps: Option, expect_response: Expect, ) -> Result, Error> where C: CardTransaction + ?Sized, { - let (ext_support, chaining_support, mut max_cmd_bytes, max_rsp_bytes) = - if let Some(caps) = card_tx.card_caps() { + let (ext_support, chaining_support, max_cmd_bytes, max_rsp_bytes) = + if let Some(caps) = card_caps { log::trace!("found card caps data!"); ( - caps.ext_support, - caps.chaining_support, - caps.max_cmd_bytes as usize, - caps.max_rsp_bytes as usize, + caps.ext_support(), + caps.chaining_support(), + caps.max_cmd_bytes() as usize, + caps.max_rsp_bytes() as usize, ) } else { log::trace!("found NO card caps data!"); @@ -107,15 +118,6 @@ where (false, false, 255, 255) }; - // If the CardTransaction implementation has an inherent limit for the cmd - // size, take that limit into account. - // (E.g. when using scdaemon as a CardTransaction backend, there is a - // limitation to 1000 bytes length for Assuan commands, which - // translates to maximum command length of a bit under 500 bytes) - if let Some(max_card_cmd_bytes) = card_tx.max_cmd_len() { - max_cmd_bytes = usize::min(max_cmd_bytes, max_card_cmd_bytes); - } - log::trace!( "ext le/lc {}, chaining {}, max cmd {}, max rsp {}", ext_support, @@ -147,8 +149,10 @@ where log::trace!("chained command mode"); + let cmd_chunk_size = if ext_support { max_cmd_bytes } else { 255 }; + // Break up payload into chunks that fit into one command, each - let chunks: Vec<_> = cmd.data().chunks(max_cmd_bytes).collect(); + let chunks: Vec<_> = cmd.data().chunks(cmd_chunk_size).collect(); for (i, d) in chunks.iter().enumerate() { let last = i == chunks.len() - 1; @@ -158,11 +162,11 @@ where let serialized = partial.serialize(ext_len, expect_response)?; - log::trace!(" -> chained APDU command: {:x?}", &serialized); + log::trace!(" -> chained APDU command: {:02x?}", &serialized); let resp = card_tx.transmit(&serialized, buf_size)?; - log::trace!(" <- APDU response: {:x?}", &resp); + log::trace!(" <- APDU response: {:02x?}", &resp); if resp.len() < 2 { return Err(Error::ResponseLength(resp.len())); @@ -200,11 +204,11 @@ where return Err(Error::CommandTooLong(serialized.len())); } - log::trace!(" -> APDU command: {:x?}", &serialized); + log::trace!(" -> APDU command: {:02x?}", &serialized); let resp = card_tx.transmit(&serialized, buf_size)?; - log::trace!(" <- APDU response: {:x?}", resp); + log::trace!(" <- APDU response: {:02x?}", resp); Ok(resp) } diff --git a/openpgp-card/src/apdu/command.rs b/openpgp-card/src/apdu/command.rs index cdb6497..1a7c447 100644 --- a/openpgp-card/src/apdu/command.rs +++ b/openpgp-card/src/apdu/command.rs @@ -81,7 +81,7 @@ impl Command { let nc = self.data.len() as u16; let mut buf = vec![self.cla, self.ins, self.p1, self.p2]; - buf.extend(Self::make_lc(nc, ext_len)); + buf.extend(Self::make_lc(nc, ext_len)?); buf.extend(&self.data); buf.extend(Self::make_le(nc, ext_len, expect_response)); @@ -89,21 +89,19 @@ impl Command { } /// Encode len for Lc field - fn make_lc(len: u16, ext_len: bool) -> Vec { - if !ext_len { - assert!( - len <= 0xff, - "{}", - "unexpected: len = {len:x?}, but ext says Short" - ); + fn make_lc(len: u16, ext_len: bool) -> Result, crate::Error> { + if !ext_len && len > 0xff { + return Err(crate::Error::InternalError(format!( + "Command len = {len:x?}, but extended length is unsupported by backend" + ))); } if len == 0 { - vec![] + Ok(vec![]) } else if !ext_len { - vec![len as u8] + Ok(vec![len as u8]) } else { - vec![0, (len >> 8) as u8, (len & 255) as u8] + Ok(vec![0, (len >> 8) as u8, (len & 255) as u8]) } } diff --git a/openpgp-card/src/card_do.rs b/openpgp-card/src/card_do.rs index 88ddae8..a3448e0 100644 --- a/openpgp-card/src/card_do.rs +++ b/openpgp-card/src/card_do.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! OpenPGP card data objects (DO) @@ -9,7 +9,8 @@ use std::time::{Duration, UNIX_EPOCH}; use chrono::{DateTime, Utc}; -use crate::{algorithm::Algo, tlv::Tlv, Error, KeySet, KeyType, Tags}; +use crate::tags::Tags; +use crate::{algorithm::AlgorithmAttributes, tlv::Tlv, Error, KeyType}; mod algo_attrs; mod algo_info; @@ -22,7 +23,7 @@ mod historical; mod key_generation_times; mod pw_status; -/// 4.4.3.1 Application Related Data +/// Application Related Data [Spec section 4.4.3.1] /// /// The "application related data" DO contains a set of DOs. /// This struct offers read access to these DOs. @@ -76,11 +77,13 @@ impl ApplicationRelatedData { #[allow(dead_code)] fn general_feature_management() -> Option { + // FIXME unimplemented!() } #[allow(dead_code)] fn discretionary_data_objects() { + // FIXME unimplemented!() } @@ -105,11 +108,11 @@ impl ApplicationRelatedData { } /// Get algorithm attributes (for each key type) - pub fn algorithm_attributes(&self, key_type: KeyType) -> Result { + pub fn algorithm_attributes(&self, key_type: KeyType) -> Result { let aa = self.0.find(key_type.algorithm_tag()); if let Some(aa) = aa { - Algo::try_from(&aa.serialize()[..]) + AlgorithmAttributes::try_from(&aa.serialize()[..]) } else { Err(Error::NotFound(format!( "Failed to get algorithm attributes for {key_type:?}." @@ -192,7 +195,7 @@ impl ApplicationRelatedData { Ok(ki.map(|v| v.serialize().into())) } - pub fn uif_pso_cds(&self) -> Result, Error> { + pub fn uif_pso_cds(&self) -> Result, Error> { let uif = self.0.find(Tags::UifSig); match uif { @@ -201,7 +204,7 @@ impl ApplicationRelatedData { } } - pub fn uif_pso_dec(&self) -> Result, Error> { + pub fn uif_pso_dec(&self) -> Result, Error> { let uif = self.0.find(Tags::UifDec); match uif { @@ -210,7 +213,7 @@ impl ApplicationRelatedData { } } - pub fn uif_pso_aut(&self) -> Result, Error> { + pub fn uif_pso_aut(&self) -> Result, Error> { let uif = self.0.find(Tags::UifAuth); match uif { @@ -235,15 +238,19 @@ impl ApplicationRelatedData { } /// Get Attestation key algorithm attributes. - pub fn attestation_key_algorithm_attributes(&mut self) -> Result, Error> { + pub fn attestation_key_algorithm_attributes( + &mut self, + ) -> Result, Error> { match self.0.find(Tags::AlgorithmAttributesAttestation) { None => Ok(None), - Some(data) => Ok(Some(Algo::try_from(data.serialize().as_slice())?)), + Some(data) => Ok(Some(AlgorithmAttributes::try_from( + data.serialize().as_slice(), + )?)), } } /// Get Attestation key generation time. - pub fn attestation_key_generation_time(&mut self) -> Result, Error> { + pub fn attestation_key_generation_time(&self) -> Result, Error> { match self.0.find(Tags::GenerationTimeAttestation) { None => Ok(None), Some(data) => { @@ -260,7 +267,7 @@ impl ApplicationRelatedData { } } - pub fn uif_attestation(&self) -> Result, Error> { + pub fn uif_attestation(&self) -> Result, Error> { let uif = self.0.find(Tags::UifAttestation); match uif { @@ -270,7 +277,7 @@ impl ApplicationRelatedData { } } -/// Security support template (see spec pg. 24) +/// Security support template [Spec page 24] #[derive(Debug)] pub struct SecuritySupportTemplate { // Digital signature counter [3 bytes] @@ -284,7 +291,7 @@ impl SecuritySupportTemplate { } } -/// An OpenPGP key generation Time (see spec pg. 24) +/// An OpenPGP key generation Time [Spec page 24] #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct KeyGenerationTime(u32); @@ -305,23 +312,23 @@ impl Display for KeyGenerationTime { } } -/// User Interaction Flag (UIF) (see spec pg. 24) +/// User Interaction Flag [Spec page 24] #[derive(Clone, Copy, Eq, PartialEq, Debug)] -pub struct UIF([u8; 2]); +pub struct UserInteractionFlag([u8; 2]); -impl TryFrom> for UIF { +impl TryFrom> for UserInteractionFlag { type Error = Error; fn try_from(v: Vec) -> Result { if v.len() == 2 { - Ok(UIF(v.try_into().unwrap())) + Ok(UserInteractionFlag(v.try_into().unwrap())) } else { Err(Error::ParseError(format!("Can't get UID from {v:x?}"))) } } } -impl UIF { +impl UserInteractionFlag { pub fn touch_policy(&self) -> TouchPolicy { self.0[0].into() } @@ -339,7 +346,7 @@ impl UIF { } } -impl Display for UIF { +impl Display for UserInteractionFlag { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -418,7 +425,8 @@ impl From for TouchPolicy { } } -/// "additional hardware for user interaction" (see spec 4.1.3.2) +/// Features of "additional hardware for user interaction" [Spec section 4.1.3.2]. +/// (Settings for these features are contained in [`UserInteractionFlag`]) pub struct Features(u8); impl From for Features { @@ -460,7 +468,7 @@ impl Display for Features { } } -/// 4.4.3.8 Key Information +/// Key Information [Spec section 4.4.3.8] pub struct KeyInformation(Vec); impl From> for KeyInformation { @@ -546,8 +554,8 @@ impl Display for KeyInformation { } } -/// KeyStatus is contained in `KeyInformation`. It encodes if key material on a card was imported -/// or generated on the card. +/// KeyStatus is contained in [`KeyInformation`]. +/// It encodes if key material on a card was imported or generated on the card. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum KeyStatus { @@ -579,8 +587,8 @@ impl Display for KeyStatus { } } -/// 4.2.1 Application Identifier (AID) -#[derive(Debug, Eq, PartialEq)] +/// Application Identifier (AID) [Spec section 4.2.1] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct ApplicationIdentifier { application: u8, version: u16, @@ -598,8 +606,8 @@ impl Display for ApplicationIdentifier { } } -/// 6 Historical Bytes -#[derive(Debug, PartialEq, Eq)] +/// Historical Bytes [Spec chapter 6] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct HistoricalBytes { /// category indicator byte cib: u8, @@ -614,8 +622,8 @@ pub struct HistoricalBytes { sib: u8, } -/// Card Capabilities (see 6 Historical Bytes) -#[derive(Debug, PartialEq, Eq)] +/// Card Capabilities [Spec chapter 6 (Historical Bytes)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CardCapabilities { command_chaining: bool, extended_lc_le: bool, @@ -638,8 +646,8 @@ impl Display for CardCapabilities { } } -/// Card service data (see 6 Historical Bytes) -#[derive(Debug, PartialEq, Eq)] +/// Card service data [Spec chapter 6 (Historical Bytes)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CardServiceData { select_by_full_df_name: bool, // Application Selection by full DF name (AID) select_by_partial_df_name: bool, // Application Selection by partial DF name @@ -685,8 +693,8 @@ impl Display for CardServiceData { } } -/// 4.4.3.7 Extended Capabilities -#[derive(Debug, Eq, PartialEq)] +/// Extended Capabilities [Spec section 4.4.3.7] +#[derive(Debug, Eq, Clone, Copy, PartialEq)] pub struct ExtendedCapabilities { secure_messaging: bool, get_challenge: bool, @@ -775,8 +783,8 @@ impl Display for ExtendedCapabilities { } } -/// 4.1.3.1 Extended length information -#[derive(Debug, Eq, PartialEq)] +/// Extended length information [Spec section 4.1.3.1] +#[derive(Debug, Eq, Clone, Copy, PartialEq)] pub struct ExtendedLengthInfo { max_command_bytes: u16, max_response_bytes: u16, @@ -790,7 +798,7 @@ impl Display for ExtendedLengthInfo { } } -/// Cardholder Related Data (see spec pg. 22) +/// Cardholder Related Data [Spec page 22] #[derive(Debug, PartialEq, Eq)] pub struct CardholderRelatedData { name: Option>, @@ -815,7 +823,8 @@ impl Display for CardholderRelatedData { } } -/// 4.4.3.5 Sex +/// Sex [Spec section 4.4.3.5]. +/// The Sex setting is accessible via [`CardholderRelatedData`]. /// /// Encoded in accordance with #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -863,7 +872,8 @@ impl From for Sex { } } -/// Individual language for Language Preferences (4.4.3.4), accessible via `CardholderRelatedData`. +/// Individual language for Language Preferences [Spec section 4.4.3.4]. +/// Language preferences are accessible via [`CardholderRelatedData`]. /// /// Encoded according to #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -918,7 +928,7 @@ impl From<&[u8; 2]> for Lang { } } -/// PW status Bytes (see spec page 23) +/// PW status Bytes [Spec page 23] #[derive(Debug, PartialEq, Eq)] pub struct PWStatusBytes { pub(crate) pw1_cds_valid_once: bool, @@ -988,7 +998,7 @@ impl PWStatusBytes { } } -/// Fingerprint (see spec pg. 23) +/// OpenPGP Fingerprint for a key slot [Spec page 23] #[derive(Clone, Eq, PartialEq)] pub struct Fingerprint([u8; 20]); @@ -1022,3 +1032,35 @@ pub(crate) fn complete(result: nom::IResult<&[u8], O>) -> Result { ))) } } + +/// A KeySet binds together a triple of information about each Key slot on a card +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeySet { + signature: Option, + decryption: Option, + authentication: Option, +} + +impl From<(Option, Option, Option)> for KeySet { + fn from(tuple: (Option, Option, Option)) -> Self { + Self { + signature: tuple.0, + decryption: tuple.1, + authentication: tuple.2, + } + } +} + +impl KeySet { + pub fn signature(&self) -> Option<&T> { + self.signature.as_ref() + } + + pub fn decryption(&self) -> Option<&T> { + self.decryption.as_ref() + } + + pub fn authentication(&self) -> Option<&T> { + self.authentication.as_ref() + } +} diff --git a/openpgp-card/src/card_do/algo_attrs.rs b/openpgp-card/src/card_do/algo_attrs.rs index f4e1f99..5750a50 100644 --- a/openpgp-card/src/card_do/algo_attrs.rs +++ b/openpgp-card/src/card_do/algo_attrs.rs @@ -10,7 +10,7 @@ use nom::bytes::complete::tag; use nom::combinator::map; use nom::{branch, bytes::complete as bytes, number::complete as number}; -use crate::algorithm::{Algo, Curve, EccAttrs, RsaAttrs}; +use crate::algorithm::{AlgorithmAttributes, Curve, EccAttributes, RsaAttributes}; use crate::card_do::complete; use crate::crypto_data::EccType; @@ -80,14 +80,17 @@ fn parse_oid(input: &[u8]) -> nom::IResult<&[u8], Curve> { ))(input) } -fn parse_rsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { +fn parse_rsa(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { let (input, _) = bytes::tag([0x01])(input)?; let (input, len_n) = number::be_u16(input)?; let (input, len_e) = number::be_u16(input)?; let (input, import_format) = number::u8(input)?; - Ok((input, Algo::Rsa(RsaAttrs::new(len_n, len_e, import_format)))) + Ok(( + input, + AlgorithmAttributes::Rsa(RsaAttributes::new(len_n, len_e, import_format)), + )) } fn parse_import_format(input: &[u8]) -> nom::IResult<&[u8], Option> { @@ -99,7 +102,7 @@ fn default_import_format(input: &[u8]) -> nom::IResult<&[u8], Option> { Ok((input, None)) } -fn parse_ecdh(input: &[u8]) -> nom::IResult<&[u8], Algo> { +fn parse_ecdh(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { let (input, _) = bytes::tag([0x12])(input)?; let (input, curve) = parse_oid(input)?; @@ -107,11 +110,11 @@ fn parse_ecdh(input: &[u8]) -> nom::IResult<&[u8], Algo> { Ok(( input, - Algo::Ecc(EccAttrs::new(EccType::ECDH, curve, import_format)), + AlgorithmAttributes::Ecc(EccAttributes::new(EccType::ECDH, curve, import_format)), )) } -fn parse_ecdsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { +fn parse_ecdsa(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { let (input, _) = bytes::tag([0x13])(input)?; let (input, curve) = parse_oid(input)?; @@ -119,11 +122,11 @@ fn parse_ecdsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { Ok(( input, - Algo::Ecc(EccAttrs::new(EccType::ECDSA, curve, import_format)), + AlgorithmAttributes::Ecc(EccAttributes::new(EccType::ECDSA, curve, import_format)), )) } -fn parse_eddsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { +fn parse_eddsa(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { let (input, _) = bytes::tag([0x16])(input)?; let (input, curve) = parse_oid(input)?; @@ -131,15 +134,15 @@ fn parse_eddsa(input: &[u8]) -> nom::IResult<&[u8], Algo> { Ok(( input, - Algo::Ecc(EccAttrs::new(EccType::EdDSA, curve, import_format)), + AlgorithmAttributes::Ecc(EccAttributes::new(EccType::EdDSA, curve, import_format)), )) } -pub(crate) fn parse(input: &[u8]) -> nom::IResult<&[u8], Algo> { +pub(crate) fn parse(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { branch::alt((parse_rsa, parse_ecdsa, parse_eddsa, parse_ecdh))(input) } -impl TryFrom<&[u8]> for Algo { +impl TryFrom<&[u8]> for AlgorithmAttributes { type Error = crate::Error; fn try_from(data: &[u8]) -> Result { diff --git a/openpgp-card/src/card_do/algo_info.rs b/openpgp-card/src/card_do/algo_info.rs index 1c90724..268efac 100644 --- a/openpgp-card/src/card_do/algo_info.rs +++ b/openpgp-card/src/card_do/algo_info.rs @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 -//! 4.4.3.11 Algorithm Information +//! Algorithm Information [Spec section 4.4.3.11] use std::convert::TryFrom; use std::fmt; @@ -10,12 +10,12 @@ use nom::branch::alt; use nom::combinator::map; use nom::{branch, bytes::complete as bytes, combinator, multi, sequence}; -use crate::algorithm::{Algo, AlgoInfo}; +use crate::algorithm::{AlgorithmAttributes, AlgorithmInformation}; use crate::card_do::{algo_attrs, complete}; use crate::KeyType; -impl AlgoInfo { - pub fn filter_by_keytype(&self, kt: KeyType) -> Vec<&Algo> { +impl AlgorithmInformation { + pub fn for_keytype(&self, kt: KeyType) -> Vec<&AlgorithmAttributes> { self.0 .iter() .filter(|(k, _)| *k == kt) @@ -24,7 +24,7 @@ impl AlgoInfo { } } -impl fmt::Display for AlgoInfo { +impl fmt::Display for AlgorithmInformation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (kt, a) in &self.0 { let kt = match kt { @@ -48,11 +48,11 @@ fn key_type(input: &[u8]) -> nom::IResult<&[u8], KeyType> { ))(input) } -fn unknown(input: &[u8]) -> nom::IResult<&[u8], Algo> { - Ok((&[], Algo::Unknown(input.to_vec()))) +fn unknown(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { + Ok((&[], AlgorithmAttributes::Unknown(input.to_vec()))) } -fn parse_one(input: &[u8]) -> nom::IResult<&[u8], Algo> { +fn parse_one(input: &[u8]) -> nom::IResult<&[u8], AlgorithmAttributes> { let (input, a) = combinator::map( combinator::flat_map(crate::tlv::length::length, bytes::take), |i| alt((combinator::all_consuming(algo_attrs::parse), unknown))(i), @@ -61,18 +61,18 @@ fn parse_one(input: &[u8]) -> nom::IResult<&[u8], Algo> { Ok((input, a?.1)) } -fn parse_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { +fn parse_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, AlgorithmAttributes)>> { multi::many0(sequence::pair(key_type, parse_one))(input) } -fn parse_tl_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { +fn parse_tl_list(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, AlgorithmAttributes)>> { let (input, (_, _, list)) = sequence::tuple((bytes::tag([0xfa]), crate::tlv::length::length, parse_list))(input)?; Ok((input, list)) } -pub(self) fn parse(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { +fn parse(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, AlgorithmAttributes)>> { // Handle two variations of input format: // a) TLV format (e.g. YubiKey 5) // b) Plain list (e.g. Gnuk, FOSS-Store Smartcard 3.4) @@ -85,11 +85,11 @@ pub(self) fn parse(input: &[u8]) -> nom::IResult<&[u8], Vec<(KeyType, Algo)>> { ))(input) } -impl TryFrom<&[u8]> for AlgoInfo { +impl TryFrom<&[u8]> for AlgorithmInformation { type Error = crate::Error; fn try_from(input: &[u8]) -> Result { - Ok(AlgoInfo(complete(parse(input))?)) + Ok(AlgorithmInformation(complete(parse(input))?)) } } @@ -99,7 +99,9 @@ impl TryFrom<&[u8]> for AlgoInfo { mod test { use std::convert::TryFrom; - use crate::algorithm::{Algo::*, AlgoInfo, Curve::*, EccAttrs, RsaAttrs}; + use crate::algorithm::{ + AlgorithmAttributes::*, AlgorithmInformation, Curve::*, EccAttributes, RsaAttributes, + }; use crate::crypto_data::EccType::*; use crate::KeyType::*; @@ -118,26 +120,35 @@ mod test { 0x1, ]; - let ai = AlgoInfo::try_from(&data[..]).unwrap(); + let ai = AlgorithmInformation::try_from(&data[..]).unwrap(); assert_eq!( ai, - AlgoInfo(vec![ - (Signing, Rsa(RsaAttrs::new(2048, 32, 0))), - (Signing, Rsa(RsaAttrs::new(4096, 32, 0))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), - (Signing, Ecc(EccAttrs::new(EdDSA, Ed25519, None))), - (Decryption, Rsa(RsaAttrs::new(2048, 32, 0))), - (Decryption, Rsa(RsaAttrs::new(4096, 32, 0))), - (Decryption, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, Cv25519, None))), - (Authentication, Rsa(RsaAttrs::new(2048, 32, 0))), - (Authentication, Rsa(RsaAttrs::new(4096, 32, 0))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), - (Authentication, Ecc(EccAttrs::new(EdDSA, Ed25519, None))) + AlgorithmInformation(vec![ + (Signing, Rsa(RsaAttributes::new(2048, 32, 0))), + (Signing, Rsa(RsaAttributes::new(4096, 32, 0))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP256r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, Secp256k1, None))), + (Signing, Ecc(EccAttributes::new(EdDSA, Ed25519, None))), + (Decryption, Rsa(RsaAttributes::new(2048, 32, 0))), + (Decryption, Rsa(RsaAttributes::new(4096, 32, 0))), + (Decryption, Ecc(EccAttributes::new(ECDSA, NistP256r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDSA, Secp256k1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, Cv25519, None))), + (Authentication, Rsa(RsaAttributes::new(2048, 32, 0))), + (Authentication, Rsa(RsaAttributes::new(4096, 32, 0))), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, NistP256r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, Secp256k1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(EdDSA, Ed25519, None)) + ) ]) ); } @@ -164,46 +175,73 @@ mod test { 0xa, 0x13, 0x2b, 0x24, 0x3, 0x3, 0x2, 0x8, 0x1, 0x1, 0xd, ]; - let ai = AlgoInfo::try_from(&data[..]).unwrap(); + let ai = AlgorithmInformation::try_from(&data[..]).unwrap(); assert_eq!( ai, - AlgoInfo(vec![ - (Signing, Rsa(RsaAttrs::new(2048, 32, 0))), - (Signing, Rsa(RsaAttrs::new(3072, 32, 0))), - (Signing, Rsa(RsaAttrs::new(4096, 32, 0))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP384r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP521r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP256r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP384r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP512r1, None))), - (Decryption, Rsa(RsaAttrs::new(2048, 32, 0))), - (Decryption, Rsa(RsaAttrs::new(3072, 32, 0))), - (Decryption, Rsa(RsaAttrs::new(4096, 32, 0))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP256r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP384r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP521r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP256r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP384r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP512r1, None))), - (Authentication, Rsa(RsaAttrs::new(2048, 32, 0))), - (Authentication, Rsa(RsaAttrs::new(3072, 32, 0))), - (Authentication, Rsa(RsaAttrs::new(4096, 32, 0))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP384r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP521r1, None))), + AlgorithmInformation(vec![ + (Signing, Rsa(RsaAttributes::new(2048, 32, 0))), + (Signing, Rsa(RsaAttributes::new(3072, 32, 0))), + (Signing, Rsa(RsaAttributes::new(4096, 32, 0))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP256r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP384r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP521r1, None))), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP256r1, None)) + ), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP384r1, None)) + ), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP512r1, None)) + ), + (Decryption, Rsa(RsaAttributes::new(2048, 32, 0))), + (Decryption, Rsa(RsaAttributes::new(3072, 32, 0))), + (Decryption, Rsa(RsaAttributes::new(4096, 32, 0))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP256r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP384r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP521r1, None))), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP256r1, None)) + ), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP384r1, None)) + ), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP512r1, None)) + ), + (Authentication, Rsa(RsaAttributes::new(2048, 32, 0))), + (Authentication, Rsa(RsaAttributes::new(3072, 32, 0))), + (Authentication, Rsa(RsaAttributes::new(4096, 32, 0))), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP256r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP256r1, None)) ), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP384r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP384r1, None)) ), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP512r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP521r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP256r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP384r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP512r1, None)) ) ]) ); @@ -245,77 +283,122 @@ mod test { 0xda, 0xb, 0x16, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x97, 0x55, 0x1, 0x5, 0x1, ]; - let ai = AlgoInfo::try_from(&data[..]).unwrap(); + let ai = AlgorithmInformation::try_from(&data[..]).unwrap(); assert_eq!( ai, - AlgoInfo(vec![ - (Signing, Rsa(RsaAttrs::new(2048, 17, 0))), - (Signing, Rsa(RsaAttrs::new(3072, 17, 0))), - (Signing, Rsa(RsaAttrs::new(4096, 17, 0))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP384r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, NistP521r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP256r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP384r1, None))), - (Signing, Ecc(EccAttrs::new(ECDSA, BrainpoolP512r1, None))), - (Signing, Ecc(EccAttrs::new(EdDSA, Ed25519, None))), - (Signing, Ecc(EccAttrs::new(EdDSA, Cv25519, None))), - (Decryption, Rsa(RsaAttrs::new(2048, 17, 0))), - (Decryption, Rsa(RsaAttrs::new(3072, 17, 0))), - (Decryption, Rsa(RsaAttrs::new(4096, 17, 0))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP256r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP384r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, NistP521r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, Secp256k1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP256r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP384r1, None))), - (Decryption, Ecc(EccAttrs::new(ECDH, BrainpoolP512r1, None))), - (Decryption, Ecc(EccAttrs::new(EdDSA, Ed25519, None))), - (Decryption, Ecc(EccAttrs::new(EdDSA, Cv25519, None))), - (Authentication, Rsa(RsaAttrs::new(2048, 17, 0))), - (Authentication, Rsa(RsaAttrs::new(3072, 17, 0))), - (Authentication, Rsa(RsaAttrs::new(4096, 17, 0))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP384r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, NistP521r1, None))), - (Authentication, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), + AlgorithmInformation(vec![ + (Signing, Rsa(RsaAttributes::new(2048, 17, 0))), + (Signing, Rsa(RsaAttributes::new(3072, 17, 0))), + (Signing, Rsa(RsaAttributes::new(4096, 17, 0))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP256r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP384r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, NistP521r1, None))), + (Signing, Ecc(EccAttributes::new(ECDSA, Secp256k1, None))), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP256r1, None)) + ), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP384r1, None)) + ), + ( + Signing, + Ecc(EccAttributes::new(ECDSA, BrainpoolP512r1, None)) + ), + (Signing, Ecc(EccAttributes::new(EdDSA, Ed25519, None))), + (Signing, Ecc(EccAttributes::new(EdDSA, Cv25519, None))), + (Decryption, Rsa(RsaAttributes::new(2048, 17, 0))), + (Decryption, Rsa(RsaAttributes::new(3072, 17, 0))), + (Decryption, Rsa(RsaAttributes::new(4096, 17, 0))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP256r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP384r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, NistP521r1, None))), + (Decryption, Ecc(EccAttributes::new(ECDH, Secp256k1, None))), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP256r1, None)) + ), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP384r1, None)) + ), + ( + Decryption, + Ecc(EccAttributes::new(ECDH, BrainpoolP512r1, None)) + ), + (Decryption, Ecc(EccAttributes::new(EdDSA, Ed25519, None))), + (Decryption, Ecc(EccAttributes::new(EdDSA, Cv25519, None))), + (Authentication, Rsa(RsaAttributes::new(2048, 17, 0))), + (Authentication, Rsa(RsaAttributes::new(3072, 17, 0))), + (Authentication, Rsa(RsaAttributes::new(4096, 17, 0))), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP256r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP256r1, None)) ), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP384r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP384r1, None)) ), ( Authentication, - Ecc(EccAttrs::new(ECDSA, BrainpoolP512r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP521r1, None)) ), - (Authentication, Ecc(EccAttrs::new(EdDSA, Ed25519, None))), - (Authentication, Ecc(EccAttrs::new(EdDSA, Cv25519, None))), - (Attestation, Rsa(RsaAttrs::new(2048, 17, 0))), - (Attestation, Rsa(RsaAttrs::new(3072, 17, 0))), - (Attestation, Rsa(RsaAttrs::new(4096, 17, 0))), - (Attestation, Ecc(EccAttrs::new(ECDSA, NistP256r1, None))), - (Attestation, Ecc(EccAttrs::new(ECDSA, NistP384r1, None))), - (Attestation, Ecc(EccAttrs::new(ECDSA, NistP521r1, None))), - (Attestation, Ecc(EccAttrs::new(ECDSA, Secp256k1, None))), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, Secp256k1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP256r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP384r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(ECDSA, BrainpoolP512r1, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(EdDSA, Ed25519, None)) + ), + ( + Authentication, + Ecc(EccAttributes::new(EdDSA, Cv25519, None)) + ), + (Attestation, Rsa(RsaAttributes::new(2048, 17, 0))), + (Attestation, Rsa(RsaAttributes::new(3072, 17, 0))), + (Attestation, Rsa(RsaAttributes::new(4096, 17, 0))), ( Attestation, - Ecc(EccAttrs::new(ECDSA, BrainpoolP256r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP256r1, None)) ), ( Attestation, - Ecc(EccAttrs::new(ECDSA, BrainpoolP384r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP384r1, None)) ), ( Attestation, - Ecc(EccAttrs::new(ECDSA, BrainpoolP512r1, None)) + Ecc(EccAttributes::new(ECDSA, NistP521r1, None)) ), - (Attestation, Ecc(EccAttrs::new(EdDSA, Ed25519, None))), - (Attestation, Ecc(EccAttrs::new(EdDSA, Cv25519, None))) + (Attestation, Ecc(EccAttributes::new(ECDSA, Secp256k1, None))), + ( + Attestation, + Ecc(EccAttributes::new(ECDSA, BrainpoolP256r1, None)) + ), + ( + Attestation, + Ecc(EccAttributes::new(ECDSA, BrainpoolP384r1, None)) + ), + ( + Attestation, + Ecc(EccAttributes::new(ECDSA, BrainpoolP512r1, None)) + ), + (Attestation, Ecc(EccAttributes::new(EdDSA, Ed25519, None))), + (Attestation, Ecc(EccAttributes::new(EdDSA, Cv25519, None))) ]) ); } diff --git a/openpgp-card/src/card_do/application_id.rs b/openpgp-card/src/card_do/application_id.rs index 579d8f6..5fdcf9f 100644 --- a/openpgp-card/src/card_do/application_id.rs +++ b/openpgp-card/src/card_do/application_id.rs @@ -57,6 +57,9 @@ impl ApplicationIdentifier { /// Mapping of manufacturer id to a name, data from: /// [2022-04-07] + /// + /// Also see: + /// https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=scd/app-openpgp.c;hb=HEAD#l292 pub fn manufacturer_name(&self) -> &'static str { match self.manufacturer { 0x0000 => "Testcard", @@ -70,6 +73,7 @@ impl ApplicationIdentifier { 0x0008 => "LogoEmail", 0x0009 => "Fidesmo AB", 0x000A => "Dangerous Things", + 0x000F => "Nitrokey GmbH", 0x000B => "Feitian Technologies", 0x002A => "Magrathea", 0x0042 => "GnuPG e.V.", diff --git a/openpgp-card/src/card_do/cardholder.rs b/openpgp-card/src/card_do/cardholder.rs index 4a332ac..53aa83d 100644 --- a/openpgp-card/src/card_do/cardholder.rs +++ b/openpgp-card/src/card_do/cardholder.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! Cardholder Related Data (see spec pg. 22) @@ -6,8 +6,8 @@ use std::convert::TryFrom; use crate::card_do::{CardholderRelatedData, Lang, Sex}; +use crate::tags::Tags; use crate::tlv::{value::Value, Tlv}; -use crate::Tags; impl CardholderRelatedData { pub fn name(&self) -> Option<&[u8]> { diff --git a/openpgp-card/src/card_do/extended_cap.rs b/openpgp-card/src/card_do/extended_cap.rs index 1a34853..cb4510c 100644 --- a/openpgp-card/src/card_do/extended_cap.rs +++ b/openpgp-card/src/card_do/extended_cap.rs @@ -9,19 +9,101 @@ use crate::card_do::ExtendedCapabilities; use crate::Error; impl ExtendedCapabilities { - pub fn max_len_special_do(&self) -> Option { - self.max_len_special_do + /// Secure Messaging supported. + /// + /// (This feature is currently only available in the SmartPGP implementation) + pub fn secure_messaging(&self) -> bool { + self.secure_messaging } + /// Support for GET CHALLENGE. + /// + /// (GET CHALLENGE generates a random number of a specified length on the smart card) + pub fn get_challenge(&self) -> bool { + self.get_challenge + } + + /// Maximum length of random number that can be requested from the card + /// (if GET CHALLENGE is supported). + /// + /// If GET CHALLENGE is not supported, the coding is 0 + pub fn max_len_challenge(&self) -> u16 { + self.max_len_challenge + } + + /// Support for Key Import + pub fn key_import(&self) -> bool { + self.key_import + } + + /// PW Status changeable + /// (also see [`crate::card_do::PWStatusBytes`]) + pub fn pw_status_change(&self) -> bool { + self.pw_status_change + } + + /// Support for Private use DOs + pub fn private_use_dos(&self) -> bool { + self.private_use_dos + } + + /// Algorithm attributes changeable + /// (also see [`crate::algorithm::AlgorithmAttributes`]) pub fn algo_attrs_changeable(&self) -> bool { self.algo_attrs_changeable } - pub fn max_cmd_len(&self) -> Option { + /// Support for encryption/decryption with AES + pub fn aes(&self) -> bool { + self.aes + } + + /// KDF-related functionality available + pub fn kdf_do(&self) -> bool { + self.kdf_do + } + + /// Maximum length of Cardholder Certificates + pub fn max_len_cardholder_cert(&self) -> u16 { + self.max_len_cardholder_cert + } + + /// Maximum length of "special DOs" + /// (Private Use, Login data, URL, Algorithm attributes, KDF etc.) + /// + /// (OpenPGP card version 3.x only) + pub fn max_len_special_do(&self) -> Option { + self.max_len_special_do + } + + /// (Private Use, Login data, URL, Algorithm attributes, KDF etc.) + /// + /// (OpenPGP card version 3.x only) + pub fn pin_block_2_format_support(&self) -> Option { + self.pin_block_2_format_support + } + + /// MANAGE SECURITY ENVIRONMENT supported (for DEC and AUT keys). + /// (See [`crate::Transaction::manage_security_environment`]) + /// + /// (OpenPGP card version 3.x only) + pub fn mse_command_support(&self) -> Option { + self.mse_command_support + } + + /// Only available in OpenPGP card version 2.x + /// + /// (For OpenPGP card version 3.x, see + /// [`crate::card_do::ExtendedLengthInfo`]) + pub(crate) fn max_cmd_len(&self) -> Option { self.max_cmd_len } - pub fn max_resp_len(&self) -> Option { + /// Only available in OpenPGP card version 2.x + /// + /// (For OpenPGP card version 3.x, see + /// [`crate::card_do::ExtendedLengthInfo`]) + pub(crate) fn max_resp_len(&self) -> Option { self.max_resp_len } } diff --git a/openpgp-card/src/card_do/key_generation_times.rs b/openpgp-card/src/card_do/key_generation_times.rs index cfc560c..ed84d45 100644 --- a/openpgp-card/src/card_do/key_generation_times.rs +++ b/openpgp-card/src/card_do/key_generation_times.rs @@ -16,7 +16,7 @@ impl From for DateTime { let naive_datetime = NaiveDateTime::from_timestamp_opt(kg.0 as i64, 0) .expect("invalid or out-of-range datetime"); - DateTime::from_utc(naive_datetime, Utc) + DateTime::from_naive_utc_and_offset(naive_datetime, Utc) } } diff --git a/openpgp-card/src/apdu/commands.rs b/openpgp-card/src/commands.rs similarity index 94% rename from openpgp-card/src/apdu/commands.rs rename to openpgp-card/src/commands.rs index fc05677..dcd2c67 100644 --- a/openpgp-card/src/apdu/commands.rs +++ b/openpgp-card/src/commands.rs @@ -1,21 +1,16 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! Pre-defined `Command` values for the OpenPGP card application use crate::apdu::command::Command; -use crate::{KeyType, ShortTag, Tags}; +use crate::tags::{ShortTag, Tags}; +use crate::{KeyType, OPENPGP_APPLICATION}; /// 7.2.1 SELECT /// (select the OpenPGP application on the card) pub(crate) fn select_openpgp() -> Command { - Command::new( - 0x00, - 0xA4, - 0x04, - 0x00, - vec![0xD2, 0x76, 0x00, 0x01, 0x24, 0x01], - ) + Command::new(0x00, 0xA4, 0x04, 0x00, OPENPGP_APPLICATION.to_vec()) } /// 7.2.6 GET DATA @@ -47,6 +42,11 @@ pub(crate) fn url() -> Command { get_data(Tags::Url) } +/// GET DO "Login Data" +pub(crate) fn login_data() -> Command { + get_data(Tags::LoginData) +} + /// GET DO "Cardholder related data" pub(crate) fn cardholder_related_data() -> Command { get_data(Tags::CardholderRelatedData) @@ -131,6 +131,11 @@ pub(crate) fn put_private_use_do(num: u8, data: Vec) -> Command { } } +/// PUT DO Login Data +pub(crate) fn put_login_data(login_data: Vec) -> Command { + put_data(Tags::LoginData, login_data) +} + /// PUT DO Name pub(crate) fn put_name(name: Vec) -> Command { put_data(Tags::Name, name) diff --git a/openpgp-card/src/crypto_data.rs b/openpgp-card/src/crypto_data.rs index 075e301..5472bda 100644 --- a/openpgp-card/src/crypto_data.rs +++ b/openpgp-card/src/crypto_data.rs @@ -5,7 +5,7 @@ //! Private key data, public key data, cryptograms for decryption, hash //! data for signing. -use crate::algorithm::Algo; +use crate::algorithm::AlgorithmAttributes; use crate::card_do::{Fingerprint, KeyGenerationTime}; use crate::{oid, Error}; @@ -169,18 +169,18 @@ impl RSAPub { #[non_exhaustive] pub struct EccPub { data: Vec, - algo: Algo, + algo: AlgorithmAttributes, } impl EccPub { - pub fn new(data: Vec, algo: Algo) -> Self { + pub fn new(data: Vec, algo: AlgorithmAttributes) -> Self { Self { data, algo } } pub fn data(&self) -> &[u8] { &self.data } - pub fn algo(&self) -> &Algo { + pub fn algo(&self) -> &AlgorithmAttributes { &self.algo } } diff --git a/openpgp-card/src/errors.rs b/openpgp-card/src/errors.rs index 76c9c0f..79eea68 100644 --- a/openpgp-card/src/errors.rs +++ b/openpgp-card/src/errors.rs @@ -10,6 +10,8 @@ //! - [`StatusBytes`], which models error statuses reported by the OpenPGP //! card application +use card_backend::SmartcardError; + /// Enum wrapper for the different error types of this crate #[derive(thiserror::Error, Debug)] #[non_exhaustive] @@ -49,6 +51,12 @@ impl From for Error { } } +impl From for Error { + fn from(sce: SmartcardError) -> Self { + Error::Smartcard(sce) + } +} + /// OpenPGP card "Status Bytes" (ok statuses and errors) #[derive(thiserror::Error, Debug, PartialEq, Eq, Copy, Clone)] #[non_exhaustive] @@ -65,6 +73,9 @@ pub enum StatusBytes { #[error("Password not checked, {0} allowed retries")] PasswordNotChecked(u8), + #[error("Execution error with non-volatile memory unchanged")] + ExecutionErrorNonVolatileMemoryUnchanged, + #[error("Triggering by the card {0}")] TriggeringByCard(u8), @@ -137,6 +148,7 @@ impl From<(u8, u8)> for StatusBytes { (0x62, 0x85) => StatusBytes::TerminationState, (0x63, 0xC0..=0xCF) => StatusBytes::PasswordNotChecked(status.1 & 0xf), + (0x64, 0x00) => StatusBytes::ExecutionErrorNonVolatileMemoryUnchanged, (0x64, 0x02..=0x80) => StatusBytes::TriggeringByCard(status.1), (0x65, 0x01) => StatusBytes::MemoryFailure, (0x66, 0x00) => StatusBytes::SecurityRelatedIssues, @@ -161,32 +173,3 @@ impl From<(u8, u8)> for StatusBytes { } } } - -/// Errors on the smartcard/reader layer -#[derive(thiserror::Error, Debug)] -#[non_exhaustive] -pub enum SmartcardError { - #[error("Failed to create a pcsc smartcard context {0}")] - ContextError(String), - - #[error("Failed to list readers: {0}")] - ReaderError(String), - - #[error("No reader found.")] - NoReaderFoundError, - - #[error("The requested card '{0}' was not found.")] - CardNotFound(String), - - #[error("Couldn't select the OpenPGP card application")] - SelectOpenPGPCardFailed, - - #[error("Failed to connect to the card: {0}")] - SmartCardConnectionError(String), - - #[error("NotTransacted (SCARD_E_NOT_TRANSACTED)")] - NotTransacted, - - #[error("Generic SmartCard Error: {0}")] - Error(String), -} diff --git a/openpgp-card/src/keys.rs b/openpgp-card/src/keys.rs index b283428..6d80af8 100644 --- a/openpgp-card/src/keys.rs +++ b/openpgp-card/src/keys.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! Generate and import keys @@ -6,81 +6,54 @@ use std::convert::TryFrom; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::algorithm::{Algo, AlgoInfo, Curve, EccAttrs, RsaAttrs}; +use crate::algorithm::{ + AlgorithmAttributes, AlgorithmInformation, Curve, EccAttributes, RsaAttributes, +}; use crate::apdu::command::Command; -use crate::apdu::commands; -use crate::card_do::{ApplicationRelatedData, Fingerprint, KeyGenerationTime}; +use crate::card_do::{Fingerprint, KeyGenerationTime}; +use crate::commands; use crate::crypto_data::{ CardUploadableKey, EccKey, EccPub, EccType, PrivateKeyMaterial, PublicKeyMaterial, RSAKey, RSAPub, }; -use crate::openpgp::OpenPgpTransaction; +use crate::tags::Tags; use crate::tlv::{length::tlv_encode_length, value::Value, Tlv}; -use crate::{apdu, Error, KeyType, Tag, Tags}; +use crate::{Error, KeyType, Tag, Transaction}; -/// Generate asymmetric key pair on the card. +/// Generate asymmetric key pair on the card and set metadata (time, fingerprint). /// -/// This is a convenience wrapper around gen_key() that: -/// - sets algorithm attributes (if not None) +/// This function doesn't set the algorithm_attributes! +/// +/// This is a convenience wrapper around [generate_asymmetric_key_pair] that: /// - generates a key pair on the card /// - sets the creation time on the card to the current host time /// - calculates fingerprint for the key and sets it on the card /// /// `fp_from_pub` calculates the fingerprint for a public key data object and /// creation timestamp -pub(crate) fn gen_key_with_metadata( - card_tx: &mut OpenPgpTransaction, +pub(crate) fn gen_key_set_metadata( + card_tx: &mut Transaction, fp_from_pub: fn(&PublicKeyMaterial, KeyGenerationTime, KeyType) -> Result, + algorithm_attributes: &AlgorithmAttributes, key_type: KeyType, - algo: Option<&Algo>, ) -> Result<(PublicKeyMaterial, KeyGenerationTime), Error> { - // Set algo on card if it's Some - if let Some(target_algo) = algo { - // FIXME: caching - let ard = card_tx.application_related_data()?; // no caching, here! - let ecap = ard.extended_capabilities()?; - - // Only set algo if card supports setting of algo attr - if ecap.algo_attrs_changeable() { - card_tx.set_algorithm_attributes(key_type, target_algo)?; - } else { - // Check if the current algo on the card is the one we want, if - // not we return an error. - - // NOTE: For RSA, the target algo shouldn't prescribe an - // Import-Format. The Import-Format should always depend on what - // the card supports. - - // let cur_algo = ard.get_algorithm_attributes(key_type)?; - // assert_eq!(&cur_algo, target_algo); - - // FIXME: return error - } - } - - // get current (possibly updated) state of algo - let ard = card_tx.application_related_data()?; // no caching, here! - let cur_algo = ard.algorithm_attributes(key_type)?; - - // generate key - let tlv = generate_asymmetric_key_pair(card_tx, key_type)?; - - // derive pubkey - let pubkey = tlv_to_pubkey(&tlv, &cur_algo)?; - - log::trace!("public {:x?}", pubkey); - - // set creation time + // get creation timestamp let time = SystemTime::now(); - - // Store creation timestamp (unix time format, limited to u32) let ts = time .duration_since(UNIX_EPOCH) .map_err(|e| Error::InternalError(format!("This should never happen {e}")))? .as_secs() as u32; - let ts = ts.into(); + // generate key + let tlv = generate_asymmetric_key_pair(card_tx, key_type)?; + // derive pubkey + let pubkey = tlv_to_pubkey(&tlv, algorithm_attributes)?; + + log::trace!("public {:x?}", pubkey); + + // Store creation timestamp (unix time format, limited to u32) + let ts = ts.into(); card_tx.set_creation_time(ts, key_type)?; // calculate/store fingerprint @@ -91,7 +64,7 @@ pub(crate) fn gen_key_with_metadata( } /// Transform a public key Tlv from the card into PublicKeyMaterial -fn tlv_to_pubkey(tlv: &Tlv, algo: &Algo) -> Result { +fn tlv_to_pubkey(tlv: &Tlv, algo: &AlgorithmAttributes) -> Result { let n = tlv.find(Tags::PublicKeyDataRsaModulus); let v = tlv.find(Tags::PublicKeyDataRsaExponent); @@ -121,7 +94,7 @@ fn tlv_to_pubkey(tlv: &Tlv, algo: &Algo) -> Result Result { log::info!("OpenPgpTransaction: generate_asymmetric_key_pair"); @@ -130,7 +103,7 @@ pub(crate) fn generate_asymmetric_key_pair( let crt = control_reference_template(key_type)?; let gen_key_cmd = commands::gen_key(crt.serialize().to_vec()); - let resp = apdu::send_command(card_tx.tx(), gen_key_cmd, true)?; + let resp = card_tx.send_command(gen_key_cmd, true)?; resp.check_ok()?; let tlv = Tlv::try_from(resp.data()?)?; @@ -145,7 +118,7 @@ pub(crate) fn generate_asymmetric_key_pair( /// /// (See 7.2.14 GENERATE ASYMMETRIC KEY PAIR) pub(crate) fn public_key( - card_tx: &mut OpenPgpTransaction, + card_tx: &mut Transaction, key_type: KeyType, ) -> Result { log::info!("OpenPgpTransaction: public_key"); @@ -158,7 +131,7 @@ pub(crate) fn public_key( let crt = control_reference_template(key_type)?; let get_pub_key_cmd = commands::get_pub_key(crt.serialize().to_vec()); - let resp = apdu::send_command(card_tx.tx(), get_pub_key_cmd, true)?; + let resp = card_tx.send_command(get_pub_key_cmd, true)?; resp.check_ok()?; let tlv = Tlv::try_from(resp.data()?)?; @@ -169,55 +142,112 @@ pub(crate) fn public_key( /// Import private key material to the card as a specific KeyType. /// -/// If the key is suitable for `key_type`, an Error is returned (either +/// If the key is unsuitable for `key_type`, an Error is returned (either /// caused by checks before attempting to upload the key to the card, or by /// an error that the card reports during an attempt to upload the key). pub(crate) fn key_import( - card_tx: &mut OpenPgpTransaction, + card_tx: &mut Transaction, key: Box, key_type: KeyType, - algo_info: Option, ) -> Result<(), Error> { log::info!("OpenPgpTransaction: key_import"); + match key.private_key()? { + PrivateKeyMaterial::R(rsa_key) => key_import_rsa( + card_tx, + key_type, + rsa_key, + key.fingerprint()?, + key.timestamp(), + ), + PrivateKeyMaterial::E(ecc_key) => key_import_ecc( + card_tx, + key_type, + ecc_key, + key.fingerprint()?, + key.timestamp(), + ), + } +} + +fn key_import_rsa( + card_tx: &mut Transaction, + key_type: KeyType, + rsa_key: Box, + fp: Fingerprint, + ts: KeyGenerationTime, +) -> Result<(), Error> { + // An error is ok (it's fine if a card doesn't offer a list of supported algorithms) + let algo_info = card_tx.algorithm_information_cached().ok().flatten(); + + // RSA bitsize + // (round up to 4-bytes, in case the key has 8+ leading zero bits) + let rsa_bits = (((rsa_key.n().len() * 8 + 31) / 32) * 32) as u16; + // FIXME: caching? let ard = card_tx.application_related_data()?; - let (algo, key_cmd) = match key.private_key()? { - PrivateKeyMaterial::R(rsa_key) => { - // RSA bitsize - // (round up to 4-bytes, in case the key has 8+ leading zero bits) - let rsa_bits = (((rsa_key.n().len() * 8 + 31) / 32) * 32) as u16; + let algo_attr = ard.algorithm_attributes(key_type)?; + let rsa_attrs = determine_rsa_attrs(rsa_bits, key_type, algo_attr, algo_info)?; - let rsa_attrs = determine_rsa_attrs(rsa_bits, key_type, &ard, algo_info)?; - - let key_cmd = rsa_key_import_cmd(key_type, rsa_key, &rsa_attrs)?; - - (Algo::Rsa(rsa_attrs), key_cmd) - } - PrivateKeyMaterial::E(ecc_key) => { - let ecc_attrs = - determine_ecc_attrs(ecc_key.oid(), ecc_key.ecc_type(), key_type, algo_info)?; - - let key_cmd = ecc_key_import_cmd(key_type, ecc_key, &ecc_attrs)?; - - (Algo::Ecc(ecc_attrs), key_cmd) - } - }; - - let fp = key.fingerprint()?; + let import_cmd = rsa_key_import_cmd(key_type, rsa_key, &rsa_attrs)?; // Now that we have marshalled all necessary information, perform all // set-operations on the card. + import_key_set_metadata( + card_tx, + key_type, + import_cmd, + fp, + ts, + AlgorithmAttributes::Rsa(rsa_attrs), + ) +} + +fn key_import_ecc( + card_tx: &mut Transaction, + key_type: KeyType, + ecc_key: Box, + fp: Fingerprint, + ts: KeyGenerationTime, +) -> Result<(), Error> { + // An error is ok (it's fine if a card doesn't offer a list of supported algorithms) + let algo_info = card_tx.algorithm_information_cached().ok().flatten(); + + let ecc_attrs = determine_ecc_attrs(ecc_key.oid(), ecc_key.ecc_type(), key_type, algo_info)?; + let import_cmd = ecc_key_import_cmd(key_type, ecc_key, &ecc_attrs)?; + + // Now that we have marshalled all necessary information, perform all + // set-operations on the card. + import_key_set_metadata( + card_tx, + key_type, + import_cmd, + fp, + ts, + AlgorithmAttributes::Ecc(ecc_attrs), + ) +} + +fn import_key_set_metadata( + card_tx: &mut Transaction, + key_type: KeyType, + import_cmd: Command, + fp: Fingerprint, + ts: KeyGenerationTime, + algorithm_attributes: AlgorithmAttributes, +) -> Result<(), Error> { + log::info!("Import key material"); // Only set algo attrs if "Extended Capabilities" lists the feature - if ard.extended_capabilities()?.algo_attrs_changeable() { - card_tx.set_algorithm_attributes(key_type, &algo)?; + if card_tx.extended_capabilities()?.algo_attrs_changeable() { + card_tx.set_algorithm_attributes(key_type, &algorithm_attributes)?; } - apdu::send_command(card_tx.tx(), key_cmd, false)?.check_ok()?; + card_tx.send_command(import_cmd, false)?.check_ok()?; + card_tx.set_fingerprint(fp, key_type)?; - card_tx.set_creation_time(key.timestamp(), key_type)?; + card_tx.set_creation_time(ts, key_type)?; Ok(()) } @@ -231,9 +261,9 @@ pub(crate) fn key_import( pub(crate) fn determine_rsa_attrs( rsa_bits: u16, key_type: KeyType, - ard: &ApplicationRelatedData, - algo_info: Option, -) -> Result { + algo_attr: AlgorithmAttributes, + algo_info: Option, +) -> Result { // Figure out suitable RSA algorithm parameters: // Does the card offer a list of algorithms? @@ -244,13 +274,11 @@ pub(crate) fn determine_rsa_attrs( } else { // No -> Get the current algorithm attributes for key_type. - let algo = ard.algorithm_attributes(key_type)?; - // Is the algorithm on the card currently set to RSA? - if let Algo::Rsa(rsa) = algo { + if let AlgorithmAttributes::Rsa(rsa) = algo_attr { // If so, use the algorithm parameters from the card and // adjust the bit length based on the user-provided key. - RsaAttrs::new(rsa_bits, rsa.len_e(), rsa.import_format()) + RsaAttributes::new(rsa_bits, rsa.len_e(), rsa.import_format()) } else { // The card doesn't provide an algorithm list, and the // current algorithm on the card is not RSA. @@ -263,7 +291,7 @@ pub(crate) fn determine_rsa_attrs( // list of which RSA parameters that model of card // supports] - RsaAttrs::new(rsa_bits, 32, 0) + RsaAttributes::new(rsa_bits, 32, 0) } }; @@ -276,8 +304,8 @@ pub(crate) fn determine_ecc_attrs( oid: &[u8], ecc_type: EccType, key_type: KeyType, - algo_info: Option, -) -> Result { + algo_info: Option, +) -> Result { // If we have an algo_info, refuse upload if oid is not listed if let Some(algo_info) = algo_info { let algos = check_card_algo_ecc(algo_info, key_type, oid); @@ -297,7 +325,7 @@ pub(crate) fn determine_ecc_attrs( // We do however, use import_format from algorithm information. if !algos.is_empty() { - return Ok(EccAttrs::new( + return Ok(EccAttributes::new( ecc_type, Curve::try_from(oid)?, algos[0].import_format(), @@ -308,19 +336,29 @@ pub(crate) fn determine_ecc_attrs( // Return a default when we have no algo_info. // (Do cards that support ecc but have no algo_info exist?) - Ok(EccAttrs::new(ecc_type, Curve::try_from(oid)?, None)) + Ok(EccAttributes::new(ecc_type, Curve::try_from(oid)?, None)) } /// Look up RsaAttrs parameters in algo_info based on key_type and rsa_bits -fn card_algo_rsa(algo_info: AlgoInfo, key_type: KeyType, rsa_bits: u16) -> Result { +fn card_algo_rsa( + algo_info: AlgorithmInformation, + key_type: KeyType, + rsa_bits: u16, +) -> Result { // Find suitable algorithm parameters (from card's list of algorithms). // Get Algos for this keytype - let keytype_algos: Vec<_> = algo_info.filter_by_keytype(key_type); + let keytype_algos: Vec<_> = algo_info.for_keytype(key_type); // Get RSA algo attributes let rsa_algos: Vec<_> = keytype_algos .iter() - .filter_map(|a| if let Algo::Rsa(r) = a { Some(r) } else { None }) + .filter_map(|a| { + if let AlgorithmAttributes::Rsa(r) = a { + Some(r) + } else { + None + } + }) .collect(); // Filter card algorithms by rsa bitlength of the key we want to upload @@ -344,16 +382,26 @@ fn card_algo_rsa(algo_info: AlgoInfo, key_type: KeyType, rsa_bits: u16) -> Resul } /// Get all entries from algo_info with matching `oid` and `key_type`. -fn check_card_algo_ecc(algo_info: AlgoInfo, key_type: KeyType, oid: &[u8]) -> Vec { +fn check_card_algo_ecc( + algo_info: AlgorithmInformation, + key_type: KeyType, + oid: &[u8], +) -> Vec { // Find suitable algorithm parameters (from card's list of algorithms). // Get Algos for this keytype - let keytype_algos: Vec<_> = algo_info.filter_by_keytype(key_type); + let keytype_algos: Vec<_> = algo_info.for_keytype(key_type); // Get attributes let ecc_algos: Vec<_> = keytype_algos .iter() - .filter_map(|a| if let Algo::Ecc(e) = a { Some(e) } else { None }) + .filter_map(|a| { + if let AlgorithmAttributes::Ecc(e) = a { + Some(e) + } else { + None + } + }) .collect(); // Find entries with this OID in the algorithm information for key_type @@ -369,7 +417,7 @@ fn check_card_algo_ecc(algo_info: AlgoInfo, key_type: KeyType, oid: &[u8]) -> Ve fn rsa_key_import_cmd( key_type: KeyType, rsa_key: Box, - rsa_attrs: &RsaAttrs, + rsa_attrs: &RsaAttributes, ) -> Result { // Assemble key command (see 4.4.3.12 Private Key Template) @@ -467,7 +515,7 @@ fn rsa_key_import_cmd( fn ecc_key_import_cmd( key_type: KeyType, ecc_key: Box, - ecc_attrs: &EccAttrs, + ecc_attrs: &EccAttributes, ) -> Result { let private = ecc_key.private(); diff --git a/openpgp-card/src/lib.rs b/openpgp-card/src/lib.rs index a89a12a..e34e86a 100644 --- a/openpgp-card/src/lib.rs +++ b/openpgp-card/src/lib.rs @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! Client library for //! [OpenPGP card](https://en.wikipedia.org/wiki/OpenPGP_card) -//! devices (such as Gnuk, YubiKey, or Java smartcards running an OpenPGP -//! card application). +//! devices (such as Gnuk, Nitrokey, YubiKey, or Java smartcards running an +//! OpenPGP card application). //! //! This library aims to offer //! - access to all features in the OpenPGP @@ -12,505 +12,53 @@ //! - without relying on a particular //! [OpenPGP implementation](https://www.openpgp.org/software/developer/). //! -//! This library can't directly access cards by itself. Instead, users -//! need to supply a backend that implements the [`CardBackend`] -//! / [`CardTransaction`] traits. The companion crate -//! [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc) -//! offers a backend that uses [PC/SC](https://en.wikipedia.org/wiki/PC/SC) to -//! communicate with Smart Cards. -//! //! The [openpgp-card-sequoia](https://crates.io/crates/openpgp-card-sequoia) //! crate offers a higher level wrapper based on the [Sequoia PGP](https://sequoia-pgp.org/) //! implementation. //! -//! See the [architecture diagram](https://gitlab.com/openpgp-card/openpgp-card#architecture) for -//! a visualization. +//! Note that this library can't directly access cards by itself. +//! Instead, users need to supply a backend that implements the +//! [`CardBackend`] and [`CardTransaction`] traits. +//! For example [card-backend-pcsc](https://crates.io/crates/card-backend-pcsc) +//! offers a backend implementation that uses [PC/SC](https://en.wikipedia.org/wiki/PC/SC) to +//! communicate with Smart Cards. +//! +//! See the [architecture diagram](https://gitlab.com/openpgp-card/openpgp-card#architecture) +//! for an overview of the ecosystem around this crate. extern crate core; pub mod algorithm; -pub(crate) mod apdu; +mod apdu; pub mod card_do; +mod commands; pub mod crypto_data; mod errors; -pub(crate) mod keys; +mod keys; mod oid; -mod openpgp; +mod tags; mod tlv; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; -use crate::apdu::commands; -use crate::card_do::ApplicationRelatedData; -pub use crate::errors::{Error, SmartcardError, StatusBytes}; -pub use crate::openpgp::{OpenPgp, OpenPgpTransaction}; -use crate::tlv::{tag::Tag, value::Value, Tlv}; +use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError}; +use tags::{ShortTag, Tags}; -/// The CardBackend trait defines a connection with an OpenPGP card via a -/// backend implementation (e.g. via the pcsc backend in the crate -/// [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)), -/// A CardBackend is only used to get access to a `CardTransaction` object. -#[blanket::blanket(derive(Box))] -pub trait CardBackend { - fn transaction(&mut self) -> Result, Error>; -} +use crate::algorithm::{AlgorithmAttributes, AlgorithmInformation}; +use crate::apdu::command::Command; +use crate::apdu::response::RawResponse; +use crate::card_do::{ + ApplicationIdentifier, ApplicationRelatedData, CardholderRelatedData, ExtendedCapabilities, + ExtendedLengthInfo, Fingerprint, HistoricalBytes, KeyGenerationTime, Lang, PWStatusBytes, + SecuritySupportTemplate, Sex, UserInteractionFlag, +}; +use crate::crypto_data::{CardUploadableKey, Cryptogram, Hash, PublicKeyMaterial}; +pub use crate::errors::{Error, StatusBytes}; +use crate::tlv::tag::Tag; +use crate::tlv::value::Value; +use crate::tlv::Tlv; -/// The CardTransaction trait defines communication with an OpenPGP card via a -/// backend implementation (e.g. the pcsc backend in the crate -/// [openpgp-card-pcsc](https://crates.io/crates/openpgp-card-pcsc)), -/// after opening a transaction from a CardBackend. -#[blanket::blanket(derive(Box))] -pub trait CardTransaction { - /// Transmit the command data in `cmd` to the card. - /// - /// `buf_size` is a hint to the backend (the backend may ignore it) - /// indicating the expected maximum response size. - fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result, Error>; - - /// Set the card capabilities in the CardTransaction. - /// - /// Setting these capabilities is typically part of a bootstrapping - /// process: the information about the card's capabilities is typically - /// requested from the card using the same CardTransaction instance, - /// before the card's capabilities have been initialized. - fn init_card_caps(&mut self, caps: CardCaps); - - /// Request the card's capabilities - /// - /// (apdu serialization makes use of this information, e.g. to - /// determine if extended length can be used) - fn card_caps(&self) -> Option<&CardCaps>; - - /// If a CardTransaction implementation introduces an additional, - /// backend-specific limit for maximum number of bytes per command, - /// this fn can indicate that limit by returning `Some(max_cmd_len)`. - fn max_cmd_len(&self) -> Option { - None - } - - /// Does the reader support FEATURE_VERIFY_PIN_DIRECT? - fn feature_pinpad_verify(&self) -> bool; - - /// Does the reader support FEATURE_MODIFY_PIN_DIRECT? - fn feature_pinpad_modify(&self) -> bool; - - /// Verify the PIN `id` via the reader pinpad - fn pinpad_verify(&mut self, pin: PinType) -> Result, Error>; - - /// Modify the PIN `id` via the reader pinpad - fn pinpad_modify(&mut self, pin: PinType) -> Result, Error>; - - /// Select the OpenPGP card application - fn select(&mut self) -> Result, Error> { - log::info!("CardTransaction: select"); - let select_openpgp = commands::select_openpgp(); - apdu::send_command(self, select_openpgp, false)?.try_into() - } - - /// Get the "application related data" from the card. - /// - /// (This data should probably be cached in a higher layer. Some parts of - /// it are needed regularly, and it does not usually change during - /// normal use of a card.) - fn application_related_data(&mut self) -> Result { - let ad = commands::application_related_data(); - let resp = apdu::send_command(self, ad, true)?; - let value = Value::from(resp.data()?, true)?; - - log::trace!(" ARD value: {:x?}", value); - - Ok(ApplicationRelatedData(Tlv::new( - Tags::ApplicationRelatedData, - value, - ))) - } - - /// Get a CardApp based on a CardTransaction. - /// - /// It is expected that SELECT has already been performed on the card - /// beforehand. - /// - /// This fn initializes the CardCaps by requesting - /// application_related_data from the card, and setting the - /// capabilities accordingly. - fn initialize(&mut self) -> Result<(), Error> { - let ard = self.application_related_data()?; - - // Determine chaining/extended length support from card - // metadata and cache this information in the CardTransaction - // implementation (as a CardCaps) - let mut ext_support = false; - let mut chaining_support = false; - - if let Ok(hist) = ard.historical_bytes() { - if let Some(cc) = hist.card_capabilities() { - chaining_support = cc.command_chaining(); - ext_support = cc.extended_lc_le(); - } - } - - let ext_cap = ard.extended_capabilities()?; - - // Get max command/response byte sizes from card - let (max_cmd_bytes, max_rsp_bytes) = - if let Ok(Some(eli)) = ard.extended_length_information() { - // In card 3.x, max lengths come from ExtendedLengthInfo - (eli.max_command_bytes(), eli.max_response_bytes()) - } else if let (Some(cmd), Some(rsp)) = (ext_cap.max_cmd_len(), ext_cap.max_resp_len()) { - // In card 2.x, max lengths come from ExtendedCapabilities - (cmd, rsp) - } else { - // Fallback: use 255 if we have no information from the card - (255, 255) - }; - - let pw_status = ard.pw_status_bytes()?; - let pw1_max = pw_status.pw1_max_len(); - let pw3_max = pw_status.pw3_max_len(); - - let caps = CardCaps { - ext_support, - chaining_support, - max_cmd_bytes, - max_rsp_bytes, - pw1_max_len: pw1_max, - pw3_max_len: pw3_max, - }; - - log::trace!("init_card_caps to: {:x?}", caps); - - self.init_card_caps(caps); - - Ok(()) - } -} - -/// Information about the capabilities of a card. -/// -/// CardCaps is used to signal capabilities (chaining, extended length support, max -/// command/response sizes, max PIN lengths) of the current card to backends. -/// -/// CardCaps is not intended for users of this library. -/// -/// (The information is gathered from the "Card Capabilities", "Extended length information" and -/// "PWStatus" DOs) -#[derive(Clone, Copy, Debug)] -pub struct CardCaps { - /// Does the card support extended Lc and Le fields? - ext_support: bool, - - /// Command chaining support? - chaining_support: bool, - - /// Maximum number of bytes in a command APDU - max_cmd_bytes: u16, - - /// Maximum number of bytes in a response APDU - max_rsp_bytes: u16, - - /// Maximum length of PW1 - pw1_max_len: u8, - - /// Maximum length of PW3 - pw3_max_len: u8, -} - -impl CardCaps { - pub fn ext_support(&self) -> bool { - self.ext_support - } - - pub fn max_rsp_bytes(&self) -> u16 { - self.max_rsp_bytes - } - - pub fn pw1_max_len(&self) -> u8 { - self.pw1_max_len - } - - pub fn pw3_max_len(&self) -> u8 { - self.pw3_max_len - } -} - -/// Tags, as specified and used in the OpenPGP card 3.4.1 spec. -/// All tags in OpenPGP card are either 1 or 2 bytes long. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -#[non_exhaustive] -#[allow(dead_code)] -pub(crate) enum Tags { - // BER identifiers - OctetString, - Null, - ObjectIdentifier, - Sequence, - - // GET DATA - PrivateUse1, - PrivateUse2, - PrivateUse3, - PrivateUse4, - ApplicationIdentifier, - LoginData, - Url, - HistoricalBytes, - CardholderRelatedData, - Name, - LanguagePref, - Sex, - ApplicationRelatedData, - ExtendedLengthInformation, - GeneralFeatureManagement, - DiscretionaryDataObjects, - ExtendedCapabilities, - AlgorithmAttributesSignature, - AlgorithmAttributesDecryption, - AlgorithmAttributesAuthentication, - PWStatusBytes, - Fingerprints, - CaFingerprints, - GenerationTimes, - KeyInformation, - UifSig, - UifDec, - UifAuth, - UifAttestation, - SecuritySupportTemplate, - DigitalSignatureCounter, - CardholderCertificate, - AlgorithmAttributesAttestation, - FingerprintAttestation, - CaFingerprintAttestation, - GenerationTimeAttestation, - KdfDo, - AlgorithmInformation, - CertificateSecureMessaging, - AttestationCertificate, - - // PUT DATA (additional Tags that don't get used for GET DATA) - FingerprintSignature, - FingerprintDecryption, - FingerprintAuthentication, - CaFingerprint1, - CaFingerprint2, - CaFingerprint3, - GenerationTimeSignature, - GenerationTimeDecryption, - GenerationTimeAuthentication, - // FIXME: +D1, D2 - ResettingCode, - PsoEncDecKey, - - // OTHER - // 4.4.3.12 Private Key Template - ExtendedHeaderList, - CardholderPrivateKeyTemplate, - ConcatenatedKeyData, - CrtKeySignature, - CrtKeyConfidentiality, - CrtKeyAuthentication, - PrivateKeyDataRsaPublicExponent, - PrivateKeyDataRsaPrime1, - PrivateKeyDataRsaPrime2, - PrivateKeyDataRsaPq, - PrivateKeyDataRsaDp1, - PrivateKeyDataRsaDq1, - PrivateKeyDataRsaModulus, - PrivateKeyDataEccPrivateKey, - PrivateKeyDataEccPublicKey, - - // 7.2.14 GENERATE ASYMMETRIC KEY PAIR - PublicKey, - PublicKeyDataRsaModulus, - PublicKeyDataRsaExponent, - PublicKeyDataEccPoint, - - // 7.2.11 PSO: DECIPHER - Cipher, - ExternalPublicKey, - - // 7.2.5 SELECT DATA - GeneralReference, - TagList, -} - -impl From for Vec { - fn from(t: Tags) -> Self { - ShortTag::from(t).into() - } -} - -impl From for Tag { - fn from(t: Tags) -> Self { - ShortTag::from(t).into() - } -} - -impl From for ShortTag { - fn from(t: Tags) -> Self { - match t { - // BER identifiers https://en.wikipedia.org/wiki/X.690#BER_encoding - Tags::OctetString => [0x04].into(), - Tags::Null => [0x05].into(), - Tags::ObjectIdentifier => [0x06].into(), - Tags::Sequence => [0x30].into(), - - // GET DATA - Tags::PrivateUse1 => [0x01, 0x01].into(), - Tags::PrivateUse2 => [0x01, 0x02].into(), - Tags::PrivateUse3 => [0x01, 0x03].into(), - Tags::PrivateUse4 => [0x01, 0x04].into(), - Tags::ApplicationIdentifier => [0x4f].into(), - Tags::LoginData => [0x5e].into(), - Tags::Url => [0x5f, 0x50].into(), - Tags::HistoricalBytes => [0x5f, 0x52].into(), - Tags::CardholderRelatedData => [0x65].into(), - Tags::Name => [0x5b].into(), - Tags::LanguagePref => [0x5f, 0x2d].into(), - Tags::Sex => [0x5f, 0x35].into(), - Tags::ApplicationRelatedData => [0x6e].into(), - Tags::ExtendedLengthInformation => [0x7f, 0x66].into(), - Tags::GeneralFeatureManagement => [0x7f, 0x74].into(), - Tags::DiscretionaryDataObjects => [0x73].into(), - Tags::ExtendedCapabilities => [0xc0].into(), - Tags::AlgorithmAttributesSignature => [0xc1].into(), - Tags::AlgorithmAttributesDecryption => [0xc2].into(), - Tags::AlgorithmAttributesAuthentication => [0xc3].into(), - Tags::PWStatusBytes => [0xc4].into(), - Tags::Fingerprints => [0xc5].into(), - Tags::CaFingerprints => [0xc6].into(), - Tags::GenerationTimes => [0xcd].into(), - Tags::KeyInformation => [0xde].into(), - Tags::UifSig => [0xd6].into(), - Tags::UifDec => [0xd7].into(), - Tags::UifAuth => [0xd8].into(), - Tags::UifAttestation => [0xd9].into(), - Tags::SecuritySupportTemplate => [0x7a].into(), - Tags::DigitalSignatureCounter => [0x93].into(), - Tags::CardholderCertificate => [0x7f, 0x21].into(), - Tags::AlgorithmAttributesAttestation => [0xda].into(), - Tags::FingerprintAttestation => [0xdb].into(), - Tags::CaFingerprintAttestation => [0xdc].into(), - Tags::GenerationTimeAttestation => [0xdd].into(), - Tags::KdfDo => [0xf9].into(), - Tags::AlgorithmInformation => [0xfa].into(), - Tags::CertificateSecureMessaging => [0xfb].into(), - Tags::AttestationCertificate => [0xfc].into(), - - // PUT DATA - Tags::FingerprintSignature => [0xc7].into(), - Tags::FingerprintDecryption => [0xc8].into(), - Tags::FingerprintAuthentication => [0xc9].into(), - Tags::CaFingerprint1 => [0xca].into(), - Tags::CaFingerprint2 => [0xcb].into(), - Tags::CaFingerprint3 => [0xcc].into(), - Tags::GenerationTimeSignature => [0xce].into(), - Tags::GenerationTimeDecryption => [0xcf].into(), - Tags::GenerationTimeAuthentication => [0xd0].into(), - Tags::ResettingCode => [0xd3].into(), - Tags::PsoEncDecKey => [0xd5].into(), - - // OTHER - // 4.4.3.12 Private Key Template - Tags::ExtendedHeaderList => [0x4d].into(), - Tags::CardholderPrivateKeyTemplate => [0x7f, 0x48].into(), - Tags::ConcatenatedKeyData => [0x5f, 0x48].into(), - Tags::CrtKeySignature => [0xb6].into(), - Tags::CrtKeyConfidentiality => [0xb8].into(), - Tags::CrtKeyAuthentication => [0xa4].into(), - Tags::PrivateKeyDataRsaPublicExponent => [0x91].into(), - Tags::PrivateKeyDataRsaPrime1 => [0x92].into(), // Note: value reused! - Tags::PrivateKeyDataRsaPrime2 => [0x93].into(), - Tags::PrivateKeyDataRsaPq => [0x94].into(), - Tags::PrivateKeyDataRsaDp1 => [0x95].into(), - Tags::PrivateKeyDataRsaDq1 => [0x96].into(), - Tags::PrivateKeyDataRsaModulus => [0x97].into(), - Tags::PrivateKeyDataEccPrivateKey => [0x92].into(), // Note: value reused! - Tags::PrivateKeyDataEccPublicKey => [0x99].into(), - - // 7.2.14 GENERATE ASYMMETRIC KEY PAIR - Tags::PublicKey => [0x7f, 0x49].into(), - Tags::PublicKeyDataRsaModulus => [0x81].into(), - Tags::PublicKeyDataRsaExponent => [0x82].into(), - Tags::PublicKeyDataEccPoint => [0x86].into(), - - // 7.2.11 PSO: DECIPHER - Tags::Cipher => [0xa6].into(), - Tags::ExternalPublicKey => [0x86].into(), - - // 7.2.5 SELECT DATA - Tags::GeneralReference => [0x60].into(), - Tags::TagList => [0x5c].into(), - } - } -} - -/// A ShortTag is a Tlv tag that is guaranteed to be either 1 or 2 bytes long. -/// -/// This covers any tag that can be used in the OpenPGP card context (the spec doesn't describe how -/// longer tags might be used.) -/// -/// (The type tlv::Tag will usually/always contain 1 or 2 byte long tags, in this library. -/// But its length is not guaranteed by the type system) -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum ShortTag { - One(u8), - Two(u8, u8), -} - -impl From for Tag { - fn from(n: ShortTag) -> Self { - match n { - ShortTag::One(t0) => [t0].into(), - ShortTag::Two(t0, t1) => [t0, t1].into(), - } - } -} - -impl From<[u8; 1]> for ShortTag { - fn from(v: [u8; 1]) -> Self { - ShortTag::One(v[0]) - } -} -impl From<[u8; 2]> for ShortTag { - fn from(v: [u8; 2]) -> Self { - ShortTag::Two(v[0], v[1]) - } -} -impl From for Vec { - fn from(t: ShortTag) -> Self { - match t { - ShortTag::One(t0) => vec![t0], - ShortTag::Two(t0, t1) => vec![t0, t1], - } - } -} - -/// Specify a PIN to *verify* (distinguishes between `Sign`, `User` and `Admin`). -/// -/// (Note that for PIN *management*, in particular changing a PIN, "signing and user" are -/// not distinguished. They always share the same PIN value `PW1`) -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum PinType { - /// Verify PW1 in mode P2=81 (for the PSO:CDS operation) - Sign, - - /// Verify PW1 in mode P2=82 (for all other User operations) - User, - - /// Verify PW3 (for Admin operations) - Admin, -} - -impl PinType { - pub fn id(&self) -> u8 { - match self { - PinType::Sign => 0x81, - PinType::User => 0x82, - PinType::Admin => 0x83, - } - } -} +const OPENPGP_APPLICATION: &[u8] = &[0xD2, 0x76, 0x00, 0x01, 0x24, 0x01]; /// Identify a Key slot on an OpenPGP card #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -519,6 +67,8 @@ pub enum KeyType { Signing, Decryption, Authentication, + + /// Attestation is a Yubico proprietary key slot Attestation, } @@ -563,34 +113,1254 @@ impl KeyType { } } -/// A KeySet binds together a triple of information about each Key on a card -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct KeySet { - signature: Option, - decryption: Option, - authentication: Option, +/// A struct to cache immutable information of a card. +/// Some of the data is stored during [`Card::new`]. +/// Other information can optionally be cached later (e.g. `ai`) +#[derive(Debug)] +struct CardImmutable { + aid: ApplicationIdentifier, + ec: ExtendedCapabilities, + hb: Option, // new in v2.0 + eli: Option, // new in v3.0 + + // First `Option` layer encodes if this cache field has been initialized, + // if `Some`, then the second `Option` layer encodes if the field exists on the card. + ai: Option>, // new in v3.4 } -impl From<(Option, Option, Option)> for KeySet { - fn from(tuple: (Option, Option, Option)) -> Self { - Self { - signature: tuple.0, - decryption: tuple.1, - authentication: tuple.2, +/// An OpenPGP card object (backed by a CardBackend implementation). +/// +/// Most users will probably want to use the `PcscCard` backend from the `card-backend-pcsc` crate. +/// +/// Users of this crate can keep a long lived [`Card`] object, including in long running programs. +/// All operations must be performed on a [`Transaction`] (which must be short lived). +pub struct Card { + /// A connection to the smart card + card: Box, + + /// Capabilites of the card, determined from hints by the Backend, + /// as well as the Application Related Data + card_caps: Option, + + /// A cache data structure for information that is immutable on OpenPGP cards. + /// Some of the information gets initialized when connecting to the card. + /// Other information my be cached on first read. + immutable: Option, +} + +impl Card { + /// Turn a [`CardBackend`] into a [`Card`] object: + /// + /// The OpenPGP application is `SELECT`ed, and the card capabilities + /// of the card are retrieved from the "Application Related Data". + pub fn new(backend: B) -> Result + where + B: Into>, + { + let card: Box = backend.into(); + + let mut op = Self { + card, + card_caps: None, + immutable: None, + }; + + let (caps, imm) = { + let mut tx = op.transaction()?; + tx.select()?; + + // Init card_caps + let ard = tx.application_related_data()?; + + // Determine chaining/extended length support from card + // metadata and cache this information in the CardTransaction + // implementation (as a CardCaps) + let mut ext_support = false; + let mut chaining_support = false; + + if let Ok(hist) = ard.historical_bytes() { + if let Some(cc) = hist.card_capabilities() { + chaining_support = cc.command_chaining(); + ext_support = cc.extended_lc_le(); + } + } + + let ext_cap = ard.extended_capabilities()?; + + // Get max command/response byte sizes from card + let (max_cmd_bytes, max_rsp_bytes) = if let Ok(Some(eli)) = + ard.extended_length_information() + { + // In card 3.x, max lengths come from ExtendedLengthInfo + (eli.max_command_bytes(), eli.max_response_bytes()) + } else if let (Some(cmd), Some(rsp)) = (ext_cap.max_cmd_len(), ext_cap.max_resp_len()) { + // In card 2.x, max lengths come from ExtendedCapabilities + (cmd, rsp) + } else { + // Fallback: use 255 if we have no information from the card + (255, 255) + }; + + let pw_status = ard.pw_status_bytes()?; + let pw1_max = pw_status.pw1_max_len(); + let pw3_max = pw_status.pw3_max_len(); + + let caps = CardCaps::new( + ext_support, + chaining_support, + max_cmd_bytes, + max_rsp_bytes, + pw1_max, + pw3_max, + ); + + let imm = CardImmutable { + aid: ard.application_id()?, + ec: ard.extended_capabilities()?, + hb: Some(ard.historical_bytes()?), + eli: ard.extended_length_information()?, + ai: None, // FIXME: initialize elsewhere? + }; + + drop(tx); + + // General mechanism to ask the backend for amendments to + // the CardCaps (e.g. to change support for "extended length") + // + // Also see https://blog.apdu.fr/posts/2011/05/extended-apdu-status-per-reader/ + let caps = op.card.limit_card_caps(caps); + + (caps, imm) + }; + + log::trace!("set card_caps to: {:x?}", caps); + op.card_caps = Some(caps); + + log::trace!("set immutable card state to: {:x?}", imm); + op.immutable = Some(imm); + + Ok(op) + } + + /// Get the internal `CardBackend`. + /// + /// This is useful to perform operations on the card with a different crate, + /// e.g. `yubikey-management`. + pub fn into_card(self) -> Box { + self.card + } + + /// Start a transaction on the underlying CardBackend. + /// The resulting [Transaction] object allows performing commands on the card. + /// + /// Note: Transactions on the Card cannot be long running. + /// They may be reset by the smart card subsystem within seconds, when idle. + pub fn transaction(&mut self) -> Result { + let card_caps = &mut self.card_caps; + let immutable = &mut self.immutable; // FIXME: unwrap + + let tx = self.card.transaction(Some(OPENPGP_APPLICATION))?; + + if tx.was_reset() { + // FIXME: Signal state invalidation to the library user? + // (E.g.: PIN verifications may have been lost.) + } + + Ok(Transaction { + tx, + card_caps, + immutable, + }) + } +} + +/// To perform commands on a [`Card`], a [`Transaction`] must be started. +/// This struct offers low-level access to OpenPGP card functionality. +/// +/// On backends that support transactions, operations are grouped together in transaction, while +/// an object of this type lives. +/// +/// A [`Transaction`] on typical underlying card subsystems must be short lived. +/// (Typically, smart cards can't be kept open for longer than a few seconds, +/// before they are automatically closed.) +pub struct Transaction<'a> { + tx: Box, + card_caps: &'a Option, + immutable: &'a mut Option, +} + +impl<'a> Transaction<'a> { + pub(crate) fn tx(&mut self) -> &mut dyn CardTransaction { + self.tx.as_mut() + } + + pub(crate) fn send_command( + &mut self, + cmd: Command, + expect_reply: bool, + ) -> Result { + apdu::send_command(&mut *self.tx, cmd, *self.card_caps, expect_reply) + } + + // SELECT + + /// Select the OpenPGP card application + pub fn select(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: select"); + + self.send_command(commands::select_openpgp(), false)? + .try_into() + } + + // TERMINATE DF + + /// 7.2.16 TERMINATE DF + pub fn terminate_df(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: terminate_df"); + + self.send_command(commands::terminate_df(), false)?; + Ok(()) + } + + // ACTIVATE FILE + + /// 7.2.17 ACTIVATE FILE + pub fn activate_file(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: activate_file"); + + self.send_command(commands::activate_file(), false)?; + Ok(()) + } + + // --- pinpad --- + + /// Does the reader support FEATURE_VERIFY_PIN_DIRECT? + pub fn feature_pinpad_verify(&self) -> bool { + self.tx.feature_pinpad_verify() + } + + /// Does the reader support FEATURE_MODIFY_PIN_DIRECT? + pub fn feature_pinpad_modify(&self) -> bool { + self.tx.feature_pinpad_modify() + } + + // --- get data --- + + /// Get the "application related data" from the card. + /// + /// (This data should probably be cached in a higher layer. Some parts of + /// it are needed regularly, and it does not usually change during + /// normal use of a card.) + pub fn application_related_data(&mut self) -> Result { + log::info!("OpenPgpTransaction: application_related_data"); + + let resp = self.send_command(commands::application_related_data(), true)?; + let value = Value::from(resp.data()?, true)?; + + log::trace!(" ARD value: {:02x?}", value); + + Ok(ApplicationRelatedData(Tlv::new( + Tags::ApplicationRelatedData, + value, + ))) + } + + // -- cached card data -- + + /// Get read access to cached immutable card information + fn card_immutable(&self) -> Result<&CardImmutable, Error> { + if let Some(imm) = &self.immutable { + Ok(imm) + } else { + // We expect that self.immutable has been initialized here + Err(Error::InternalError( + "Unexpected state of immutable cache".to_string(), + )) } } -} -impl KeySet { - pub fn signature(&self) -> Option<&T> { - self.signature.as_ref() + /// Application Identifier. + /// + /// This function returns data that is cached during initialization. + /// Calling it doesn't require sending a command to the card. + pub fn application_identifier(&self) -> Result { + Ok(self.card_immutable()?.aid) } - pub fn decryption(&self) -> Option<&T> { - self.decryption.as_ref() + /// Extended capabilities. + /// + /// This function returns data that is cached during initialization. + /// Calling it doesn't require sending a command to the card. + pub fn extended_capabilities(&self) -> Result { + Ok(self.card_immutable()?.ec) } - pub fn authentication(&self) -> Option<&T> { - self.authentication.as_ref() + /// Historical Bytes (if available). + /// + /// This function returns data that is cached during initialization. + /// Calling it doesn't require sending a command to the card. + pub fn historical_bytes(&self) -> Result, Error> { + Ok(self.card_immutable()?.hb) + } + + /// Extended length info (if available). + /// + /// This function returns data that is cached during initialization. + /// Calling it doesn't require sending a command to the card. + pub fn extended_length_info(&self) -> Result, Error> { + Ok(self.card_immutable()?.eli) + } + + #[allow(dead_code)] + fn algorithm_information_cached(&mut self) -> Result, Error> { + // FIXME: merge this fn with the regular/public `algorithm_information()` fn? + + if self.immutable.is_none() { + // We expect that self.immutable has been initialized here + return Err(Error::InternalError( + "Unexpected state of immutable cache".to_string(), + )); + } + + if self.immutable.as_ref().unwrap().ai.is_none() { + // Cached ai is unset, initialize it now! + + let ai = self.algorithm_information()?; + self.immutable.as_mut().unwrap().ai = Some(ai); + } + + Ok(self.immutable.as_ref().unwrap().ai.clone().unwrap()) + } + + // --- login data (5e) --- + + /// Get URL (5f50) + pub fn url(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: url"); + + self.send_command(commands::url(), true)?.try_into() + } + + /// Get Login Data (5e) + pub fn login_data(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: login_data"); + + self.send_command(commands::login_data(), true)?.try_into() + } + + /// Get cardholder related data (65) + pub fn cardholder_related_data(&mut self) -> Result { + log::info!("OpenPgpTransaction: cardholder_related_data"); + + let resp = self.send_command(commands::cardholder_related_data(), true)?; + + resp.data()?.try_into() + } + + /// Get security support template (7a) + pub fn security_support_template(&mut self) -> Result { + log::info!("OpenPgpTransaction: security_support_template"); + + let resp = self.send_command(commands::security_support_template(), true)?; + + let tlv = Tlv::try_from(resp.data()?)?; + + let dst = tlv.find(Tags::DigitalSignatureCounter).ok_or_else(|| { + Error::NotFound("Couldn't get DigitalSignatureCounter DO".to_string()) + })?; + + if let Value::S(data) = dst { + let mut data = data.to_vec(); + if data.len() != 3 { + return Err(Error::ParseError(format!( + "Unexpected length {} for DigitalSignatureCounter DO", + data.len() + ))); + } + + data.insert(0, 0); // prepend a zero + let data: [u8; 4] = data.try_into().unwrap(); + + let dsc: u32 = u32::from_be_bytes(data); + Ok(SecuritySupportTemplate { dsc }) + } else { + Err(Error::NotFound( + "Failed to process SecuritySupportTemplate".to_string(), + )) + } + } + + /// Get cardholder certificate (each for AUT, DEC and SIG). + /// + /// Call select_data() before calling this fn to select a particular + /// certificate (if the card supports multiple certificates). + /// + /// According to the OpenPGP card specification: + /// + /// The cardholder certificate DOs are designed to store a certificate (e. g. X.509) + /// for the keys in the card. They can be used to identify the card in a client-server + /// authentication, where specific non-OpenPGP-certificates are needed, for S-MIME and + /// other x.509 related functions. + /// + /// (See + /// for some discussion of the `cardholder certificate` OpenPGP card feature) + #[allow(dead_code)] + pub fn cardholder_certificate(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: cardholder_certificate"); + + self.send_command(commands::cardholder_certificate(), true)? + .try_into() + } + + /// Call "GET NEXT DATA" for the DO cardholder certificate. + /// + /// Cardholder certificate data for multiple slots can be read from the card by first calling + /// cardholder_certificate(), followed by up to two calls to next_cardholder_certificate(). + pub fn next_cardholder_certificate(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: next_cardholder_certificate"); + + self.send_command(commands::get_next_cardholder_certificate(), true)? + .try_into() + } + + /// Get "Algorithm Information" + pub fn algorithm_information(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: algorithm_information"); + + let resp = self.send_command(commands::algo_info(), true)?; + + let ai = resp.data()?.try_into()?; + Ok(Some(ai)) + } + + /// Get "Attestation Certificate (Yubico)" + pub fn attestation_certificate(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: attestation_certificate"); + + self.send_command(commands::attestation_certificate(), true)? + .try_into() + } + + /// Firmware Version (YubiKey specific (?)) + pub fn firmware_version(&mut self) -> Result, Error> { + log::info!("OpenPgpTransaction: firmware_version"); + + self.send_command(commands::firmware_version(), true)? + .try_into() + } + + /// Set identity (Nitrokey Start specific (?)). + /// [see: + /// + /// ] + pub fn set_identity(&mut self, id: u8) -> Result, Error> { + log::info!("OpenPgpTransaction: set_identity"); + + let resp = self.send_command(commands::set_identity(id), false); + + // Apparently it's normal to get "NotTransacted" from pcsclite when + // the identity switch was successful. + if let Err(Error::Smartcard(SmartcardError::NotTransacted)) = resp { + Ok(vec![]) + } else { + resp?.try_into() + } + } + + /// SELECT DATA ("select a DO in the current template"). + /// + /// This command currently only applies to + /// [`cardholder_certificate`](Transaction::cardholder_certificate) and + /// [`set_cardholder_certificate`](Transaction::set_cardholder_certificate) + /// in OpenPGP card. + + /// (This library leaves it up to consumers to decide on a strategy for dealing with this + /// issue. Possible strategies include: + /// - asking the card for its [`Transaction::firmware_version`] + /// and using the workaround if version <=5.4.3 + /// - trying this command first without the workaround, then with workaround if the card + /// returns [`StatusBytes::IncorrectParametersCommandDataField`] + /// - for read operations: using [`Transaction::next_cardholder_certificate`] + /// instead of SELECT DATA) + pub fn select_data(&mut self, num: u8, tag: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: select_data"); + + let tlv = Tlv::new( + Tags::GeneralReference, + Value::C(vec![Tlv::new(Tags::TagList, Value::S(tag.to_vec()))]), + ); + + let mut data = tlv.serialize(); + + // YubiKey 5 up to (and including) firmware version 5.4.3 need a workaround + // for this command. + // + // When sending the SELECT DATA command as defined in the card spec, without enabling the + // workaround, bad YubiKey firmware versions (<= 5.4.3) return + // `StatusBytes::IncorrectParametersCommandDataField` + // + // FIXME: caching for `firmware_version`? + if let Ok(version) = self.firmware_version() { + if version.len() == 3 + && version[0] == 5 + && (version[1] < 4 || (version[1] == 4 && version[2] <= 3)) + { + // Workaround for YubiKey 5. + // This hack is needed <= 5.4.3 according to ykman sources + // (see _select_certificate() in ykman/openpgp.py). + + assert!(data.len() <= 255); // catch blatant misuse: tags are 1-2 bytes long + + data.insert(0, data.len() as u8); + } + } + + let cmd = commands::select_data(num, data); + + // Possible response data (Control Parameter = CP) don't need to be evaluated by the + // application (See "7.2.5 SELECT DATA") + self.send_command(cmd, true)?.check_ok()?; + + Ok(()) + } + + // --- optional private DOs (0101 - 0104) --- + + /// Get data from "private use" DO. + /// + /// `num` must be between 1 and 4. + pub fn private_use_do(&mut self, num: u8) -> Result, Error> { + log::info!("OpenPgpTransaction: private_use_do"); + + if !(1..=4).contains(&num) { + return Err(Error::UnsupportedFeature(format!( + "Illegal Private Use DO num '{}'", + num, + ))); + } + + let cmd = commands::private_use_do(num); + self.send_command(cmd, true)?.try_into() + } + + // ---------- + + /// Reset all state on this OpenPGP card. + /// + /// Note: the "factory reset" operation is not directly offered by the + /// card spec. It is implemented as a series of OpenPGP card commands: + /// - send 4 bad requests to verify pw1, + /// - send 4 bad requests to verify pw3, + /// - terminate_df, + /// - activate_file. + /// + /// With most cards, this sequence of operations causes the card + /// to revert to a "blank" state. + /// + /// (However, e.g. vanilla Gnuk doesn't support this functionality. + /// Gnuk needs to be built with the `--enable-factory-reset` + /// option to the `configure` script to enable this functionality). + pub fn factory_reset(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: factory_reset"); + + // send 4 bad requests to verify pw1 + for _ in 0..4 { + let resp = self.verify_pw1_sign(&[0x40; 8]); + + if !(matches!( + resp, + Err(Error::CardStatus(StatusBytes::SecurityStatusNotSatisfied)) + | Err(Error::CardStatus(StatusBytes::AuthenticationMethodBlocked)) + | Err(Error::CardStatus( + StatusBytes::ExecutionErrorNonVolatileMemoryUnchanged + )) + | Err(Error::CardStatus(StatusBytes::PasswordNotChecked(_))) + )) { + return Err(Error::InternalError( + "Unexpected status for reset, at pw1.".into(), + )); + } + } + + // send 4 bad requests to verify pw3 + for _ in 0..4 { + let resp = self.verify_pw3(&[0x40; 8]); + + if !(matches!( + resp, + Err(Error::CardStatus(StatusBytes::SecurityStatusNotSatisfied)) + | Err(Error::CardStatus(StatusBytes::AuthenticationMethodBlocked)) + | Err(Error::CardStatus( + StatusBytes::ExecutionErrorNonVolatileMemoryUnchanged + )) + | Err(Error::CardStatus(StatusBytes::PasswordNotChecked(_))) + )) { + return Err(Error::InternalError( + "Unexpected status for reset, at pw3.".into(), + )); + } + } + + self.terminate_df()?; + self.activate_file()?; + + Ok(()) + } + + // --- verify/modify --- + + /// Verify pw1 (user) for signing operation (mode 81). + /// + /// Depending on the PW1 status byte (see Extended Capabilities) this + /// access condition is only valid for one PSO:CDS command or remains + /// valid for several attempts. + pub fn verify_pw1_sign(&mut self, pin: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw1_sign"); + + let cmd = commands::verify_pw1_81(pin.to_vec()); + + self.send_command(cmd, false)?.try_into() + } + + /// Verify pw1 (user) for signing operation (mode 81) using a + /// pinpad on the card reader. If no usable pinpad is found, an error + /// is returned. + /// + /// Depending on the PW1 status byte (see Extended Capabilities) this + /// access condition is only valid for one PSO:CDS command or remains + /// valid for several attempts. + pub fn verify_pw1_sign_pinpad(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw1_sign_pinpad"); + + let cc = *self.card_caps; + + let res = self.tx().pinpad_verify(PinType::Sign, &cc)?; + RawResponse::try_from(res)?.try_into() + } + + /// Check the current access of PW1 for signing (mode 81). + /// + /// If verification is not required, an empty Ok Response is returned. + /// + /// (Note: + /// - some cards don't correctly implement this feature, e.g. YubiKey 5 + /// - some cards that don't support this instruction may decrease the pin's error count, + /// eventually requiring the user to reset the pin) + pub fn check_pw1_sign(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: check_pw1_sign"); + + let verify = commands::verify_pw1_81(vec![]); + self.send_command(verify, false)?.try_into() + } + + /// Verify PW1 (user). + /// (For operations except signing, mode 82). + pub fn verify_pw1_user(&mut self, pin: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw1_user"); + + let verify = commands::verify_pw1_82(pin.to_vec()); + self.send_command(verify, false)?.try_into() + } + + /// Verify PW1 (user) for operations except signing (mode 82), + /// using a pinpad on the card reader. If no usable pinpad is found, + /// an error is returned. + + pub fn verify_pw1_user_pinpad(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw1_user_pinpad"); + + let cc = *self.card_caps; + + let res = self.tx().pinpad_verify(PinType::User, &cc)?; + RawResponse::try_from(res)?.try_into() + } + + /// Check the current access of PW1. + /// (For operations except signing, mode 82). + /// + /// If verification is not required, an empty Ok Response is returned. + /// + /// (Note: + /// - some cards don't correctly implement this feature, e.g. YubiKey 5 + /// - some cards that don't support this instruction may decrease the pin's error count, + /// eventually requiring the user to reset the pin) + pub fn check_pw1_user(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: check_pw1_user"); + + let verify = commands::verify_pw1_82(vec![]); + self.send_command(verify, false)?.try_into() + } + + /// Verify PW3 (admin). + pub fn verify_pw3(&mut self, pin: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw3"); + + let verify = commands::verify_pw3(pin.to_vec()); + self.send_command(verify, false)?.try_into() + } + + /// Verify PW3 (admin) using a pinpad on the card reader. If no usable + /// pinpad is found, an error is returned. + pub fn verify_pw3_pinpad(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: verify_pw3_pinpad"); + + let cc = *self.card_caps; + + let res = self.tx().pinpad_verify(PinType::Admin, &cc)?; + RawResponse::try_from(res)?.try_into() + } + + /// Check the current access of PW3 (admin). + /// + /// If verification is not required, an empty Ok Response is returned. + /// + /// (Note: + /// - some cards don't correctly implement this feature, e.g. YubiKey 5 + /// - some cards that don't support this instruction may decrease the pin's error count, + /// eventually requiring the user to factory reset the card) + pub fn check_pw3(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: check_pw3"); + + let verify = commands::verify_pw3(vec![]); + self.send_command(verify, false)?.try_into() + } + + /// Change the value of PW1 (user password). + /// + /// The current value of PW1 must be presented in `old` for authorization. + pub fn change_pw1(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: change_pw1"); + + let mut data = vec![]; + data.extend(old); + data.extend(new); + + let change = commands::change_pw1(data); + self.send_command(change, false)?.try_into() + } + + /// Change the value of PW1 (0x81) using a pinpad on the + /// card reader. If no usable pinpad is found, an error is returned. + pub fn change_pw1_pinpad(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: change_pw1_pinpad"); + + let cc = *self.card_caps; + + // Note: for change PW, only 0x81 and 0x83 are used! + // 0x82 is implicitly the same as 0x81. + let res = self.tx().pinpad_modify(PinType::Sign, &cc)?; + RawResponse::try_from(res)?.try_into() + } + + /// Change the value of PW3 (admin password). + /// + /// The current value of PW3 must be presented in `old` for authorization. + pub fn change_pw3(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: change_pw3"); + + let mut data = vec![]; + data.extend(old); + data.extend(new); + + let change = commands::change_pw3(data); + self.send_command(change, false)?.try_into() + } + + /// Change the value of PW3 (admin password) using a pinpad on the + /// card reader. If no usable pinpad is found, an error is returned. + pub fn change_pw3_pinpad(&mut self) -> Result<(), Error> { + log::info!("OpenPgpTransaction: change_pw3_pinpad"); + + let cc = *self.card_caps; + + let res = self.tx().pinpad_modify(PinType::Admin, &cc)?; + RawResponse::try_from(res)?.try_into() + } + + /// Reset the error counter for PW1 (user password) and set a new value + /// for PW1. + /// + /// For authorization, either: + /// - PW3 must have been verified previously, + /// - secure messaging must be currently used, + /// - the resetting_code must be presented. + pub fn reset_retry_counter_pw1( + &mut self, + new_pw1: &[u8], + resetting_code: Option<&[u8]>, + ) -> Result<(), Error> { + log::info!("OpenPgpTransaction: reset_retry_counter_pw1"); + + let cmd = commands::reset_retry_counter_pw1(resetting_code, new_pw1); + self.send_command(cmd, false)?.try_into() + } + + // --- decrypt --- + + /// Decrypt the ciphertext in `dm`, on the card. + /// + /// (This is a wrapper around the low-level pso_decipher + /// operation, it builds the required `data` field from `dm`) + pub fn decipher(&mut self, dm: Cryptogram) -> Result, Error> { + match dm { + Cryptogram::RSA(message) => { + // "Padding indicator byte (00) for RSA" (pg. 69) + let mut data = vec![0x0]; + data.extend_from_slice(message); + + // Call the card to decrypt `data` + self.pso_decipher(data) + } + Cryptogram::ECDH(eph) => { + // "In case of ECDH the card supports a partial decrypt + // only. The input is a cipher DO with the following data:" + // A6 xx Cipher DO + // -> 7F49 xx Public Key DO + // -> 86 xx External Public Key + + // External Public Key + let epk = Tlv::new(Tags::ExternalPublicKey, Value::S(eph.to_vec())); + + // Public Key DO + let pkdo = Tlv::new(Tags::PublicKey, Value::C(vec![epk])); + + // Cipher DO + let cdo = Tlv::new(Tags::Cipher, Value::C(vec![pkdo])); + + self.pso_decipher(cdo.serialize()) + } + } + } + + /// Run decryption operation on the smartcard (low level operation) + /// (7.2.11 PSO: DECIPHER) + /// + /// (consider using the [`Self::decipher`] method if you don't want to create + /// the data field manually) + pub fn pso_decipher(&mut self, data: Vec) -> Result, Error> { + log::info!("OpenPgpTransaction: pso_decipher"); + + // The OpenPGP card is already connected and PW1 82 has been verified + let dec_cmd = commands::decryption(data); + let resp = self.send_command(dec_cmd, true)?; + + Ok(resp.data()?.to_vec()) + } + + /// Set the key to be used for the pso_decipher and the internal_authenticate commands. + /// + /// Valid until next reset of of the card or the next call to `select` + /// The only keys that can be configured by this command are the `Decryption` and `Authentication` keys. + /// + /// The following first sets the *Authentication* key to be used for [`Self::pso_decipher`] + /// and then sets the *Decryption* key to be used for [`Self::internal_authenticate`]. + /// + /// ```no_run + /// # use openpgp_card::{KeyType, Transaction}; + /// # let mut tx: Transaction<'static> = panic!(); + /// tx.manage_security_environment(KeyType::Decryption, KeyType::Authentication)?; + /// tx.manage_security_environment(KeyType::Authentication, KeyType::Decryption)?; + /// # Result::<(), openpgp_card::Error>::Ok(()) + /// ``` + pub fn manage_security_environment( + &mut self, + for_operation: KeyType, + key_ref: KeyType, + ) -> Result<(), Error> { + log::info!("OpenPgpTransaction: manage_security_environment"); + + if !matches!(for_operation, KeyType::Authentication | KeyType::Decryption) + || !matches!(key_ref, KeyType::Authentication | KeyType::Decryption) + { + return Err(Error::UnsupportedAlgo("Only Decryption and Authentication keys can be manipulated by manage_security_environment".to_string())); + } + + let cmd = commands::manage_security_environment(for_operation, key_ref); + let resp = self.send_command(cmd, false)?; + resp.check_ok()?; + Ok(()) + } + + // --- sign --- + + /// Sign `hash`, on the card. + /// + /// This is a wrapper around the low-level + /// pso_compute_digital_signature operation. + /// It builds the required `data` field from `hash`. + /// + /// For RSA, this means a "DigestInfo" data structure is generated. + /// (see 7.2.10.2 DigestInfo for RSA). + /// + /// With ECC the hash data is processed as is, using + /// [`Self::pso_compute_digital_signature`]. + pub fn signature_for_hash(&mut self, hash: Hash) -> Result, Error> { + self.pso_compute_digital_signature(digestinfo(hash)) + } + + /// Run signing operation on the smartcard (low level operation) + /// (7.2.10 PSO: COMPUTE DIGITAL SIGNATURE) + /// + /// (consider using the [`Self::signature_for_hash`] method if you don't + /// want to create the data field manually) + pub fn pso_compute_digital_signature(&mut self, data: Vec) -> Result, Error> { + log::info!("OpenPgpTransaction: pso_compute_digital_signature"); + + let cds_cmd = commands::signature(data); + + let resp = self.send_command(cds_cmd, true)?; + + Ok(resp.data().map(|d| d.to_vec())?) + } + + // --- internal authenticate --- + + /// Auth-sign `hash`, on the card. + /// + /// This is a wrapper around the low-level + /// internal_authenticate operation. + /// It builds the required `data` field from `hash`. + /// + /// For RSA, this means a "DigestInfo" data structure is generated. + /// (see 7.2.10.2 DigestInfo for RSA). + /// + /// With ECC the hash data is processed as is. + pub fn authenticate_for_hash(&mut self, hash: Hash) -> Result, Error> { + self.internal_authenticate(digestinfo(hash)) + } + + /// Run signing operation on the smartcard (low level operation) + /// (7.2.13 INTERNAL AUTHENTICATE) + /// + /// (consider using the `authenticate_for_hash()` method if you don't + /// want to create the data field manually) + pub fn internal_authenticate(&mut self, data: Vec) -> Result, Error> { + log::info!("OpenPgpTransaction: internal_authenticate"); + + let ia_cmd = commands::internal_authenticate(data); + let resp = self.send_command(ia_cmd, true)?; + + Ok(resp.data().map(|d| d.to_vec())?) + } + + // --- PUT DO --- + + /// Set data of "private use" DO. + /// + /// `num` must be between 1 and 4. + /// + /// Access condition: + /// - 1/3 need PW1 (82) + /// - 2/4 need PW3 + pub fn set_private_use_do(&mut self, num: u8, data: Vec) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_private_use_do"); + + if !(1..=4).contains(&num) { + return Err(Error::UnsupportedFeature(format!( + "Illegal Private Use DO num '{}'", + num, + ))); + } + + let cmd = commands::put_private_use_do(num, data); + self.send_command(cmd, true)?.try_into() + } + + pub fn set_login(&mut self, login: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_login"); + + let cmd = commands::put_login_data(login.to_vec()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_name(&mut self, name: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_name"); + + let cmd = commands::put_name(name.to_vec()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_lang(&mut self, lang: &[Lang]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_lang"); + + let bytes: Vec<_> = lang.iter().flat_map(|&l| Vec::::from(l)).collect(); + + let cmd = commands::put_lang(bytes); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_sex(&mut self, sex: Sex) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_sex"); + + let cmd = commands::put_sex((&sex).into()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_url(&mut self, url: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_url"); + + let cmd = commands::put_url(url.to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set cardholder certificate (for AUT, DEC or SIG). + /// + /// Call select_data() before calling this fn to select a particular + /// certificate (if the card supports multiple certificates). + pub fn set_cardholder_certificate(&mut self, data: Vec) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_cardholder_certificate"); + + let cmd = commands::put_cardholder_certificate(data); + self.send_command(cmd, false)?.try_into() + } + + /// Set algorithm attributes for a key slot (4.4.3.9 Algorithm Attributes) + /// + /// Note: `algorithm_attributes` needs to precisely specify the + /// RSA bit-size of e (if applicable), and import format, with values + /// that the current card supports. + pub fn set_algorithm_attributes( + &mut self, + key_type: KeyType, + algorithm_attributes: &AlgorithmAttributes, + ) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_algorithm_attributes"); + + // Don't set algorithm if the feature is not available? + let ecap = self.extended_capabilities()?; + if !ecap.algo_attrs_changeable() { + // Don't change the algorithm attributes, if the card doesn't support change + // FIXME: Compare current and requested setting and return an error, if they differ? + + return Ok(()); + } + + // Command to PUT the algorithm attributes + let cmd = commands::put_data( + key_type.algorithm_tag(), + algorithm_attributes.to_data_object()?, + ); + + self.send_command(cmd, false)?.try_into() + } + + /// Set PW Status Bytes. + /// + /// If `long` is false, send 1 byte to the card, otherwise 4. + /// According to the spec, length information should not be changed. + /// + /// So, effectively, with 'long == false' the setting `pw1_cds_multi` + /// can be changed. + /// With 'long == true', the settings `pw1_pin_block` and `pw3_pin_block` + /// can also be changed. + /// + /// (See OpenPGP card spec, pg. 28) + pub fn set_pw_status_bytes( + &mut self, + pw_status: &PWStatusBytes, + long: bool, + ) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_pw_status_bytes"); + + let data = pw_status.serialize_for_put(long); + + let cmd = commands::put_pw_status(data); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_fingerprint(&mut self, fp: Fingerprint, key_type: KeyType) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_fingerprint"); + + let cmd = commands::put_data(key_type.fingerprint_put_tag(), fp.as_bytes().to_vec()); + + self.send_command(cmd, false)?.try_into() + } + + pub fn set_ca_fingerprint_1(&mut self, fp: Fingerprint) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_ca_fingerprint_1"); + + let cmd = commands::put_data(Tags::CaFingerprint1, fp.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_ca_fingerprint_2(&mut self, fp: Fingerprint) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_ca_fingerprint_2"); + + let cmd = commands::put_data(Tags::CaFingerprint2, fp.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_ca_fingerprint_3(&mut self, fp: Fingerprint) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_ca_fingerprint_3"); + + let cmd = commands::put_data(Tags::CaFingerprint3, fp.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + pub fn set_creation_time( + &mut self, + time: KeyGenerationTime, + key_type: KeyType, + ) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_creation_time"); + + // Timestamp update + let time_value: Vec = time.get().to_be_bytes().to_vec(); + + let cmd = commands::put_data(key_type.timestamp_put_tag(), time_value); + + self.send_command(cmd, false)?.try_into() + } + + // FIXME: optional DO SM-Key-ENC + + // FIXME: optional DO SM-Key-MAC + + /// Set resetting code + /// (4.3.4 Resetting Code) + pub fn set_resetting_code(&mut self, resetting_code: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_resetting_code"); + + let cmd = commands::put_data(Tags::ResettingCode, resetting_code.to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set AES key for symmetric decryption/encryption operations. + /// + /// Optional DO (announced in Extended Capabilities) for + /// PSO:ENC/DEC with AES (32 bytes dec. in case of + /// AES256, 16 bytes dec. in case of AES128). + pub fn set_pso_enc_dec_key(&mut self, key: &[u8]) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_pso_enc_dec_key"); + + let cmd = commands::put_data(Tags::PsoEncDecKey, key.to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set UIF for PSO:CDS + pub fn set_uif_pso_cds(&mut self, uif: &UserInteractionFlag) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_uif_pso_cds"); + + let cmd = commands::put_data(Tags::UifSig, uif.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set UIF for PSO:DEC + pub fn set_uif_pso_dec(&mut self, uif: &UserInteractionFlag) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_uif_pso_dec"); + + let cmd = commands::put_data(Tags::UifDec, uif.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set UIF for PSO:AUT + pub fn set_uif_pso_aut(&mut self, uif: &UserInteractionFlag) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_uif_pso_aut"); + + let cmd = commands::put_data(Tags::UifAuth, uif.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Set UIF for Attestation key + pub fn set_uif_attestation(&mut self, uif: &UserInteractionFlag) -> Result<(), Error> { + log::info!("OpenPgpTransaction: set_uif_attestation"); + + let cmd = commands::put_data(Tags::UifAttestation, uif.as_bytes().to_vec()); + self.send_command(cmd, false)?.try_into() + } + + /// Generate Attestation (Yubico) + pub fn generate_attestation(&mut self, key_type: KeyType) -> Result<(), Error> { + log::info!("OpenPgpTransaction: generate_attestation"); + + let key = match key_type { + KeyType::Signing => 0x01, + KeyType::Decryption => 0x02, + KeyType::Authentication => 0x03, + _ => return Err(Error::InternalError("Unexpected KeyType".to_string())), + }; + + let cmd = commands::generate_attestation(key); + self.send_command(cmd, false)?.try_into() + } + + // FIXME: Attestation key algo attr, FP, CA-FP, creation time + + // FIXME: SM keys (ENC and MAC) with Tags D1 and D2 + + // FIXME: KDF DO + + // FIXME: certificate used with secure messaging + + // FIXME: Attestation Certificate (Yubico) + + // ----------------- + + /// Import an existing private key to the card. + /// (This implicitly sets the algorithm attributes, fingerprint and timestamp) + pub fn key_import( + &mut self, + key: Box, + key_type: KeyType, + ) -> Result<(), Error> { + keys::key_import(self, key, key_type) + } + + /// Generate a key on the card. + /// (7.2.14 GENERATE ASYMMETRIC KEY PAIR) + pub fn generate_key( + &mut self, + fp_from_pub: fn( + &PublicKeyMaterial, + KeyGenerationTime, + KeyType, + ) -> Result, + key_type: KeyType, + ) -> Result<(PublicKeyMaterial, KeyGenerationTime), Error> { + // get current (possibly updated) state of algorithm_attributes + let ard = self.application_related_data()?; // no caching, here! + let cur_algo = ard.algorithm_attributes(key_type)?; + + keys::gen_key_set_metadata(self, fp_from_pub, &cur_algo, key_type) + } + + /// Get public key material from the card. + /// + /// Note: this fn returns a set of raw public key data (not an + /// OpenPGP data structure). + /// + /// Note also that the information from the card is insufficient to + /// reconstruct a pre-existing OpenPGP public key that corresponds to + /// the private key on the card. + pub fn public_key(&mut self, key_type: KeyType) -> Result { + keys::public_key(self, key_type) + } +} + +fn digestinfo(hash: Hash) -> Vec { + match hash { + Hash::SHA256(_) | Hash::SHA384(_) | Hash::SHA512(_) => { + let tlv = Tlv::new( + Tags::Sequence, + Value::C(vec![ + Tlv::new( + Tags::Sequence, + Value::C(vec![ + Tlv::new( + Tags::ObjectIdentifier, + // unwrapping is ok, for SHA* + Value::S(hash.oid().unwrap().to_vec()), + ), + Tlv::new(Tags::Null, Value::S(vec![])), + ]), + ), + Tlv::new(Tags::OctetString, Value::S(hash.digest().to_vec())), + ]), + ); + + tlv.serialize() + } + Hash::EdDSA(d) => d.to_vec(), + Hash::ECDSA(d) => d.to_vec(), } } diff --git a/openpgp-card/src/openpgp.rs b/openpgp-card/src/openpgp.rs deleted file mode 100644 index c69374f..0000000 --- a/openpgp-card/src/openpgp.rs +++ /dev/null @@ -1,1040 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::convert::{TryFrom, TryInto}; - -use crate::algorithm::{Algo, AlgoInfo, AlgoSimple}; -use crate::apdu::commands; -use crate::apdu::response::RawResponse; -use crate::card_do::{ - ApplicationRelatedData, CardholderRelatedData, Fingerprint, KeyGenerationTime, Lang, - PWStatusBytes, SecuritySupportTemplate, Sex, UIF, -}; -use crate::crypto_data::{CardUploadableKey, Cryptogram, Hash, PublicKeyMaterial}; -use crate::tlv::{value::Value, Tlv}; -use crate::{ - apdu, keys, CardBackend, CardTransaction, Error, KeyType, PinType, SmartcardError, StatusBytes, - Tag, Tags, -}; - -/// An OpenPGP card access object, backed by a CardBackend implementation. -/// -/// Most users will probably want to use the `PcscCard` backend from the `openpgp-card-pcsc` crate. -/// -/// Users of this crate can keep a long lived OpenPgp object. All operations must be performed on -/// a short lived `OpenPgpTransaction`. -pub struct OpenPgp { - card: Box, -} - -impl OpenPgp { - pub fn new(backend: B) -> Self - where - B: Into>, - { - Self { - card: backend.into(), - } - } - - /// Get an OpenPgpTransaction object. This starts a transaction on the underlying - /// CardBackend. - /// - /// Note: transactions on the Card cannot be long running, they will be reset within seconds - /// when idle. - pub fn transaction(&mut self) -> Result { - Ok(OpenPgpTransaction { - tx: self.card.transaction()?, - }) - } -} - -/// Low-level access to OpenPGP card functionality. -/// -/// On backends that support transactions, operations are grouped together in transaction, while -/// an object of this type lives. -/// -/// An OpenPgpTransaction on typical underlying card subsystems must be short lived. Typically, -/// smart cards can't be kept open for longer than a few seconds, before they are automatically -/// closed. -pub struct OpenPgpTransaction<'a> { - tx: Box, -} - -impl<'a> OpenPgpTransaction<'a> { - pub(crate) fn tx(&mut self) -> &mut dyn CardTransaction { - self.tx.as_mut() - } - - // --- pinpad --- - - /// Does the reader support FEATURE_VERIFY_PIN_DIRECT? - pub fn feature_pinpad_verify(&self) -> bool { - self.tx.feature_pinpad_verify() - } - - /// Does the reader support FEATURE_MODIFY_PIN_DIRECT? - pub fn feature_pinpad_modify(&self) -> bool { - self.tx.feature_pinpad_modify() - } - - // --- get data --- - - /// Get the "application related data" from the card. - /// - /// (This data should probably be cached in a higher layer. Some parts of - /// it are needed regularly, and it does not usually change during - /// normal use of a card.) - pub fn application_related_data(&mut self) -> Result { - log::info!("OpenPgpTransaction: application_related_data"); - - self.tx.application_related_data() - } - - // --- login data (5e) --- - - /// Get URL (5f50) - pub fn url(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: url"); - - let resp = apdu::send_command(self.tx(), commands::url(), true)?; - - Ok(resp.data()?.to_vec()) - } - - /// Get cardholder related data (65) - pub fn cardholder_related_data(&mut self) -> Result { - log::info!("OpenPgpTransaction: cardholder_related_data"); - - let crd = commands::cardholder_related_data(); - let resp = apdu::send_command(self.tx(), crd, true)?; - resp.check_ok()?; - - CardholderRelatedData::try_from(resp.data()?) - } - - /// Get security support template (7a) - pub fn security_support_template(&mut self) -> Result { - log::info!("OpenPgpTransaction: security_support_template"); - - let sst = commands::security_support_template(); - let resp = apdu::send_command(self.tx(), sst, true)?; - resp.check_ok()?; - - let tlv = Tlv::try_from(resp.data()?)?; - let res = tlv.find(Tag::from([0x93])).ok_or_else(|| { - Error::NotFound("Couldn't get SecuritySupportTemplate DO".to_string()) - })?; - - if let Value::S(data) = res { - let mut data = data.to_vec(); - if data.len() != 3 { - return Err(Error::ParseError(format!( - "Unexpected length {} for 'Digital signature counter' DO", - data.len() - ))); - } - - data.insert(0, 0); // prepend a zero - let data: [u8; 4] = data.try_into().unwrap(); - - let dsc: u32 = u32::from_be_bytes(data); - Ok(SecuritySupportTemplate { dsc }) - } else { - Err(Error::NotFound( - "Failed to process SecuritySupportTemplate".to_string(), - )) - } - } - - /// Get cardholder certificate (each for AUT, DEC and SIG). - /// - /// Call select_data() before calling this fn to select a particular - /// certificate (if the card supports multiple certificates). - /// - /// According to the OpenPGP card specification: - /// - /// The cardholder certificate DOs are designed to store a certificate (e. g. X.509) - /// for the keys in the card. They can be used to identify the card in a client-server - /// authentication, where specific non-OpenPGP-certificates are needed, for S-MIME and - /// other x.509 related functions. - /// - /// (See - /// for some discussion of the `cardholder certificate` OpenPGP card feature) - #[allow(dead_code)] - pub fn cardholder_certificate(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: cardholder_certificate"); - - let cmd = commands::cardholder_certificate(); - apdu::send_command(self.tx(), cmd, true)?.try_into() - } - - /// Call "GET NEXT DATA" for the DO cardholder certificate. - /// - /// Cardholder certificate data for multiple slots can be read from the card by first calling - /// cardholder_certificate(), followed by up to two calls to next_cardholder_certificate(). - pub fn next_cardholder_certificate(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: next_cardholder_certificate"); - - let cmd = commands::get_next_cardholder_certificate(); - apdu::send_command(self.tx(), cmd, true)?.try_into() - } - - /// Get "Algorithm Information" - pub fn algorithm_information(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: algorithm_information"); - - let resp = apdu::send_command(self.tx(), commands::algo_info(), true)?; - resp.check_ok()?; - - let ai = AlgoInfo::try_from(resp.data()?)?; - Ok(Some(ai)) - } - - /// Get "Attestation Certificate (Yubico)" - pub fn attestation_certificate(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: attestation_certificate"); - - let resp = apdu::send_command(self.tx(), commands::attestation_certificate(), true)?; - - Ok(resp.data()?.into()) - } - - /// Firmware Version (YubiKey specific (?)) - pub fn firmware_version(&mut self) -> Result, Error> { - log::info!("OpenPgpTransaction: firmware_version"); - - let resp = apdu::send_command(self.tx(), commands::firmware_version(), true)?; - - Ok(resp.data()?.into()) - } - - /// Set identity (Nitrokey Start specific (?)). - /// [see: - /// - /// ] - pub fn set_identity(&mut self, id: u8) -> Result, Error> { - log::info!("OpenPgpTransaction: set_identity"); - - let resp = apdu::send_command(self.tx(), commands::set_identity(id), false); - - // Apparently it's normal to get "NotTransacted" from pcsclite when - // the identity switch was successful. - if let Err(Error::Smartcard(SmartcardError::NotTransacted)) = resp { - Ok(vec![]) - } else { - Ok(resp?.data()?.into()) - } - } - - /// SELECT DATA ("select a DO in the current template"). - /// - /// This command currently only applies to - /// [`cardholder_certificate`](OpenPgpTransaction::cardholder_certificate) and - /// [`set_cardholder_certificate`](OpenPgpTransaction::set_cardholder_certificate) - /// in OpenPGP card. - /// - /// `yk_workaround`: YubiKey 5 up to (and including) firmware version 5.4.3 need a workaround - /// for this command. Set to `true` to apply this workaround. - /// (When sending the SELECT DATA command as defined in the card spec, without enabling the - /// workaround, bad YubiKey firmware versions (<= 5.4.3) return - /// [`IncorrectParametersCommandDataField`](StatusBytes::IncorrectParametersCommandDataField)) - /// - /// (This library leaves it up to consumers to decide on a strategy for dealing with this - /// issue. Possible strategies include: - /// - asking the card for its [`firmware_version`](OpenPgpTransaction::firmware_version) - /// and using the workaround if version <=5.4.3 - /// - trying this command first without the workaround, then with workaround if the card - /// returns - /// [`IncorrectParametersCommandDataField`](StatusBytes::IncorrectParametersCommandDataField) - /// - for read operations: using - /// [`next_cardholder_certificate`](OpenPgpTransaction::next_cardholder_certificate) - /// instead of SELECT DATA) - pub fn select_data(&mut self, num: u8, tag: &[u8], yk_workaround: bool) -> Result<(), Error> { - log::info!("OpenPgpTransaction: select_data"); - - let tlv = Tlv::new( - Tags::GeneralReference, - Value::C(vec![Tlv::new(Tags::TagList, Value::S(tag.to_vec()))]), - ); - - let mut data = tlv.serialize(); - - if yk_workaround { - // Workaround for YubiKey 5. - // This hack is needed <= 5.4.3 according to ykman sources - // (see _select_certificate() in ykman/openpgp.py). - - assert!(data.len() <= 255); // catch blatant misuse: tags are 1-2 bytes long - - data.insert(0, data.len() as u8); - } - - let cmd = commands::select_data(num, data); - - // Possible response data (Control Parameter = CP) don't need to be evaluated by the - // application (See "7.2.5 SELECT DATA") - apdu::send_command(self.tx(), cmd, true)?.try_into()?; - - Ok(()) - } - - // --- optional private DOs (0101 - 0104) --- - - /// Get data from "private use" DO. - /// - /// `num` must be between 1 and 4. - pub fn private_use_do(&mut self, num: u8) -> Result, Error> { - log::info!("OpenPgpTransaction: private_use_do"); - - assert!((1..=4).contains(&num)); - - let cmd = commands::private_use_do(num); - let resp = apdu::send_command(self.tx(), cmd, true)?; - - Ok(resp.data()?.to_vec()) - } - - // ---------- - - /// Reset all state on this OpenPGP card. - /// - /// Note: the "factory reset" operation is not directly offered by the - /// card spec. It is implemented as a series of OpenPGP card commands: - /// - send 4 bad requests to verify pw1, - /// - send 4 bad requests to verify pw3, - /// - terminate_df, - /// - activate_file. - /// - /// With most cards, this sequence of operations causes the card - /// to revert to a "blank" state. - /// - /// (However, e.g. vanilla Gnuk doesn't support this functionality. - /// Gnuk needs to be built with the `--enable-factory-reset` - /// option to the `configure` script to enable this functionality). - pub fn factory_reset(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: factory_reset"); - - // send 4 bad requests to verify pw1 - // [apdu 00 20 00 81 08 40 40 40 40 40 40 40 40] - for _ in 0..4 { - log::info!(" verify_pw1_81"); - let verify = commands::verify_pw1_81([0x40; 8].to_vec()); - let resp = apdu::send_command(self.tx(), verify, false)?; - if !(resp.status() == StatusBytes::SecurityStatusNotSatisfied - || resp.status() == StatusBytes::AuthenticationMethodBlocked - || matches!(resp.status(), StatusBytes::PasswordNotChecked(_))) - { - return Err(Error::InternalError( - "Unexpected status for reset, at pw1.".into(), - )); - } - } - - // send 4 bad requests to verify pw3 - // [apdu 00 20 00 83 08 40 40 40 40 40 40 40 40] - for _ in 0..4 { - log::info!(" verify_pw3"); - let verify = commands::verify_pw3([0x40; 8].to_vec()); - let resp = apdu::send_command(self.tx(), verify, false)?; - - if !(resp.status() == StatusBytes::SecurityStatusNotSatisfied - || resp.status() == StatusBytes::AuthenticationMethodBlocked - || matches!(resp.status(), StatusBytes::PasswordNotChecked(_))) - { - return Err(Error::InternalError( - "Unexpected status for reset, at pw3.".into(), - )); - } - } - - // terminate_df [apdu 00 e6 00 00] - log::info!(" terminate_df"); - let term = commands::terminate_df(); - let resp = apdu::send_command(self.tx(), term, false)?; - resp.check_ok()?; - - // activate_file [apdu 00 44 00 00] - log::info!(" activate_file"); - let act = commands::activate_file(); - let resp = apdu::send_command(self.tx(), act, false)?; - resp.check_ok()?; - - Ok(()) - } - - // --- verify/modify --- - - /// Verify pw1 (user) for signing operation (mode 81). - /// - /// Depending on the PW1 status byte (see Extended Capabilities) this - /// access condition is only valid for one PSO:CDS command or remains - /// valid for several attempts. - pub fn verify_pw1_sign(&mut self, pin: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw1_sign"); - - let verify = commands::verify_pw1_81(pin.to_vec()); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Verify pw1 (user) for signing operation (mode 81) using a - /// pinpad on the card reader. If no usable pinpad is found, an error - /// is returned. - /// - /// Depending on the PW1 status byte (see Extended Capabilities) this - /// access condition is only valid for one PSO:CDS command or remains - /// valid for several attempts. - pub fn verify_pw1_sign_pinpad(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw1_sign_pinpad"); - - let res = self.tx().pinpad_verify(PinType::Sign)?; - RawResponse::try_from(res)?.try_into() - } - - /// Check the current access of PW1 for signing (mode 81). - /// - /// If verification is not required, an empty Ok Response is returned. - /// - /// (Note: - /// - some cards don't correctly implement this feature, e.g. YubiKey 5 - /// - some cards that don't support this instruction may decrease the pin's error count, - /// eventually requiring the user to reset the pin) - pub fn check_pw1_sign(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: check_pw1_sign"); - - let verify = commands::verify_pw1_81(vec![]); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Verify PW1 (user). - /// (For operations except signing, mode 82). - pub fn verify_pw1_user(&mut self, pin: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw1_user"); - - let verify = commands::verify_pw1_82(pin.to_vec()); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Verify PW1 (user) for operations except signing (mode 82), - /// using a pinpad on the card reader. If no usable pinpad is found, - /// an error is returned. - - pub fn verify_pw1_user_pinpad(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw1_user_pinpad"); - - let res = self.tx().pinpad_verify(PinType::User)?; - RawResponse::try_from(res)?.try_into() - } - - /// Check the current access of PW1. - /// (For operations except signing, mode 82). - /// - /// If verification is not required, an empty Ok Response is returned. - /// - /// (Note: - /// - some cards don't correctly implement this feature, e.g. YubiKey 5 - /// - some cards that don't support this instruction may decrease the pin's error count, - /// eventually requiring the user to reset the pin) - pub fn check_pw1_user(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: check_pw1_user"); - - let verify = commands::verify_pw1_82(vec![]); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Verify PW3 (admin). - pub fn verify_pw3(&mut self, pin: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw3"); - - let verify = commands::verify_pw3(pin.to_vec()); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Verify PW3 (admin) using a pinpad on the card reader. If no usable - /// pinpad is found, an error is returned. - pub fn verify_pw3_pinpad(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: verify_pw3_pinpad"); - - let res = self.tx().pinpad_verify(PinType::Admin)?; - RawResponse::try_from(res)?.try_into() - } - - /// Check the current access of PW3 (admin). - /// - /// If verification is not required, an empty Ok Response is returned. - /// - /// (Note: - /// - some cards don't correctly implement this feature, e.g. YubiKey 5 - /// - some cards that don't support this instruction may decrease the pin's error count, - /// eventually requiring the user to reset the pin) - pub fn check_pw3(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: check_pw3"); - - let verify = commands::verify_pw3(vec![]); - apdu::send_command(self.tx(), verify, false)?.try_into() - } - - /// Change the value of PW1 (user password). - /// - /// The current value of PW1 must be presented in `old` for authorization. - pub fn change_pw1(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: change_pw1"); - - let mut data = vec![]; - data.extend(old); - data.extend(new); - - let change = commands::change_pw1(data); - apdu::send_command(self.tx(), change, false)?.try_into() - } - - /// Change the value of PW1 (0x81) using a pinpad on the - /// card reader. If no usable pinpad is found, an error is returned. - pub fn change_pw1_pinpad(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: change_pw1_pinpad"); - - // Note: for change PW, only 0x81 and 0x83 are used! - // 0x82 is implicitly the same as 0x81. - let res = self.tx().pinpad_modify(PinType::Sign)?; - RawResponse::try_from(res)?.try_into() - } - - /// Change the value of PW3 (admin password). - /// - /// The current value of PW3 must be presented in `old` for authorization. - pub fn change_pw3(&mut self, old: &[u8], new: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: change_pw3"); - - let mut data = vec![]; - data.extend(old); - data.extend(new); - - let change = commands::change_pw3(data); - apdu::send_command(self.tx(), change, false)?.try_into() - } - - /// Change the value of PW3 (admin password) using a pinpad on the - /// card reader. If no usable pinpad is found, an error is returned. - pub fn change_pw3_pinpad(&mut self) -> Result<(), Error> { - log::info!("OpenPgpTransaction: change_pw3_pinpad"); - - let res = self.tx().pinpad_modify(PinType::Admin)?; - RawResponse::try_from(res)?.try_into() - } - - /// Reset the error counter for PW1 (user password) and set a new value - /// for PW1. - /// - /// For authorization, either: - /// - PW3 must have been verified previously, - /// - secure messaging must be currently used, - /// - the resetting_code must be presented. - pub fn reset_retry_counter_pw1( - &mut self, - new_pw1: &[u8], - resetting_code: Option<&[u8]>, - ) -> Result<(), Error> { - log::info!("OpenPgpTransaction: reset_retry_counter_pw1"); - - let reset = commands::reset_retry_counter_pw1(resetting_code, new_pw1); - apdu::send_command(self.tx(), reset, false)?.try_into() - } - - // --- decrypt --- - - /// Decrypt the ciphertext in `dm`, on the card. - /// - /// (This is a wrapper around the low-level pso_decipher - /// operation, it builds the required `data` field from `dm`) - pub fn decipher(&mut self, dm: Cryptogram) -> Result, Error> { - match dm { - Cryptogram::RSA(message) => { - // "Padding indicator byte (00) for RSA" (pg. 69) - let mut data = vec![0x0]; - data.extend_from_slice(message); - - // Call the card to decrypt `data` - self.pso_decipher(data) - } - Cryptogram::ECDH(eph) => { - // "In case of ECDH the card supports a partial decrypt - // only. The input is a cipher DO with the following data:" - // A6 xx Cipher DO - // -> 7F49 xx Public Key DO - // -> 86 xx External Public Key - - // External Public Key - let epk = Tlv::new(Tags::ExternalPublicKey, Value::S(eph.to_vec())); - - // Public Key DO - let pkdo = Tlv::new(Tags::PublicKey, Value::C(vec![epk])); - - // Cipher DO - let cdo = Tlv::new(Tags::Cipher, Value::C(vec![pkdo])); - - self.pso_decipher(cdo.serialize()) - } - } - } - - /// Run decryption operation on the smartcard (low level operation) - /// (7.2.11 PSO: DECIPHER) - /// - /// (consider using the `decipher()` method if you don't want to create - /// the data field manually) - pub fn pso_decipher(&mut self, data: Vec) -> Result, Error> { - log::info!("OpenPgpTransaction: pso_decipher"); - - // The OpenPGP card is already connected and PW1 82 has been verified - let dec_cmd = commands::decryption(data); - let resp = apdu::send_command(self.tx(), dec_cmd, true)?; - resp.check_ok()?; - - Ok(resp.data().map(|d| d.to_vec())?) - } - - /// Set the key to be used for the pso_decipher and the internal_authenticate commands. - /// - /// Valid until next reset of of the card or the next call to `select` - /// The only keys that can be configured by this command are the `Decryption` and `Authentication` keys. - /// - /// The following first sets the *Authentication* key to be used for [pso_decipher](OpenPgpTransaction::pso_decipher) - /// and then sets the *Decryption* key to be used for [internal_authenticate](OpenPgpTransaction::internal_authenticate). - /// - /// ```no_run - /// # use openpgp_card::{KeyType, OpenPgpTransaction}; - /// # let mut tx: OpenPgpTransaction<'static> = panic!(); - /// tx.manage_security_environment(KeyType::Decryption, KeyType::Authentication)?; - /// tx.manage_security_environment(KeyType::Authentication, KeyType::Decryption)?; - /// # Result::<(), openpgp_card::Error>::Ok(()) - /// ``` - pub fn manage_security_environment( - &mut self, - for_operation: KeyType, - key_ref: KeyType, - ) -> Result<(), Error> { - log::info!("OpenPgpTransaction: manage_security_environment"); - - if !matches!(for_operation, KeyType::Authentication | KeyType::Decryption) - || !matches!(key_ref, KeyType::Authentication | KeyType::Decryption) - { - return Err(Error::UnsupportedAlgo("Only Decryption and Authentication keys can be manipulated by manage_security_environment".to_string())); - } - - let cmd = commands::manage_security_environment(for_operation, key_ref); - let resp = apdu::send_command(self.tx(), cmd, false)?; - resp.check_ok()?; - Ok(()) - } - - // --- sign --- - - /// Sign `hash`, on the card. - /// - /// This is a wrapper around the low-level - /// pso_compute_digital_signature operation. - /// It builds the required `data` field from `hash`. - /// - /// For RSA, this means a "DigestInfo" data structure is generated. - /// (see 7.2.10.2 DigestInfo for RSA). - /// - /// With ECC the hash data is processed as is, using - /// pso_compute_digital_signature. - pub fn signature_for_hash(&mut self, hash: Hash) -> Result, Error> { - self.pso_compute_digital_signature(digestinfo(hash)) - } - - /// Run signing operation on the smartcard (low level operation) - /// (7.2.10 PSO: COMPUTE DIGITAL SIGNATURE) - /// - /// (consider using the `signature_for_hash()` method if you don't - /// want to create the data field manually) - pub fn pso_compute_digital_signature(&mut self, data: Vec) -> Result, Error> { - log::info!("OpenPgpTransaction: pso_compute_digital_signature"); - - let cds_cmd = commands::signature(data); - - let resp = apdu::send_command(self.tx(), cds_cmd, true)?; - - Ok(resp.data().map(|d| d.to_vec())?) - } - - // --- internal authenticate --- - - /// Auth-sign `hash`, on the card. - /// - /// This is a wrapper around the low-level - /// internal_authenticate operation. - /// It builds the required `data` field from `hash`. - /// - /// For RSA, this means a "DigestInfo" data structure is generated. - /// (see 7.2.10.2 DigestInfo for RSA). - /// - /// With ECC the hash data is processed as is. - pub fn authenticate_for_hash(&mut self, hash: Hash) -> Result, Error> { - self.internal_authenticate(digestinfo(hash)) - } - - /// Run signing operation on the smartcard (low level operation) - /// (7.2.13 INTERNAL AUTHENTICATE) - /// - /// (consider using the `authenticate_for_hash()` method if you don't - /// want to create the data field manually) - pub fn internal_authenticate(&mut self, data: Vec) -> Result, Error> { - log::info!("OpenPgpTransaction: internal_authenticate"); - - let ia_cmd = commands::internal_authenticate(data); - let resp = apdu::send_command(self.tx(), ia_cmd, true)?; - - Ok(resp.data().map(|d| d.to_vec())?) - } - - // --- PUT DO --- - - /// Set data of "private use" DO. - /// - /// `num` must be between 1 and 4. - /// - /// Access condition: - /// - 1/3 need PW1 (82) - /// - 2/4 need PW3 - pub fn set_private_use_do(&mut self, num: u8, data: Vec) -> Result, Error> { - log::info!("OpenPgpTransaction: set_private_use_do"); - - assert!((1..=4).contains(&num)); - - let cmd = commands::put_private_use_do(num, data); - let resp = apdu::send_command(self.tx(), cmd, true)?; - - Ok(resp.data()?.to_vec()) - } - - pub fn set_name(&mut self, name: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_name"); - - let put_name = commands::put_name(name.to_vec()); - apdu::send_command(self.tx(), put_name, false)?.try_into() - } - - pub fn set_lang(&mut self, lang: &[Lang]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_lang"); - - let bytes: Vec = lang - .iter() - .flat_map(|&l| Into::>::into(l)) - .collect(); - - let put_lang = commands::put_lang(bytes); - apdu::send_command(self.tx(), put_lang, false)?.try_into() - } - - pub fn set_sex(&mut self, sex: Sex) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_sex"); - - let put_sex = commands::put_sex((&sex).into()); - apdu::send_command(self.tx(), put_sex, false)?.try_into() - } - - pub fn set_url(&mut self, url: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_url"); - - let put_url = commands::put_url(url.to_vec()); - apdu::send_command(self.tx(), put_url, false)?.try_into() - } - - /// Set cardholder certificate (for AUT, DEC or SIG). - /// - /// Call select_data() before calling this fn to select a particular - /// certificate (if the card supports multiple certificates). - pub fn set_cardholder_certificate(&mut self, data: Vec) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_cardholder_certificate"); - - let cmd = commands::put_cardholder_certificate(data); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set algorithm attributes - /// (4.4.3.9 Algorithm Attributes) - pub fn set_algorithm_attributes( - &mut self, - key_type: KeyType, - algo: &Algo, - ) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_algorithm_attributes"); - - // Command to PUT the algorithm attributes - let cmd = commands::put_data(key_type.algorithm_tag(), algo.to_data_object()?); - - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set PW Status Bytes. - /// - /// If `long` is false, send 1 byte to the card, otherwise 4. - /// According to the spec, length information should not be changed. - /// - /// So, effectively, with 'long == false' the setting `pw1_cds_multi` - /// can be changed. - /// With 'long == true', the settings `pw1_pin_block` and `pw3_pin_block` - /// can also be changed. - /// - /// (See OpenPGP card spec, pg. 28) - pub fn set_pw_status_bytes( - &mut self, - pw_status: &PWStatusBytes, - long: bool, - ) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_pw_status_bytes"); - - let data = pw_status.serialize_for_put(long); - - let cmd = commands::put_pw_status(data); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - pub fn set_fingerprint(&mut self, fp: Fingerprint, key_type: KeyType) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_fingerprint"); - - let fp_cmd = commands::put_data(key_type.fingerprint_put_tag(), fp.as_bytes().to_vec()); - - apdu::send_command(self.tx(), fp_cmd, false)?.try_into() - } - - pub fn set_ca_fingerprint_1(&mut self, fp: Fingerprint) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_ca_fingerprint_1"); - - let fp_cmd = commands::put_data(Tags::CaFingerprint1, fp.as_bytes().to_vec()); - apdu::send_command(self.tx(), fp_cmd, false)?.try_into() - } - - pub fn set_ca_fingerprint_2(&mut self, fp: Fingerprint) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_ca_fingerprint_2"); - - let fp_cmd = commands::put_data(Tags::CaFingerprint2, fp.as_bytes().to_vec()); - apdu::send_command(self.tx(), fp_cmd, false)?.try_into() - } - - pub fn set_ca_fingerprint_3(&mut self, fp: Fingerprint) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_ca_fingerprint_3"); - - let fp_cmd = commands::put_data(Tags::CaFingerprint3, fp.as_bytes().to_vec()); - apdu::send_command(self.tx(), fp_cmd, false)?.try_into() - } - - pub fn set_creation_time( - &mut self, - time: KeyGenerationTime, - key_type: KeyType, - ) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_creation_time"); - - // Timestamp update - let time_value: Vec = time - .get() - .to_be_bytes() - .iter() - .skip_while(|&&e| e == 0) - .copied() - .collect(); - - let time_cmd = commands::put_data(key_type.timestamp_put_tag(), time_value); - - apdu::send_command(self.tx(), time_cmd, false)?.try_into() - } - - // FIXME: optional DO SM-Key-ENC - - // FIXME: optional DO SM-Key-MAC - - /// Set resetting code - /// (4.3.4 Resetting Code) - pub fn set_resetting_code(&mut self, resetting_code: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_resetting_code"); - - let cmd = commands::put_data(Tags::ResettingCode, resetting_code.to_vec()); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set AES key for symmetric decryption/encryption operations. - /// - /// Optional DO (announced in Extended Capabilities) for - /// PSO:ENC/DEC with AES (32 bytes dec. in case of - /// AES256, 16 bytes dec. in case of AES128). - pub fn set_pso_enc_dec_key(&mut self, key: &[u8]) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_pso_enc_dec_key"); - - let fp_cmd = commands::put_data(Tags::PsoEncDecKey, key.to_vec()); - - apdu::send_command(self.tx(), fp_cmd, false)?.try_into() - } - - // FIXME: optional DO for PSO:ENC/DEC with AES - - /// Set UIF for PSO:CDS - pub fn set_uif_pso_cds(&mut self, uif: &UIF) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_uif_pso_cds"); - - let cmd = commands::put_data(Tags::UifSig, uif.as_bytes().to_vec()); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set UIF for PSO:DEC - pub fn set_uif_pso_dec(&mut self, uif: &UIF) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_uif_pso_dec"); - - let cmd = commands::put_data(Tags::UifDec, uif.as_bytes().to_vec()); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set UIF for PSO:AUT - pub fn set_uif_pso_aut(&mut self, uif: &UIF) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_uif_pso_aut"); - - let cmd = commands::put_data(Tags::UifAuth, uif.as_bytes().to_vec()); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Set UIF for Attestation key - pub fn set_uif_attestation(&mut self, uif: &UIF) -> Result<(), Error> { - log::info!("OpenPgpTransaction: set_uif_attestation"); - - let cmd = commands::put_data(Tags::UifAttestation, uif.as_bytes().to_vec()); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - /// Generate Attestation (Yubico) - pub fn generate_attestation(&mut self, key_type: KeyType) -> Result<(), Error> { - log::info!("OpenPgpTransaction: generate_attestation"); - - let key = match key_type { - KeyType::Signing => 0x01, - KeyType::Decryption => 0x02, - KeyType::Authentication => 0x03, - _ => return Err(Error::InternalError("Unexpected KeyType".to_string())), - }; - - let cmd = commands::generate_attestation(key); - apdu::send_command(self.tx(), cmd, false)?.try_into() - } - - // FIXME: Attestation key algo attr, FP, CA-FP, creation time - - // FIXME: SM keys (ENC and MAC) with Tags D1 and D2 - - // FIXME: KDF DO - - // FIXME: certificate used with secure messaging - - // FIXME: Attestation Certificate (Yubico) - - // ----------------- - - /// Import an existing private key to the card. - /// (This implicitly sets the algorithm info, fingerprint and timestamp) - pub fn key_import( - &mut self, - key: Box, - key_type: KeyType, - ) -> Result<(), Error> { - let algo_info = self.algorithm_information(); - - // An error is ok - it's fine if a card doesn't offer a list of - // supported algorithms - let algo_info = algo_info.unwrap_or(None); - - keys::key_import(self, key, key_type, algo_info) - } - - /// Generate a key on the card. - /// (7.2.14 GENERATE ASYMMETRIC KEY PAIR) - /// - /// If the `algo` parameter is Some, then this algorithm will be set on - /// the card for "key_type". - /// - /// Note: `algo` needs to precisely specify the RSA bitsize of e (if - /// applicable), and import format, with values that the current card - /// supports. - pub fn generate_key( - &mut self, - fp_from_pub: fn( - &PublicKeyMaterial, - KeyGenerationTime, - KeyType, - ) -> Result, - key_type: KeyType, - algo: Option<&Algo>, - ) -> Result<(PublicKeyMaterial, KeyGenerationTime), Error> { - keys::gen_key_with_metadata(self, fp_from_pub, key_type, algo) - } - - /// Generate a key on the card. - /// (7.2.14 GENERATE ASYMMETRIC KEY PAIR) - /// - /// This is a wrapper around generate_key() which allows - /// using the simplified `AlgoSimple` algorithm selector enum. - /// - /// Note: AlgoSimple doesn't specify card specific details (such as - /// bitsize of e for RSA, and import format). This function determines - /// these values based on information from the card. - pub fn generate_key_simple( - &mut self, - fp_from_pub: fn( - &PublicKeyMaterial, - KeyGenerationTime, - KeyType, - ) -> Result, - key_type: KeyType, - simple: AlgoSimple, - ) -> Result<(PublicKeyMaterial, KeyGenerationTime), Error> { - let ard = self.application_related_data()?; - let algo_info = if let Ok(ai) = self.algorithm_information() { - ai - } else { - None - }; - - let algo = simple.determine_algo(key_type, &ard, algo_info)?; - - Self::generate_key(self, fp_from_pub, key_type, Some(&algo)) - } - - /// Get public key material from the card. - /// - /// Note: this fn returns a set of raw public key data (not an - /// OpenPGP data structure). - /// - /// Note also that the information from the card is insufficient to - /// reconstruct a pre-existing OpenPGP public key that corresponds to - /// the private key on the card. - pub fn public_key(&mut self, key_type: KeyType) -> Result { - keys::public_key(self, key_type) - } -} - -fn digestinfo(hash: Hash) -> Vec { - match hash { - Hash::SHA256(_) | Hash::SHA384(_) | Hash::SHA512(_) => { - let tlv = Tlv::new( - Tags::Sequence, - Value::C(vec![ - Tlv::new( - Tags::Sequence, - Value::C(vec![ - Tlv::new( - Tags::ObjectIdentifier, - // unwrapping is ok, for SHA* - Value::S(hash.oid().unwrap().to_vec()), - ), - Tlv::new(Tags::Null, Value::S(vec![])), - ]), - ), - Tlv::new(Tags::OctetString, Value::S(hash.digest().to_vec())), - ]), - ); - - tlv.serialize() - } - Hash::EdDSA(d) => d.to_vec(), - Hash::ECDSA(d) => d.to_vec(), - } -} diff --git a/openpgp-card/src/tags.rs b/openpgp-card/src/tags.rs new file mode 100644 index 0000000..bd8b1af --- /dev/null +++ b/openpgp-card/src/tags.rs @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::tlv::tag::Tag; + +/// Tags, as specified and used in the OpenPGP card 3.4.1 spec. +/// All tags in OpenPGP card are either 1 or 2 bytes long. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[non_exhaustive] +#[allow(dead_code)] +pub(crate) enum Tags { + // BER identifiers + OctetString, + Null, + ObjectIdentifier, + Sequence, + + // GET DATA + PrivateUse1, + PrivateUse2, + PrivateUse3, + PrivateUse4, + ApplicationIdentifier, + LoginData, + Url, + HistoricalBytes, + CardholderRelatedData, + Name, + LanguagePref, + Sex, + ApplicationRelatedData, + ExtendedLengthInformation, + GeneralFeatureManagement, + DiscretionaryDataObjects, + ExtendedCapabilities, + AlgorithmAttributesSignature, + AlgorithmAttributesDecryption, + AlgorithmAttributesAuthentication, + PWStatusBytes, + Fingerprints, + CaFingerprints, + GenerationTimes, + KeyInformation, + UifSig, + UifDec, + UifAuth, + UifAttestation, + SecuritySupportTemplate, + DigitalSignatureCounter, + CardholderCertificate, + AlgorithmAttributesAttestation, + FingerprintAttestation, + CaFingerprintAttestation, + GenerationTimeAttestation, + KdfDo, + AlgorithmInformation, + CertificateSecureMessaging, + AttestationCertificate, + + // PUT DATA (additional Tags that don't get used for GET DATA) + FingerprintSignature, + FingerprintDecryption, + FingerprintAuthentication, + CaFingerprint1, + CaFingerprint2, + CaFingerprint3, + GenerationTimeSignature, + GenerationTimeDecryption, + GenerationTimeAuthentication, + // FIXME: +D1, D2 + ResettingCode, + PsoEncDecKey, + + // OTHER + // 4.4.3.12 Private Key Template + ExtendedHeaderList, + CardholderPrivateKeyTemplate, + ConcatenatedKeyData, + CrtKeySignature, + CrtKeyConfidentiality, + CrtKeyAuthentication, + PrivateKeyDataRsaPublicExponent, + PrivateKeyDataRsaPrime1, + PrivateKeyDataRsaPrime2, + PrivateKeyDataRsaPq, + PrivateKeyDataRsaDp1, + PrivateKeyDataRsaDq1, + PrivateKeyDataRsaModulus, + PrivateKeyDataEccPrivateKey, + PrivateKeyDataEccPublicKey, + + // 7.2.14 GENERATE ASYMMETRIC KEY PAIR + PublicKey, + PublicKeyDataRsaModulus, + PublicKeyDataRsaExponent, + PublicKeyDataEccPoint, + + // 7.2.11 PSO: DECIPHER + Cipher, + ExternalPublicKey, + + // 7.2.5 SELECT DATA + GeneralReference, + TagList, +} + +impl From for Vec { + fn from(t: Tags) -> Self { + ShortTag::from(t).into() + } +} + +impl From for Tag { + fn from(t: Tags) -> Self { + ShortTag::from(t).into() + } +} + +impl From for ShortTag { + fn from(t: Tags) -> Self { + match t { + // BER identifiers https://en.wikipedia.org/wiki/X.690#BER_encoding + Tags::OctetString => [0x04].into(), + Tags::Null => [0x05].into(), + Tags::ObjectIdentifier => [0x06].into(), + Tags::Sequence => [0x30].into(), + + // GET DATA + Tags::PrivateUse1 => [0x01, 0x01].into(), + Tags::PrivateUse2 => [0x01, 0x02].into(), + Tags::PrivateUse3 => [0x01, 0x03].into(), + Tags::PrivateUse4 => [0x01, 0x04].into(), + Tags::ApplicationIdentifier => [0x4f].into(), + Tags::LoginData => [0x5e].into(), + Tags::Url => [0x5f, 0x50].into(), + Tags::HistoricalBytes => [0x5f, 0x52].into(), + Tags::CardholderRelatedData => [0x65].into(), + Tags::Name => [0x5b].into(), + Tags::LanguagePref => [0x5f, 0x2d].into(), + Tags::Sex => [0x5f, 0x35].into(), + Tags::ApplicationRelatedData => [0x6e].into(), + Tags::ExtendedLengthInformation => [0x7f, 0x66].into(), + Tags::GeneralFeatureManagement => [0x7f, 0x74].into(), + Tags::DiscretionaryDataObjects => [0x73].into(), + Tags::ExtendedCapabilities => [0xc0].into(), + Tags::AlgorithmAttributesSignature => [0xc1].into(), + Tags::AlgorithmAttributesDecryption => [0xc2].into(), + Tags::AlgorithmAttributesAuthentication => [0xc3].into(), + Tags::PWStatusBytes => [0xc4].into(), + Tags::Fingerprints => [0xc5].into(), + Tags::CaFingerprints => [0xc6].into(), + Tags::GenerationTimes => [0xcd].into(), + Tags::KeyInformation => [0xde].into(), + Tags::UifSig => [0xd6].into(), + Tags::UifDec => [0xd7].into(), + Tags::UifAuth => [0xd8].into(), + Tags::UifAttestation => [0xd9].into(), + Tags::SecuritySupportTemplate => [0x7a].into(), + Tags::DigitalSignatureCounter => [0x93].into(), + Tags::CardholderCertificate => [0x7f, 0x21].into(), + Tags::AlgorithmAttributesAttestation => [0xda].into(), + Tags::FingerprintAttestation => [0xdb].into(), + Tags::CaFingerprintAttestation => [0xdc].into(), + Tags::GenerationTimeAttestation => [0xdd].into(), + Tags::KdfDo => [0xf9].into(), + Tags::AlgorithmInformation => [0xfa].into(), + Tags::CertificateSecureMessaging => [0xfb].into(), + Tags::AttestationCertificate => [0xfc].into(), + + // PUT DATA + Tags::FingerprintSignature => [0xc7].into(), + Tags::FingerprintDecryption => [0xc8].into(), + Tags::FingerprintAuthentication => [0xc9].into(), + Tags::CaFingerprint1 => [0xca].into(), + Tags::CaFingerprint2 => [0xcb].into(), + Tags::CaFingerprint3 => [0xcc].into(), + Tags::GenerationTimeSignature => [0xce].into(), + Tags::GenerationTimeDecryption => [0xcf].into(), + Tags::GenerationTimeAuthentication => [0xd0].into(), + Tags::ResettingCode => [0xd3].into(), + Tags::PsoEncDecKey => [0xd5].into(), + + // OTHER + // 4.4.3.12 Private Key Template + Tags::ExtendedHeaderList => [0x4d].into(), + Tags::CardholderPrivateKeyTemplate => [0x7f, 0x48].into(), + Tags::ConcatenatedKeyData => [0x5f, 0x48].into(), + Tags::CrtKeySignature => [0xb6].into(), + Tags::CrtKeyConfidentiality => [0xb8].into(), + Tags::CrtKeyAuthentication => [0xa4].into(), + Tags::PrivateKeyDataRsaPublicExponent => [0x91].into(), + Tags::PrivateKeyDataRsaPrime1 => [0x92].into(), // Note: value reused! + Tags::PrivateKeyDataRsaPrime2 => [0x93].into(), + Tags::PrivateKeyDataRsaPq => [0x94].into(), + Tags::PrivateKeyDataRsaDp1 => [0x95].into(), + Tags::PrivateKeyDataRsaDq1 => [0x96].into(), + Tags::PrivateKeyDataRsaModulus => [0x97].into(), + Tags::PrivateKeyDataEccPrivateKey => [0x92].into(), // Note: value reused! + Tags::PrivateKeyDataEccPublicKey => [0x99].into(), + + // 7.2.14 GENERATE ASYMMETRIC KEY PAIR + Tags::PublicKey => [0x7f, 0x49].into(), + Tags::PublicKeyDataRsaModulus => [0x81].into(), + Tags::PublicKeyDataRsaExponent => [0x82].into(), + Tags::PublicKeyDataEccPoint => [0x86].into(), + + // 7.2.11 PSO: DECIPHER + Tags::Cipher => [0xa6].into(), + Tags::ExternalPublicKey => [0x86].into(), + + // 7.2.5 SELECT DATA + Tags::GeneralReference => [0x60].into(), + Tags::TagList => [0x5c].into(), + } + } +} + +/// A ShortTag is a Tlv tag that is guaranteed to be either 1 or 2 bytes long. +/// +/// This covers any tag that can be used in the OpenPGP card context (the spec doesn't describe how +/// longer tags might be used.) +/// +/// (The type tlv::Tag will usually/always contain 1 or 2 byte long tags, in this library. +/// But its length is not guaranteed by the type system) +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ShortTag { + One(u8), + Two(u8, u8), +} + +impl From for Tag { + fn from(n: ShortTag) -> Self { + match n { + ShortTag::One(t0) => [t0].into(), + ShortTag::Two(t0, t1) => [t0, t1].into(), + } + } +} + +impl From<[u8; 1]> for ShortTag { + fn from(v: [u8; 1]) -> Self { + ShortTag::One(v[0]) + } +} + +impl From<[u8; 2]> for ShortTag { + fn from(v: [u8; 2]) -> Self { + ShortTag::Two(v[0], v[1]) + } +} + +impl From for Vec { + fn from(t: ShortTag) -> Self { + match t { + ShortTag::One(t0) => vec![t0], + ShortTag::Two(t0, t1) => vec![t0, t1], + } + } +} diff --git a/openpgp-card/src/tlv.rs b/openpgp-card/src/tlv.rs index b661f93..2a10d87 100644 --- a/openpgp-card/src/tlv.rs +++ b/openpgp-card/src/tlv.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 pub(crate) mod length; @@ -87,7 +87,8 @@ mod test { use hex_literal::hex; use super::{Tlv, Value}; - use crate::{Error, Tags}; + use crate::tags::Tags; + use crate::Error; #[test] fn test_tlv0() { diff --git a/pcsc/Cargo.toml b/pcsc/Cargo.toml index 32677d7..2149b80 100644 --- a/pcsc/Cargo.toml +++ b/pcsc/Cargo.toml @@ -1,18 +1,18 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-License-Identifier: MIT OR Apache-2.0 [package] -name = "openpgp-card-pcsc" -description = "PCSC OpenPGP card backend, for use with the openpgp-card crate" +name = "card-backend-pcsc" +description = "PCSC card backend, e.g. for use with the openpgp-card crate" authors = ["Heiko Schaefer "] license = "MIT OR Apache-2.0" -version = "0.3.0" +version = "0.5.0" edition = "2018" repository = "https://gitlab.com/openpgp-card/openpgp-card" -documentation = "https://docs.rs/crate/openpgp-card-pcsc" +documentation = "https://docs.rs/crate/card-backend-pcsc" [dependencies] -openpgp-card = { path = "../openpgp-card", version = "0.3" } +card-backend = { path = "../card-backend", version = "0.2" } iso7816-tlv = "0.4" pcsc = "2.7" log = "0.4" diff --git a/pcsc/README.md b/pcsc/README.md index 436cc05..82e93bc 100644 --- a/pcsc/README.md +++ b/pcsc/README.md @@ -1,17 +1,19 @@ -# PC/SC client for the openpgp-card library +# PC/SC based smart card backend This crate provides `PcscBackend` and `PcscTransaction`, which are implementations of the -`CardBackend` and `CardTransactions` traits from the [`openpgp-card`](https://crates.io/crates/openpgp-card) crate. +`CardBackend` and `CardTransactions` traits from the [`card-backend`](https://crates.io/crates/card-backend) crate. This implementation uses the [pcsc](https://crates.io/crates/pcsc) Rust wrapper crate to access OpenPGP cards. -## Documentation +Mainly intended for use with the [openpgp-card](https://gitlab.com/openpgp-card/openpgp-card) library. + +## Documentation on PC/SC [PC/SC](https://en.wikipedia.org/wiki/PC/SC) is a standard for interaction with smartcards and readers. diff --git a/pcsc/src/lib.rs b/pcsc/src/lib.rs index 98dedd6..71f5d18 100644 --- a/pcsc/src/lib.rs +++ b/pcsc/src/lib.rs @@ -1,39 +1,37 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 -//! This crate implements the `CardBackend`/`CardTransaction` backend for -//! `openpgp-card`. It uses the PCSC middleware to access the OpenPGP -//! application on smart cards. +//! This crate implements the traits [CardBackend] and [CardTransaction]. +//! It uses the PCSC middleware to access smart cards. +//! +//! This crate is mainly intended for use by the `openpgp-card` crate. use std::collections::HashMap; use std::convert::TryInto; +use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError}; use iso7816_tlv::simple::Tlv; -use openpgp_card::card_do::ApplicationRelatedData; -use openpgp_card::{CardBackend, CardCaps, CardTransaction, Error, PinType, SmartcardError}; +use pcsc::Disposition; const FEATURE_VERIFY_PIN_DIRECT: u8 = 0x06; const FEATURE_MODIFY_PIN_DIRECT: u8 = 0x07; -fn default_mode(mode: Option) -> pcsc::ShareMode { - if let Some(mode) = mode { - mode - } else { - pcsc::ShareMode::Shared - } -} - /// An opened PCSC Card (without open transaction). -/// The OpenPGP application on the card is `select`-ed while setting up a PcscCard object. +/// Note: No application is `select`-ed on the card while setting up a PcscCard object. /// /// This struct can be used to hold on to a Card, even while no operations /// are performed on the Card. To perform operations on the card, a -/// `TxClient` object needs to be obtained (via PcscCard::transaction()). +/// [PcscTransaction] object needs to be obtained (via [PcscBackend::transaction]). pub struct PcscBackend { card: pcsc::Card, mode: pcsc::ShareMode, - card_caps: Option, reader_caps: HashMap, + + // The reader name could be used as a hint about capabilities + // (e.g. readers that don't support extended length) + #[allow(dead_code)] + reader_name: String, + // FIXME: add a "adjust_card_caps" fn to card-backend? (could replace `max_cmd_len`) } /// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia) @@ -56,118 +54,106 @@ impl From for Box { /// ) pub struct PcscTransaction<'b> { tx: pcsc::Transaction<'b>, - card_caps: Option, // FIXME: manual copy from PcscCard - reader_caps: HashMap, // FIXME: manual copy from PcscCard + reader_caps: HashMap, // FIXME: gets manually cloned + was_reset: bool, } impl<'b> PcscTransaction<'b> { /// Start a transaction on `card`. /// - /// `reselect` set to `false` is only used internally in this crate, - /// during initial setup of cards. Otherwise it must be `true`, to - /// cause a select() call on cards that have been reset. - fn new(card: &'b mut PcscBackend, reselect: bool) -> Result { - use pcsc::Disposition; + /// If `reselect_application` is set, the application is SELECTed, + /// if the card reports having been reset. + fn new( + card: &'b mut PcscBackend, + reselect_application: Option<&[u8]>, + ) -> Result { + log::trace!("Start a transaction"); let mut was_reset = false; - let card_caps = card.card_caps(); - let reader_caps = card.reader_caps(); - let mode = card.mode(); + let mode = card.mode; + let reader_caps = card.reader_caps.clone(); - let mut c = card.card(); + let mut c = &mut card.card; loop { match c.transaction2() { - Ok(mut tx) => { - // A transaction has been successfully started + Ok(tx) => { + // A pcsc transaction has been successfully started - if was_reset { - log::trace!("start_tx: card was reset, select!"); - - let mut txc = Self { - tx, - card_caps, - reader_caps: reader_caps.clone(), - }; - - // In contexts where the caller of this fn - // expects that the card has already been opened, - // re-open the card here. - // For initial card-opening, we don't do this, then - // the caller always expects a card that has not - // been "select"ed yet. - if reselect { - PcscTransaction::select(&mut txc)?; - } - - tx = txc.tx; - } - - let txc = Self { + let mut pt = Self { tx, - card_caps, reader_caps, + was_reset: false, }; - break Ok(txc); + if was_reset { + log::trace!("Card was reset"); + + pt.was_reset = true; + + // If the caller expects that an application on the + // card has been selected, re-select the application + // here. + // + // When initially opening a card, we don't do this + // (freshly opened cards don't have an application + // "SELECT"ed). + if let Some(app) = reselect_application { + log::trace!("Will re-select an application after card reset"); + + let mut res = CardTransaction::select(&mut pt, app)?; + log::trace!("select res: {:0x?}", res); + + // Drop any bytes before the status code. + // + // e.g. SELECT on Basic Card 3.4 returns: + // [6f, 1d, + // 62, 15, 84, 10, d2, 76, 0, 1, 24, 1, 3, 4, 0, 5, 0, 0, a8, 35, 0, 0, 8a, 1, 5, 64, 4, 53, 2, c4, 41, + // 90, 0] + if res.len() > 2 { + res.drain(0..res.len() - 2); + } + + if res != [0x90, 0x00] { + break Err(SmartcardError::Error(format!( + "Error while attempting to (re-)select {:x?}, status code {:x?}", + app, res + ))); + } + + log::trace!("re-select ok"); + } + } + + break Ok(pt); } Err((c_, pcsc::Error::ResetCard)) => { // Card was reset, need to reconnect was_reset = true; - // drop(res); - c = c_; log::trace!("start_tx: do reconnect"); - { - c.reconnect(mode, pcsc::Protocols::ANY, Disposition::ResetCard) - .map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!( - "Reconnect failed: {e:?}" - ))) - })?; - } + c.reconnect(mode, pcsc::Protocols::ANY, Disposition::ResetCard) + .map_err(|e| SmartcardError::Error(format!("Reconnect failed: {e:?}")))?; log::trace!("start_tx: reconnected."); - // -> try opening a transaction again + // -> try opening a transaction again, in the next loop run } Err((_, e)) => { log::trace!("start_tx: error {:?}", e); - break Err(Error::Smartcard(SmartcardError::Error(format!( - "Error: {e:?}" - )))); + break Err(SmartcardError::Error(format!("Error: {e:?}"))); } }; } } - /// Try to select the OpenPGP application on a card - fn select(card_tx: &mut PcscTransaction) -> Result<(), Error> { - if ::select(card_tx).is_ok() { - Ok(()) - } else { - Err(Error::Smartcard(SmartcardError::SelectOpenPGPCardFailed)) - } - } - - /// Get application_related_data from card - fn application_related_data( - card_tx: &mut PcscTransaction, - ) -> Result { - ::application_related_data(card_tx).map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!( - "TxClient: failed to get application_related_data {e:x?}" - ))) - }) - } - /// GET_FEATURE_REQUEST /// (see http://pcscworkgroup.com/Download/Specifications/pcsc10_v2.02.09.pdf) - fn features(&mut self) -> Result, Error> { + fn features(&mut self) -> Result, SmartcardError> { let mut recv = vec![0; 1024]; let cm_ioctl_get_feature_request = pcsc::ctl_code(3400); @@ -175,9 +161,7 @@ impl<'b> PcscTransaction<'b> { .tx .control(cm_ioctl_get_feature_request, &[], &mut recv) .map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!( - "GET_FEATURE_REQUEST control call failed: {e:?}" - ))) + SmartcardError::Error(format!("GET_FEATURE_REQUEST control call failed: {e:?}")) })?; Ok(Tlv::parse_all(res)) @@ -190,42 +174,39 @@ impl<'b> PcscTransaction<'b> { PinType::Admin => 8, } } + /// Get the maximum pin length for pin_id. - fn max_pin_len(&self, pin: PinType) -> Result { - if let Some(card_caps) = self.card_caps { + fn max_pin_len( + &self, + pin: PinType, + card_caps: &Option, + ) -> Result { + if let Some(card_caps) = card_caps { match pin { PinType::User | PinType::Sign => Ok(card_caps.pw1_max_len()), PinType::Admin => Ok(card_caps.pw3_max_len()), } } else { - Err(Error::InternalError("card_caps is None".into())) + Err(SmartcardError::Error("card_caps is None".into())) } } } impl CardTransaction for PcscTransaction<'_> { - fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result, Error> { + fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result, SmartcardError> { let mut resp_buffer = vec![0; buf_size]; let resp = self .tx .transmit(cmd, &mut resp_buffer) .map_err(|e| match e { - pcsc::Error::NotTransacted => Error::Smartcard(SmartcardError::NotTransacted), - _ => Error::Smartcard(SmartcardError::Error(format!("Transmit failed: {e:?}"))), + pcsc::Error::NotTransacted => SmartcardError::NotTransacted, + _ => SmartcardError::Error(format!("Transmit failed: {e:?}")), })?; Ok(resp.to_vec()) } - fn init_card_caps(&mut self, caps: CardCaps) { - self.card_caps = Some(caps); - } - - fn card_caps(&self) -> Option<&CardCaps> { - self.card_caps.as_ref() - } - fn feature_pinpad_verify(&self) -> bool { self.reader_caps.contains_key(&FEATURE_VERIFY_PIN_DIRECT) } @@ -234,9 +215,13 @@ impl CardTransaction for PcscTransaction<'_> { self.reader_caps.contains_key(&FEATURE_MODIFY_PIN_DIRECT) } - fn pinpad_verify(&mut self, pin: PinType) -> Result, Error> { + fn pinpad_verify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, SmartcardError> { let pin_min_size = self.min_pin_len(pin); - let pin_max_size = self.max_pin_len(pin)?; + let pin_max_size = self.max_pin_len(pin, card_caps)?; // Default to varlen, for now. // (NOTE: Some readers don't support varlen, and need explicit length @@ -310,26 +295,28 @@ impl CardTransaction for PcscTransaction<'_> { let verify_ioctl: [u8; 4] = self .reader_caps .get(&FEATURE_VERIFY_PIN_DIRECT) - .ok_or_else(|| Error::Smartcard(SmartcardError::Error("no reader_capability".into())))? + .ok_or_else(|| SmartcardError::Error("no reader_capability".into()))? .value() .try_into() - .map_err(|e| Error::ParseError(format!("unexpected feature data: {e:?}")))?; + .map_err(|e| SmartcardError::Error(format!("unexpected feature data: {e:?}")))?; let res = self .tx .control(u32::from_be_bytes(verify_ioctl).into(), &send, &mut recv) - .map_err(|e: pcsc::Error| { - Error::Smartcard(SmartcardError::Error(format!("pcsc Error: {e:?}"))) - })?; + .map_err(|e: pcsc::Error| SmartcardError::Error(format!("pcsc Error: {e:?}")))?; log::trace!(" <- pcsc pinpad_verify result: {:x?}", res); Ok(res.to_vec()) } - fn pinpad_modify(&mut self, pin: PinType) -> Result, Error> { + fn pinpad_modify( + &mut self, + pin: PinType, + card_caps: &Option, + ) -> Result, SmartcardError> { let pin_min_size = self.min_pin_len(pin); - let pin_max_size = self.max_pin_len(pin)?; + let pin_max_size = self.max_pin_len(pin, card_caps)?; // Default to varlen, for now. // (NOTE: Some readers don't support varlen, and need explicit length @@ -413,36 +400,30 @@ impl CardTransaction for PcscTransaction<'_> { let modify_ioctl: [u8; 4] = self .reader_caps .get(&FEATURE_MODIFY_PIN_DIRECT) - .ok_or_else(|| Error::Smartcard(SmartcardError::Error("no reader_capability".into())))? + .ok_or_else(|| SmartcardError::Error("no reader_capability".into()))? .value() .try_into() - .map_err(|e| Error::ParseError(format!("unexpected feature data: {e:?}")))?; + .map_err(|e| SmartcardError::Error(format!("unexpected feature data: {e:?}")))?; let res = self .tx .control(u32::from_be_bytes(modify_ioctl).into(), &send, &mut recv) - .map_err(|e: pcsc::Error| { - Error::Smartcard(SmartcardError::Error(format!("pcsc Error: {e:?}"))) - })?; + .map_err(|e: pcsc::Error| SmartcardError::Error(format!("pcsc Error: {e:?}")))?; log::trace!(" <- pcsc pinpad_modify result: {:x?}", res); Ok(res.to_vec()) } + + fn was_reset(&self) -> bool { + self.was_reset + } } impl PcscBackend { - fn card(&mut self) -> &mut pcsc::Card { - &mut self.card - } - - fn mode(&self) -> pcsc::ShareMode { - self.mode - } - - /// A list of "raw" opened PCSC Cards (without selecting the OpenPGP card - /// application) - fn raw_pcsc_cards(mode: pcsc::ShareMode) -> Result, SmartcardError> { + /// A list of "raw" opened PCSC Cards and reader names + /// (No application is selected) + fn raw_pcsc_cards(mode: pcsc::ShareMode) -> Result, SmartcardError> { log::trace!("raw_pcsc_cards start"); let ctx = match pcsc::Context::establish(pcsc::Scope::User) { @@ -467,16 +448,13 @@ impl PcscBackend { log::trace!(" readers: {:?}", readers); - let mut found_reader = false; - let mut cards = vec![]; // Find a reader with a SmartCard. for reader in readers { - // We've seen at least one smartcard reader - found_reader = true; + let name = String::from_utf8_lossy(reader.to_bytes()); - log::trace!("Checking reader: {:?}", reader); + log::trace!("Checking reader: {:?}", name); // Try connecting to card in this reader let card = match ctx.connect(reader, mode, pcsc::Protocols::ANY) { @@ -495,126 +473,59 @@ impl PcscBackend { log::trace!("Found card"); - cards.push(card); + cards.push((card, name.to_string())); } - if !found_reader { - Err(SmartcardError::NoReaderFoundError) - } else { - Ok(cards) - } - } - - /// Starts from a list of all pcsc cards, then compares their OpenPGP - /// application identity with `ident` (if `ident` is None, all Cards are - /// returned). Returns fully initialized PcscCard structs for all matching - /// cards. - fn cards_filter(ident: Option<&str>, mode: pcsc::ShareMode) -> Result, Error> { - let mut cards: Vec = vec![]; - - for mut card in Self::raw_pcsc_cards(mode).map_err(Error::Smartcard)? { - log::trace!("cards_filter: next card"); - log::trace!(" status: {:x?}", card.status2_owned()); - - let mut store_card = false; - { - // start transaction - let mut p = PcscBackend::new(card, mode); - let mut txc = PcscTransaction::new(&mut p, false)?; - - { - if let Err(e) = PcscTransaction::select(&mut txc) { - log::trace!(" select error: {:?}", e); - } else { - // successfully opened the OpenPGP application - log::trace!(" select ok, will read ARD"); - log::trace!(" status: {:x?}", txc.tx.status2_owned()); - - if let Some(ident) = ident { - if let Ok(ard) = PcscTransaction::application_related_data(&mut txc) { - let aid = ard.application_id()?; - - if aid.ident() == ident.to_ascii_uppercase() { - // FIXME: handle multiple cards with matching ident - log::info!(" found card: {:?} (will use)", ident); - - // we want to return this one card - store_card = true; - } else { - log::info!(" found card: {:?} (won't use)", aid.ident()); - } - } else { - // couldn't read ARD for this card. - // ignore and move on - continue; - } - } else { - // we want to return all cards - store_card = true; - } - } - } - - drop(txc); - card = p.card; - } - - if store_card { - let pcsc = PcscBackend::new(card, mode); - cards.push(pcsc.initialize_card()?); - } - } - - log::trace!("cards_filter: found {} cards", cards.len()); - Ok(cards) } - /// Return all cards on which the OpenPGP application could be selected. + /// Returns an Iterator over Smart Cards that are accessible via PCSC. /// - /// Each card has the OpenPGP application selected, card_caps and reader_caps have been - /// initialized. - pub fn cards(mode: Option) -> Result, Error> { - Self::cards_filter(None, default_mode(mode)) + /// No application is SELECTed on the cards. + /// You can not assume that any particular application is available on the cards. + pub fn cards( + mode: Option, + ) -> Result>, SmartcardError> { + let mode = mode.unwrap_or(pcsc::ShareMode::Shared); + + let cards = Self::raw_pcsc_cards(mode)?; + + Ok(cards.into_iter().map(move |card| { + let backend = PcscBackend { + card: card.0, + mode, + reader_caps: Default::default(), + reader_name: card.1, + }; + + backend.initialize_card() + })) } - /// Returns the OpenPGP card that matches `ident`, if it is available. - /// A fully initialized PcscCard is returned: the OpenPGP application has - /// been selected, card_caps and reader_caps have been initialized. - pub fn open_by_ident(ident: &str, mode: Option) -> Result { - log::trace!("open_by_ident for {:?}", ident); + /// Returns an Iterator over Smart Cards that are accessible via PCSC. + /// Like [Self::cards], but returns the cards as [CardBackend]. + pub fn card_backends( + mode: Option, + ) -> Result< + impl Iterator, SmartcardError>>, + SmartcardError, + > { + let cards = PcscBackend::cards(mode)?; - let mut cards = Self::cards_filter(Some(ident), default_mode(mode))?; - - if !cards.is_empty() { - // FIXME: handle >1 cards found - - Ok(cards.pop().unwrap()) - } else { - Err(Error::Smartcard(SmartcardError::CardNotFound( - ident.to_string(), - ))) - } + Ok(cards.map(|c| match c { + Ok(c) => Ok(Box::new(c) as Box), + Err(e) => Err(e), + })) } - fn new(card: pcsc::Card, mode: pcsc::ShareMode) -> Self { - Self { - card, - mode, - card_caps: None, - reader_caps: HashMap::new(), - } - } - - /// Initialized a PcscCard: - /// - Obtain and store feature lists from reader (pinpad functionality). - /// - Get ARD from card, set CardCaps based on ARD. - fn initialize_card(mut self) -> Result { + /// Initialize this PcscBackend (obtains and stores feature lists from reader, + /// to determine if the reader offers PIN pad functionality). + fn initialize_card(mut self) -> Result { log::trace!("pcsc initialize_card"); let mut h: HashMap = HashMap::default(); - let mut txc = PcscTransaction::new(&mut self, true)?; + let mut txc = PcscTransaction::new(&mut self, None)?; // Get Features from reader (pinpad verify/modify) if let Ok(feat) = txc.features() { @@ -624,33 +535,41 @@ impl PcscBackend { } } - // Initialize CardTransaction (set CardCaps from ARD) - ::initialize(&mut txc)?; - - let cc = txc.card_caps().cloned(); - drop(txc); - self.card_caps = cc; - for (a, b) in h { self.reader_caps.insert(a, b); } Ok(self) } - - fn card_caps(&self) -> Option { - self.card_caps - } - fn reader_caps(&self) -> HashMap { - self.reader_caps.clone() - } } impl CardBackend for PcscBackend { - /// Get a TxClient for this PcscCard (this starts a transaction) - fn transaction(&mut self) -> Result, Error> { - Ok(Box::new(PcscTransaction::new(self, true)?)) + fn limit_card_caps(&self, card_caps: CardCaps) -> CardCaps { + let mut ext = card_caps.ext_support(); + + // Disable "extended length" support when the card reader is known not to support it + if self.reader_name.starts_with("ACS ACR122U") { + log::debug!("Disabling ext_support for reader {}", self.reader_name); + ext = false; + } + + CardCaps::new( + ext, + card_caps.chaining_support(), + card_caps.max_cmd_bytes(), + card_caps.max_rsp_bytes(), + card_caps.pw1_max_len(), + card_caps.pw3_max_len(), + ) + } + + /// Get a CardTransaction for this PcscBackend (this starts a transaction) + fn transaction( + &mut self, + reselect_application: Option<&[u8]>, + ) -> Result, SmartcardError> { + Ok(Box::new(PcscTransaction::new(self, reselect_application)?)) } } diff --git a/scdc/Cargo.toml b/scdc/Cargo.toml index c395a37..c6485d8 100644 --- a/scdc/Cargo.toml +++ b/scdc/Cargo.toml @@ -1,19 +1,19 @@ -# SPDX-FileCopyrightText: 2021 Heiko Schaefer +# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer # SPDX-License-Identifier: MIT OR Apache-2.0 [package] -name = "openpgp-card-scdc" -description = "Experimental SCDaemon Client, for use with the openpgp-card crate" +name = "card-backend-scdc" +description = "Experimental SCDaemon Client, e.g. for use with the openpgp-card crate" authors = ["Heiko Schaefer "] license = "MIT OR Apache-2.0" -version = "0.2.2" +version = "0.5.0" edition = "2018" repository = "https://gitlab.com/openpgp-card/openpgp-card" documentation = "https://docs.rs/crate/openpgp-card-scdc" [dependencies] -openpgp-card = { path = "../openpgp-card", version = "0.3" } -sequoia-ipc = "0.27" +card-backend = { path = "../card-backend", version = "0.2" } +sequoia-ipc = "0.29" hex = "0.4" futures = "0.3" tokio = { version = "1.13.1", features = ["rt-multi-thread"] } diff --git a/scdc/README.md b/scdc/README.md index 420eea1..b204a7b 100644 --- a/scdc/README.md +++ b/scdc/README.md @@ -1,18 +1,19 @@ -**scdaemon client for the openpgp-card library** +# scdaemon based backend (e.g., for the openpgp-card library) -This crate provides `ScdBackend`/`ScdTransaction`, which is an implementation of the -`CardBackend`/`CardTransaction` traits that uses an instance of GnuPG's +This crate provides `ScdBackend`/`ScdTransaction`, which is an implementation +of the `CardBackend`/`CardTransaction` traits that uses an instance of GnuPG's [scdaemon](https://www.gnupg.org/documentation/manuals/gnupg/Invoking-SCDAEMON.html) to access OpenPGP cards. -Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transaction guarantees. +Note that (unlike `card-backend-pcsc`), this backend doesn't implement +transaction guarantees. -**Known limitations** +## Known limitations - Uploading RSA 4096 keys via `scdaemon` doesn't work with cards that don't support Command Chaining (e.g. the "Floss Shop OpenPGP Smart Card"). @@ -24,14 +25,14 @@ Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transacti OpenPGP card operations fit into this constraint). - When using `scdaemon` via pcsc (by configuring `scdaemon` with - `disable-ccid`), choosing a specific card of multiple plugged in OpenPGP + `disable-ccid`), choosing a specific card of multiple plugged-in OpenPGP cards seems to be broken. So you probably want to plug in only one OpenPGP card at a time when using `openpgp-card-scdc` combined with `disable-ccid`. - When using `scdaemon` via its default `ccid` driver, choosing a - specific one of multiple plugged in OpenPGP cards seems to only work up - to 4 plugged in cards. + specific one of multiple plugged-in OpenPGP cards seems to only work up + to 4 plugged-in cards. So you probably want to plug in at most four OpenPGP cards at a time when - using `openpgp-card-scdc` with its ccid driver. + using `card-backend-scdc` with its ccid driver. (This limit has been raised in GnuPG 2.3.x) diff --git a/scdc/src/lib.rs b/scdc/src/lib.rs index 2632d84..9dacb4c 100644 --- a/scdc/src/lib.rs +++ b/scdc/src/lib.rs @@ -1,17 +1,17 @@ -// SPDX-FileCopyrightText: 2021 Heiko Schaefer +// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer // SPDX-License-Identifier: MIT OR Apache-2.0 //! This crate implements the experimental `ScdBackend`/`ScdTransaction` backend for the //! `openpgp-card` crate. -//! It uses GnuPG's scdaemon (via GnuPG Agent) to access OpenPGP cards. +//! It uses GnuPG's scdaemon (via GnuPG Agent) to access smart cards (including OpenPGP cards). //! -//! Note that (unlike `openpgp-card-pcsc`), this backend doesn't implement transaction guarantees. +//! Note that (unlike `card-backend-pcsc`), this backend doesn't implement transaction guarantees. use std::sync::Mutex; +use card_backend::{CardBackend, CardCaps, CardTransaction, PinType, SmartcardError}; use futures::StreamExt; use lazy_static::lazy_static; -use openpgp_card::{CardBackend, CardCaps, CardTransaction, Error, PinType, SmartcardError}; use sequoia_ipc::assuan::Response; use sequoia_ipc::gnupg::{Agent, Context}; use tokio::runtime::Runtime; @@ -33,36 +33,44 @@ lazy_static! { /// communication within GnuPG? Are \r\n added?) const ASSUAN_LINELENGTH: usize = 1000; -/// The maximum number of bytes for a command that we will send to +/// The maximum number of bytes for a command that we can send to /// scdaemon (via Assuan). /// /// Each command byte gets sent via Assuan as a two-character hex string. /// -/// 22 characters are used to send "SCD APDU --exlen=abcd " -/// (So, as a defensive limit, 25 characters are subtracted). +/// 17 characters are used to send "SCD APDU --exlen " /// /// In concrete terms, this limit means that some commands (with big /// parameters) cannot be sent to the card, when the card doesn't support /// command chaining (like the floss-shop "OpenPGP Smart Card 3.4"). /// /// In particular, uploading rsa4096 keys fails via scdaemon, with such cards. -const APDU_CMD_BYTES_MAX: usize = (ASSUAN_LINELENGTH - 25) / 2; +/// +/// The value of "36" was experimentally determined: +/// This value results in the biggest APDU_CMD_BYTES_MAX that still allows +/// uploading an RSA4k key onto a YubiKey 5. +const APDU_CMD_BYTES_MAX: usize = (ASSUAN_LINELENGTH - 36) / 2; /// An implementation of the CardBackend trait that uses GnuPG's scdaemon -/// (via GnuPG Agent) to access OpenPGP card devices. +/// (via GnuPG Agent) to access smart cards. pub struct ScdBackend { agent: Agent, - card_caps: Option, +} + +/// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia) +impl From for Box { + fn from(backend: ScdBackend) -> Box { + Box::new(backend) + } } impl ScdBackend { - /// Open a CardApp that uses an scdaemon instance as its backend. - /// The specific card with AID `serial` is requested from scdaemon. - pub fn open_by_serial(agent: Option, serial: &str) -> Result { + /// Request card with AID `serial` from scdaemon, and return it as a ScdBackend. + /// + /// The client may provide a GnuPG `agent` to use. + pub fn open_by_serial(agent: Option, serial: &str) -> Result { let mut card = ScdBackend::new(agent, true)?; - card.select_card(serial)?; - - card.transaction()?.initialize()?; + card.select_by_serial(serial)?; Ok(card) } @@ -72,22 +80,21 @@ impl ScdBackend { /// If multiple cards are available, scdaemon implicitly selects one. /// /// (NOTE: implicitly picking an unspecified card might be a bad idea. - /// You might want to avoid using this function.) - pub fn open_yolo(agent: Option) -> Result { - let mut card = ScdBackend::new(agent, true)?; - - card.transaction()?.initialize()?; + /// You might want to avoid using this function, or check which card + /// you received.) + pub fn open_yolo(agent: Option) -> Result { + let card = ScdBackend::new(agent, true)?; Ok(card) } /// Helper fn that shuts down scdaemon via GnuPG Agent. /// This may be useful to obtain access to a Smard card via PCSC. - pub fn shutdown_scd(agent: Option) -> Result<(), Error> { + pub fn shutdown_scd(agent: Option) -> Result<(), SmartcardError> { let mut scdc = Self::new(agent, false)?; - scdc.send("SCD RESTART")?; - scdc.send("SCD BYE")?; + scdc.execute("SCD RESTART")?; + scdc.execute("SCD BYE")?; Ok(()) } @@ -97,26 +104,18 @@ impl ScdBackend { /// /// If `agent` is None, a Context with the default GnuPG home directory /// is used. - fn new(agent: Option, init: bool) -> Result { - let agent = if let Some(agent) = agent { - agent - } else { - // Create and use a new Agent based on a default Context - let ctx = Context::new().map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!("Context::new failed {e}"))) - })?; - RT.lock() - .unwrap() - .block_on(Agent::connect(&ctx)) - .map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!("Agent::connect failed {e}"))) - })? - }; + fn new(agent: Option, init: bool) -> Result { + let agent = agent.unwrap_or({ + let rt = RT.lock().unwrap(); - let mut scdc = Self { - agent, - card_caps: None, - }; + // Create and use a new Agent based on a default Context + let ctx = Context::new() + .map_err(|e| SmartcardError::Error(format!("Context::new failed {e}")))?; + rt.block_on(Agent::connect(&ctx)) + .map_err(|e| SmartcardError::Error(format!("Agent::connect failed {e}")))? + }); + + let mut scdc = Self { agent }; if init { scdc.serialno()?; @@ -125,24 +124,23 @@ impl ScdBackend { Ok(scdc) } - fn send2(&mut self, cmd: &str) -> Result<(), Error> { - self.agent.send(cmd).map_err(|e| { - Error::Smartcard(SmartcardError::Error(format!( - "scdc agent send failed: {e}" - ))) - }) + /// Just send a command, without looking at the results at all + fn send_cmd(&mut self, cmd: &str) -> Result<(), SmartcardError> { + self.agent + .send(cmd) + .map_err(|e| SmartcardError::Error(format!("scdc agent send failed: {e}"))) } - /// Call "SCD SERIALNO", which causes scdaemon to be started by gpg - /// agent (if it's not running yet). - fn serialno(&mut self) -> Result<(), Error> { + /// Call "SCD SERIALNO", which causes scdaemon to be started by gpg-agent + /// (if it's not running yet). + fn serialno(&mut self) -> Result<(), SmartcardError> { let rt = RT.lock().unwrap(); - let send = "SCD SERIALNO"; - self.send2(send)?; + let cmd = "SCD SERIALNO"; + self.send_cmd(cmd)?; while let Some(response) = rt.block_on(self.agent.next()) { - log::trace!("init res: {:x?}", response); + log::trace!("SCD SERIALNO res: {:x?}", response); if let Ok(Response::Status { .. }) = response { // drop remaining lines @@ -154,26 +152,22 @@ impl ScdBackend { } } - Err(Error::Smartcard(SmartcardError::Error( - "SCDC init() failed".into(), - ))) + Err(SmartcardError::Error("SCDC init() failed".into())) } - /// Ask scdameon to switch to using a specific OpenPGP card, based on + /// Ask scdaemon to switch to using a specific OpenPGP card, based on /// its `serial`. - fn select_card(&mut self, serial: &str) -> Result<(), Error> { - let send = format!("SCD SERIALNO --demand={serial}"); - self.send2(&send)?; - + fn select_by_serial(&mut self, serial: &str) -> Result<(), SmartcardError> { let rt = RT.lock().unwrap(); + let send = format!("SCD SERIALNO --demand={serial}"); + self.send_cmd(&send)?; + while let Some(response) = rt.block_on(self.agent.next()) { log::trace!("select res: {:x?}", response); if response.is_err() { - return Err(Error::Smartcard(SmartcardError::CardNotFound( - serial.into(), - ))); + return Err(SmartcardError::CardNotFound(serial.into())); } if let Ok(Response::Status { .. }) = response { @@ -186,24 +180,22 @@ impl ScdBackend { } } - Err(Error::Smartcard(SmartcardError::CardNotFound( - serial.into(), - ))) + Err(SmartcardError::CardNotFound(serial.into())) } - fn send(&mut self, cmd: &str) -> Result<(), Error> { - self.send2(cmd)?; - + fn execute(&mut self, cmd: &str) -> Result<(), SmartcardError> { let rt = RT.lock().unwrap(); + self.send_cmd(cmd)?; + while let Some(response) = rt.block_on(self.agent.next()) { log::trace!("select res: {:x?}", response); if let Err(e) = response { - return Err(Error::Smartcard(SmartcardError::Error(format!("{e:?}")))); + return Err(SmartcardError::Error(format!("{e:?}"))); } - if let Ok(..) = response { + if response.is_ok() { // drop remaining lines while let Some(_drop) = rt.block_on(self.agent.next()) { log::trace!(" drop: {:x?}", _drop); @@ -213,14 +205,30 @@ impl ScdBackend { } } - Err(Error::Smartcard(SmartcardError::Error(format!( + Err(SmartcardError::Error(format!( "Error sending command {cmd}" - )))) + ))) } } impl CardBackend for ScdBackend { - fn transaction(&mut self) -> Result, Error> { + fn limit_card_caps(&self, card_caps: CardCaps) -> CardCaps { + let max_cmd_bytes = u16::min(APDU_CMD_BYTES_MAX as u16, card_caps.max_cmd_bytes()); + + CardCaps::new( + card_caps.ext_support(), + card_caps.chaining_support(), + max_cmd_bytes, + card_caps.max_rsp_bytes(), + card_caps.pw1_max_len(), + card_caps.pw3_max_len(), + ) + } + + fn transaction( + &mut self, + _reselect_application: Option<&[u8]>, + ) -> Result, SmartcardError> { Ok(Box::new(ScdTransaction { scd: self })) } } @@ -230,41 +238,39 @@ pub struct ScdTransaction<'a> { } impl CardTransaction for ScdTransaction<'_> { - fn transmit(&mut self, cmd: &[u8], _: usize) -> Result, Error> { + fn transmit(&mut self, cmd: &[u8], _: usize) -> Result, SmartcardError> { log::trace!("SCDC cmd len {}", cmd.len()); let hex = hex::encode(cmd); - // (Unwrap is ok here, not having a card_caps is fine) - let ext = if self.card_caps().is_some() && self.card_caps().unwrap().ext_support() { - // If we know about card_caps, and can do extended length we - // set "exlen" accordingly ... - format!("--exlen={} ", self.card_caps().unwrap().max_rsp_bytes()) - } else { - // ... otherwise don't send "exlen" to scdaemon - "".to_string() - }; + // Always set "--exlen" (without explicit parameter for length) + // + // FIXME: Does this ever cause problems? + // + // Hypothesis: this should be ok. + // Allowing too big of a return value should not do any effective harm + // (just maybe cause some slightly too large memory allocations). + let ext = "--exlen ".to_string(); - let send = format!("SCD APDU {ext}{hex}\n"); + let send = format!("SCD APDU {ext}{hex}"); log::trace!("SCDC command: '{}'", send); if send.len() > ASSUAN_LINELENGTH { - return Err(Error::Smartcard(SmartcardError::Error(format!( + return Err(SmartcardError::Error(format!( "APDU command is too long ({}) to send via Assuan", send.len() - )))); + ))); } - self.scd.send2(&send)?; - let rt = RT.lock().unwrap(); + self.scd.send_cmd(&send)?; + while let Some(response) = rt.block_on(self.scd.agent.next()) { - log::trace!("res: {:x?}", response); if response.is_err() { - return Err(Error::Smartcard(SmartcardError::Error(format!( + return Err(SmartcardError::Error(format!( "Unexpected error response from SCD {response:?}" - )))); + ))); } if let Ok(Response::Data { partial }) = response { @@ -279,17 +285,7 @@ impl CardTransaction for ScdTransaction<'_> { } } - Err(Error::Smartcard(SmartcardError::Error( - "no response found".into(), - ))) - } - - fn init_card_caps(&mut self, caps: CardCaps) { - self.scd.card_caps = Some(caps); - } - - fn card_caps(&self) -> Option<&CardCaps> { - self.scd.card_caps.as_ref() + Err(SmartcardError::Error("no response found".into())) } /// Return limit for APDU command size via scdaemon (based on Assuan @@ -298,23 +294,36 @@ impl CardTransaction for ScdTransaction<'_> { Some(APDU_CMD_BYTES_MAX) } - /// FIXME: not implemented yet + /// Not implemented yet fn feature_pinpad_verify(&self) -> bool { false } - /// FIXME: not implemented yet + /// Not implemented yet fn feature_pinpad_modify(&self) -> bool { false } - /// FIXME: not implemented yet - fn pinpad_verify(&mut self, _id: PinType) -> Result, Error> { + /// Not implemented yet + fn pinpad_verify( + &mut self, + _id: PinType, + _card_caps: &Option, + ) -> Result, SmartcardError> { unimplemented!() } - /// FIXME: not implemented yet - fn pinpad_modify(&mut self, _id: PinType) -> Result, Error> { + /// Not implemented yet + fn pinpad_modify( + &mut self, + _id: PinType, + _card_caps: &Option, + ) -> Result, SmartcardError> { unimplemented!() } + + /// Not implemented here + fn was_reset(&self) -> bool { + false + } } diff --git a/tools/Cargo.toml b/tools/Cargo.toml deleted file mode 100644 index fb0c3ec..0000000 --- a/tools/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -# SPDX-FileCopyrightText: 2022 Nora Widdecke -# SPDX-License-Identifier: MIT OR Apache-2.0 - -[package] -name = "openpgp-card-tools" -description = "CLI tools for OpenPGP cards" -license = "MIT OR Apache-2.0" -version = "0.9.1" -authors = ["Heiko Schaefer "] -edition = "2018" -repository = "https://gitlab.com/openpgp-card/openpgp-card" -documentation = "https://docs.rs/crate/openpgp-card-tools" - -[[bin]] -name = "opgpcard" -path = "src/opgpcard.rs" - -[dependencies] -sequoia-openpgp = { version = "1.3", default-features = false } -openpgp-card-pcsc = { path = "../pcsc", version = "0.3" } -openpgp-card-sequoia = { path = "../openpgp-card-sequoia", version = "0.1.1", default-features = false } -sshkeys = "0.3.2" -pem = "1" -rpassword = "6" -anyhow = "1" -clap = { version = "3", features = ["derive", "wrap_help"] } -env_logger = "0.9" -log = "0.4" -serde_json = "1.0.86" -serde = { version = "1.0.145", features = ["derive"] } -semver = "1.0.14" -serde_yaml = "0.9.13" -thiserror = "1.0.37" -indoc = "1" - -[build-dependencies] -subplot-build = "0.5.0" - -[dev-dependencies] -fehler = "1.0.0" -subplotlib = "0.5.0" - -[profile.release] -codegen-units = 1 - -[features] -default = ["sequoia-openpgp/default"] - -[package.metadata.cargo-udeps.ignore] -development = ["fehler", "subplotlib"] diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index 7eb126a..0000000 --- a/tools/README.md +++ /dev/null @@ -1,736 +0,0 @@ - - -# OpenPGP card tools - -This crate contains the `opgpcard` tool for inspecting, configuring and using OpenPGP -cards. - -# Install - -One easy way to install this crate is via the "cargo" tool. - -The following build dependencies are needed for current Debian: - -``` -# apt install rustc cargo clang pkg-config nettle-dev libpcsclite-dev -``` - -And for current Fedora: - -``` -# dnf install rustc cargo clang nettle-devel pcsc-lite-devel -``` - -Afterwards, you can install this crate by running: - -``` -$ cargo install openpgp-card-tools -``` - -Finally, add `$HOME/.cargo/bin` to your PATH to be able to run the installed -binaries. - -`opgpcard` uses the PC/SC framework. So on Linux-based systems, you need to make sure the `pcscd` -service is running, to be able to access your OpenPGP cards. - -## opgpcard - -A tool to inspect, configure and use OpenPGP cards. - -This tool is designed to be equally convenient for regular interactive use, as well as from scripts. -To this end, all functionality of this tool is alternatively usable in a non-interactive manner (see below). - -When using the tool in interactive contexts, two methods of PIN entry are supported: -in most cases, PINs can (and must) be entered via the host computer. -When a pin pad is available on the smartcard reader, PIN entry will be requested via this pin pad. - -### List cards - -List idents of all currently connected cards: - -``` -$ opgpcard list -Available OpenPGP cards: - ABCD:01234567 - 0007:87654321 -``` - -### Inspect card status - -Print status information about the data on a card. -The card is implicitly selected (if exactly one card is connected): - -``` -$ opgpcard status -OpenPGP card ABCD:01234567 (card version 3.4) - -Cardholder: Alice Adams -Language preferences: 'en' - -Signature key - Fingerprint: 034B 348C EDA2 064C AA22 74E4 7563 E86F 5CAB C2A4 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Ed25519 (EdDSA) - Signatures made: 11 - -Decryption key - Fingerprint: 338B EE09 3950 D831 A76F 0EB9 13D6 2DF6 8C9E 5176 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Cv25519 (ECDH) - -Authentication key - Fingerprint: 4881 A22E 7EC6 26D1 1202 50B0 A7D7 F0D5 0C8D F719 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Ed25519 (EdDSA) - -Remaining PIN attempts: User: 3, Admin: 3, Reset Code: 0 -``` - -Explicitly print the status information for a specific card (this command syntax is needed, when more than one card -is plugged in): - -``` -$ opgpcard status --card ABCD:01234567 -``` - -Add `-v` for more verbose card status: - -``` -OpenPGP card ABCD:01234567 (card version 3.4) - -Cardholder: Alice Adams -Language preferences: 'en' - -Signature key - Fingerprint: 034B 348C EDA2 064C AA22 74E4 7563 E86F 5CAB C2A4 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Ed25519 (EdDSA) - Touch policy: Cached (features: Button) - Key Status: generated - User PIN presentation is valid for unlimited signatures - Signatures made: 11 - -Decryption key - Fingerprint: 338B EE09 3950 D831 A76F 0EB9 13D6 2DF6 8C9E 5176 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Cv25519 (ECDH) - Touch policy: Off (features: Button) - Key Status: generated - -Authentication key - Fingerprint: 4881 A22E 7EC6 26D1 1202 50B0 A7D7 F0D5 0C8D F719 - Creation Time: 2022-05-21 13:15:19 UTC - Algorithm: Ed25519 (EdDSA) - Touch policy: Off (features: Button) - Key Status: generated - -Attestation key: - Algorithm: RSA 2048 [e 17] - Touch policy: Cached (features: Button) - -Remaining PIN attempts: User: 3, Admin: 3, Reset Code: 0 -Key Status (#129): imported -``` - -The `--public-key-material` flag additionally outputs the raw public key data for each key slot. - -### Get an OpenPGP public key representation from a card - -This command returns an OpenPGP public key representation of the keys on a card. - -To bind the decryption and authentication subkeys (if any) to the signing key, the user pin needs to be provided. - -``` -$ opgpcard pubkey -OpenPGP card ABCD:01234567 -Enter User PIN: ------BEGIN PGP PUBLIC KEY BLOCK----- -Comment: F9C7 97CB 1AF2 1C68 AEEC 8D4D 1002 89F5 5EF6 B2D4 -Comment: Alice Adams - -xjMEYkOmahYJKwYBBAHaRw8BAQdADwHIuuSgboyzgcLci8Hc0Q15YHKfDP8/CZG4 -uumYosXNA2JhesLABgQTFgoAeAWCYkjTagWJAAAAAAkQEAKJ9V72stRHFAAAAAAA -HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnifpLw5yhNlKffk7V+P9g -idnIM3j6l3k34+p7tMQmCPoCmwMWIQT5x5fLGvIcaK7sjU0QAon1Xvay1AAAhJkB -AIEhZTDuc9xARVK8ta51SOpX3mZs/UYA5a+UrB6vpmZ3AP4k14gFQ6q/cl/SOhPR -FpCAvYlqL8rb3gc2sFIZDfYUDM4zBGJDpmoWCSsGAQQB2kcPAQEHQDRodITykZoi -hIIPZcFZ2bMXvo20YEv+I1eg2kFQ2qSqwsAGBBgWCgB4BYJiSNNqBYkAAAAACRAQ -Aon1Xvay1EcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcI -5rVHhWA5cGdYlyQJYRXv4osAyFlyznFiUOATnoT6LgKbIBYhBPnHl8sa8hxoruyN -TRACifVe9rLUAADpTwD/a+AlBGryfLsqFzIhdJRpGkoOl0H+xcgk3vcaPUQq0pcA -/3TtUmaJ5w60qb/Px7/Q+MTymHH54elRY4lvwIfbvkUIzjgEYkOmahIKKwYBBAGX -VQEFAQEHQO5KBZ7cMwwjsXGOWWMqgAkCyNdw7smcx/+jBEk0m38dAwEKCcLABgQY -FgoAeAWCYkjTagWJAAAAAAkQEAKJ9V72stRHFAAAAAAAHgAgc2FsdEBub3RhdGlv -bnMuc2VxdW9pYS1wZ3Aub3Jn9IwQkbcw9W0jfrduv1q4qNhsOgJWkGTMbVyvQCug -YpcCmwwWIQT5x5fLGvIcaK7sjU0QAon1Xvay1AAAfTwBAPSQq/hGcGjAWNePHoLH -5zA/ePu1vaY1nh2dPhqtUg8+AP0TDG96MJxlM8SJUQXtQsJCAEo4qT9GnGi7MyTU -nvraDw== -=es4l ------END PGP PUBLIC KEY BLOCK----- -``` - -You can query a specific card - -``` -$ opgpcard pubkey --card ABCD:01234567 -``` - -In the process of exporting the key material on a card as a certificate (public key), one or more User IDs can be -bound to the certificate: - -``` -$ opgpcard pubkey --userid "Alice Adams " -``` - - -#### Caution: the exported public key material isn't always what you want - -The result of exporting public key material from a card is only an approximation of the original public key, since -some metadata is not available on OpenPGP cards. This missing metadata includes expiration dates. - -Also, if your card only contains subkeys, but not the original primary key, then the exported certificate will use the -signing subkey from the card as the primary key for the exported certificate. - -One way to safely process this exported public key material from a card is via `sq key adopt`. - -You can use this approach when you have access to your private primary key material (in the following example, we -assume this key is available in `key.pgp`). Then you can bind the public key material from a card to your key: - -``` -opgpcard pubkey > public.key -sq key adopt key.pgp public.pgp -``` - -In that process, you will be able to manually set any relevant flags. - - -### Using a card for ssh auth - -To use an OpenPGP card for ssh login authentication, a PGP authentication key needs to exist on the card. - -`opgpcard ssh` then shows the ssh public key string representation of the PGP authentication -key on the card, like this: - -``` -$ opgpcard ssh -OpenPGP card ABCD:01234567 - -Authentication key fingerprint: -59A5CD3EA88F8707D887EAAE13545F404E11BE1C - -SSH public key: -ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567 -``` - -To allow login to a remote machine, that ssh public key can be added to -`.ssh/authorized_keys` on that remote machine. - -In the example output above, this string is the ssh public key: - -`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567` - -### Show OpenPGP card metadata - -Print information about the capabilities of a card, including the list of supported algorithms (if the card returns -that list). - -Most of the output is probably not of interest to regular users. - -``` -$ opgpcard info -OpenPGP card FFFE:12345678 (card version 2.0) - -Application Identifier: D276000124 01 0200 FFFE 12345678 0000 -Manufacturer [FFFE]: Range reserved for randomly assigned serial numbers. - -Card Capabilities: -- command chaining - -Card service data: -- Application Selection by full DF name -- EF.DIR and EF.ATR/INFO access services by the GET DATA command (BER-TLV): 010 - -Extended Capabilities: -- get challenge -- key import -- PW Status changeable -- algorithm attributes changeable -- KDF-DO -- maximum length of challenge: 32 -- maximum length cardholder certificates: 2048 -- maximum command length: 255 -- maximum response length: 256 - -Supported algorithms: -- SIG: RSA 2048 [e 32] -- SIG: RSA 4096 [e 32] -- SIG: Secp256k1 (ECDSA) -- SIG: Ed25519 (EdDSA) -- SIG: Ed448 (EdDSA) -- DEC: RSA 2048 [e 32] -- DEC: RSA 4096 [e 32] -- DEC: Secp256k1 (ECDSA) -- DEC: Cv25519 (ECDH) -- DEC: X448 (ECDH) -- AUT: RSA 2048 [e 32] -- AUT: RSA 4096 [e 32] -- AUT: Secp256k1 (ECDSA) -- AUT: Ed25519 (EdDSA) -- AUT: Ed448 (EdDSA) -``` - -Or to query a specific card: - -``` -$ opgpcard info --card ABCD:01234567 -``` - -### Admin commands - -All `admin` commands need the Admin PIN. It can be provided as a file, with `-P `, -for non-interactive use (see below). - -By default, the PIN must be entered interactively on the host computer, or via a pin pad if the OpenPGP card is -used in a smart card reader that has a pin pad. - -#### Set touch policy - -Cards can require confirmation by the user before cryptographic operations are performed -(this confirmation feature is often implemented as a mechanical button on the card). - -However, not all cards implement this feature. - -Rationale: when a card requires touch confirmation, an attacker who gains control of the user's host computer -cannot perform cryptographic operations on the card at will - even after they learn the user's PINs. - -This feature is configured per key slot. The user can choose to require (or not require) touch confirmation separately -for signing, decryption, authentication and attestation operations. - -E.g., when the touch policy is set to `On` for the `SIG` key slot, then every signing operation requires a touch button -confirmation: - -``` -$ opgpcard admin --card ABCD:01234567 touch --key SIG --policy On -``` - -Valid values for the key slot are: `SIG`, `DEC`, `AUT`, `ATT` (some cards only support the first three). - -Available policies can include: `Off`, `On`, `Fixed`, `Cached`, `CachedFixed`. -Some cards only support a subset of these. - -- `Off` means that no touch confirmation is required. -- `On` means that each operation requires on touch confirmation. -- The `Fixed` policies are like `On`, but the policies cannot be changed without performing a factory reset on the card. -- With the `Cached` policies, a touch confirmation is valid for multiple operations within 15 seconds. - - -#### Set cardholder name - -Set the (informational) cardholder name: - -``` -$ opgpcard admin --card ABCD:01234567 name "Alice Adams" -``` - -#### Set certificate URL - -The URL field on OpenPGP cards is intended to point to the certificate (or "public key") the corresponds to the keys -that are present on the card. - -It can be set like this: - -``` -$ opgpcard admin --card ABCD:01234567 url "https://key.url.example" -``` - -##### Using `keys.openpgp.org` for the URL - -If you have uploaded (or plan to upload) your certificate (your public key) to the `keys.openpgp.org` keyserver, -you can point the URL field on your card there: - -If the fingerprint of your certificate is `0123456789ABCDEF0123456789ABCDEF01234567`, then you can set the URL -as follows: - -``` -$ opgpcard admin --card FFFE:12345678 url "https://keys.openpgp.org/vks/v1/by-fingerprint/0123456789ABCDEF0123456789ABCDEF01234567" -``` - -##### Other common options for certificate URLs - -You can use any URL that serves your certificate ("public key"), including links to: - -- gitlab (`https://gitlab.com/.gpg`) or github (`https://github.com/.gpg`) -- any other keyserver, such as https://keyserver.ubuntu.com/, -- a WKD server, -- a copy of your certificate on your personal website, ... - - -#### Import keys - -Import private key onto a card. This works if at most one (sub)key per role -(sign, decrypt, auth) exists in `key.priv`: - -``` -$ opgpcard admin --card ABCD:01234567 import key.priv -``` - -Import private key onto a card while explicitly selecting subkeys. Explicitly -specified fingerprints are necessary if more than one subkey exists -in `key.priv` for any role (spaces in fingerprints are ignored). - -``` -$ opgpcard admin --card ABCD:01234567 -P import key.priv \ - --sig-fp "F290 DBBF 21DB 8634 3C96 157B 87BE 15B7 F548 D97C" \ - --dec-fp "3C6E 08F6 7613 8935 8B8D 7666 73C7 F1A9 EEDA C360" \ - --auth-fp "D6AA 48EF 39A2 6F26 C42D 5BCB AAD2 14D5 5332 C838" -``` - -When fingerprints are only specified for a subset of the roles, no keys will -be imported for the other roles. - -If the private (sub)keys in the import file are password protected, the user will be prompted to enter -the password. If (sub)keys are encrypted with different passwords, the user will be prompted multiple times. -(Background: OpenPGP keys can be password protected when they are stored in files, but on an OpenPGP card -the keys always exist in unencrypted form. Therefore, they need to be decrypted for import.) - -(NOTE: There is currently no mechanism to non-interactively provide passwords to import password protected -OpenPGP keys) - -#### Generate Keys on the card - -This command generates new keys on an OpenPGP card. It creates the corresponding certificate ("public key") -representation in an output file. - -``` -$ opgpcard admin --card ABCD:01234567 generate --output cv25519 -``` - -Note that key generation needs both the Admin PIN and the User PIN (the User PIN is needed to export the new key as -a public key). - -Output will look like: - -``` -Enter Admin PIN: -Enter User PIN: - Generate subkey for Signing - Generate subkey for Decryption - Generate subkey for Authentication -``` - -The `` will contain the corresponding certificate ("public key"). - -As part of the process of generating key material on a card, one or more User IDs can be included with the exported -certificate: - -``` -$ opgpcard admin --card ABCD:01234567 generate --userid "Alice Adams " --output cv25519 -``` - - -### Signing - -For now, this tool only supports creating detached signatures, like this -(if no input file is set, stdin is read): - -``` -$ opgpcard sign --detached --card ABCD:01234567 -``` - -### Decrypting - -Decryption using a card (if no input file is set, stdin is read): - -``` -$ opgpcard decrypt --card ABCD:01234567 -``` - -### PIN management - -OpenPGP cards use PINs (numerical passwords) to verify that a user is allowed to perform an operation. - -To use cryptographic operations on a card (such as decryption or signing), the *User PIN* is required. - -To configure a card (for example to import OpenPGP key material into the card's key slots), the *Admin PIN* is needed. - -By default, on unconfigured (or factory reset) cards, the User PIN is typically set to `123456`, -and the Admin PIN is set to `12345678`. - -#### Blocked cards and resetting - -When a user has entered a wrong User PIN too often, the card goes into a blocked state, in which presenting the -User PIN successfully is not possible anymore. The purpose of this is to prevent attackers from trying all possible -PINs (e.g. after stealing a card). - -To be able to use the card again, the User PIN must be "reset". - -A User PIN reset can be performed by presenting the Admin PIN. - -#### The resetting code - -OpenPGP cards offer an additional, optional, *Resetting Code* mechanism. - -The resetting code may be configured on a card and used to reset the User PIN if it has been forgotten or blocked. -When unblocking a card with the Resetting Code, the Admin PIN is not needed. - -The Resetting Code mechanism is only useful in scenarios where a user doesn't have access to (or prefers not to use) -the Admin PIN (e.g. in some corporate settings, users might not be given the Admin PIN for -their cards. Instead, an admin may define a resetting code and give that code to the user). - -On un-configured (or factory reset) cards, the Resetting Code is typically unset. - - -#### Set a new User PIN - -Setting a new User PIN requires the Admin PIN: - -``` -$ opgpcard pin --card ABCD:01234567 set-user -``` - -#### Set new Admin PIN - -This requires the (previous) Admin PIN. - -``` -$ opgpcard pin --card ABCD:01234567 set-admin -``` - -#### Reset User PIN with Admin PIN - -The User PIN can be reset to a different (or the same) PIN by providing the Admin PIN. -This is possible at any time, including when a wrong User PIN has been entered too often, -and the card refuses to accept the User PIN anymore. - -``` -$ opgpcard pin --card ABCD:01234567 reset-user -``` - -#### Configuring the resetting code - -The resetting code is an alternative mechanism to recover from a lost or locked User PIN. - -You can set the resetting code after verifying the Admin PIN. Once a resetting code is configured on your card, -you can use that code to reset the User PIN without needing the Admin PIN. - -``` -$ opgpcard pin --card ABCD:01234567 set-reset -``` - -#### Reset User PIN with the resetting code - -If a resetting code is configured on a card, you can use that code to reset the User PIN: - -``` -$ opgpcard pin --card ABCD:01234567 reset-user-rc -Enter resetting code: -Enter new User PIN: -Repeat the new User PIN: - -User PIN has been set. -``` - -### Factory reset - -A factory reset erases all data on your card, including the private key material that the card stores. - -``` -$ opgpcard factory-reset --card ABCD:01234567 -``` - -NOTE: you do not need a PIN to reset a card! - -### Directly entering PINs on card readers with pin pad - -If your OpenPGP card is inserted in a card reader with a pin pad, this tool -offers you the option to use the pin pad to enter the User- or Admin PINs. -To do this, you can omit the `-p` and/or `-P` parameters. Then you will -be prompted to enter the user or Admin PINs where needed. - - -### Machine-readable Output (JSON, YAML) - -This tool is can optionally provide its output in JSON (or YAML) format. -The functionality is intended for scripting use. - -For all commands that return relevant output, the parameter `--output-format json` chooses JSON as the output format. - -For example, with the `status` command: - -``` -$ opgpcard --output-format json status -{ - "schema_version": "0.9.0", - "ident": "ABCD:01234567", - "card_version": "3.4", - "cardholder_name": "Alice Adams", - "language_preferences": [], - "certificate_url": "http://alice.example/alice.pgp", - "signature_key": { - "fingerprint": "A393 4505 BC51 1177 2E0B 845A 142C C9AB 7126 5C00", - "creation_time": "2022-10-31 13:45:35 UTC", - "algorithm": "Ed25519 (EdDSA)", - "touch_policy": "Off", - "touch_features": "Button", - "status": "generated", - "public_key_material": "ECC [Ed25519 (EdDSA)], data: 3A2B88EF788FA59575E3C4DB89EE367DBD0D9E93B6CE26B7686D32E94958F32A" - }, - "signature_count": 3, - "user_pin_valid_for_only_one_signature": false, - "decryption_key": { - "fingerprint": "0643 F2A9 6605 4158 CCFA B11F C7D2 0DBA DA64 84E0", - "creation_time": "2022-10-31 13:45:35 UTC", - "algorithm": "Cv25519 (ECDH)", - "touch_policy": "Off", - "touch_features": "Button", - "status": "generated", - "public_key_material": "ECC [Cv25519 (ECDH)], data: AF97CA49B2D89998605985AEDAA19097A0CE7E5CC681B1ABD1C8610933FDB320" - }, - "authentication_key": { - "fingerprint": "2BA3 3B42 90DE 337D 1DF8 54B3 2E20 E550 3ABC 57A9", - "creation_time": "2022-10-31 13:45:35 UTC", - "algorithm": "Ed25519 (EdDSA)", - "touch_policy": "Off", - "touch_features": "Button", - "status": "generated", - "public_key_material": "ECC [Ed25519 (EdDSA)], data: 80178ECE7F16ACDFDB0A645C81E72287761F03488CE3AE01F74279AA88A9018C" - }, - "attestation_key": { - "fingerprint": null, - "creation_time": null, - "algorithm": "RSA 2048 [e 17]", - "touch_policy": "Off", - "touch_features": "Button", - "status": null, - "public_key_material": null - }, - "user_pin_remaining_attempts": 3, - "admin_pin_remaining_attempts": 3, - "reset_code_remaining_attempts": 0 -} -``` - - -### Non-interactive use - -All commands that require PIN entry can be used non-interactively by providing PINs via files -(see the section "Using file-descriptors to provide PINs" for a variation on this). - -In almost all contexts, `-p` is used to provide the User PIN and `-P` to provide the Admin PIN -(the exception is when changing a PIN on the card, then a different parameter is used to provide the new PIN). - -**Examples of non-interactive use** - -- Setting the cardholder name: - -`$ opgpcard admin --card ABCD:01234567 -P name "Alice Adams"` - -- Importing a key to the card: - -`$ opgpcard admin --card ABCD:01234567 -P import key.priv` - -- Generating key material on the card: - -`$ opgpcard admin --card ABCD:01234567 -P generate -p --output cv25519` - -- Creating a detached signature: - -`$ opgpcard sign --detached --card ABCD:01234567 -p ` - -**Examples of non-interactive PIN management** - -- Setting a new User PIN: - -`$ opgpcard pin --card ABCD:01234567 set-user -p -q ` - -- Setting a new Admin PIN: - -`$ opgpcard pin --card ABCD:01234567 set-admin -P -Q ` - -- Setting a new User PIN based on the Admin PIN (and unblocking the card, if needed): - -`$ opgpcard pin --card ABCD:01234567 reset-user -P -p ` - -- Setting the resetting code: - -`$ opgpcard pin --card ABCD:01234567 set-reset -P -r ` - -- Setting a new User ID based on the resetting code (and unblocking the card, if needed): - -`$ opgpcard pin --card ABCD:01234567 reset-user-rc -r -p ` - -#### Using file-descriptors to provide PINs - -When using a shell like -[bash](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings) -, you can pass User- and/or Admin PINs via file-descriptors (instead of from a file on disk): - -``` -$ opgpcard sign --detached --card ABCD:01234567 -p /dev/fd/3 3<<<123456 -``` - -``` -$ opgpcard admin --card ABCD:01234567 -P /dev/fd/3 generate -p /dev/fd/4 --output cv25519 3<<<12345678 4<<<123456 -``` - -### Attestation - -Yubico implements a [proprietary extension](https://developers.yubico.com/PGP/Attestation.html) to the OpenPGP card -standard to *"cryptographically certify that a certain asymmetric key has been generated on device, and not imported"*. - -This feature is available on YubiKey 5 devices with firmware version 5.2 or newer. - -#### Attestation key/certificate - -*"The YubiKey is preloaded with an attestation certificate and matching attestation key issued by the Yubico CA. -The template and key are replaceable, which permits an individual or organization to issue attestations verifiable -with their own CA if they prefer. If replaced, the Yubico template can never be restored."* - -This tool does not currently support replacing the attestation key on a YubiKey. -It only supports use of the Yubico-provided attestation key to generate "attestation statements". - -The attestation certificate on a card can be inspected as follows: - -``` -$ opgpcard attestation cert ------BEGIN CERTIFICATE----- -[...] ------END CERTIFICATE----- -``` - -#### Generating an attestation statement - -For any key slot on the card you can generate an attestation statement, -if the key material in that key slot has been generated on the card. - -It's not possible to generate attestation statements for key material that was imported to the card -(the attestation statement certifies that the key has been generated on the card). - -To generate an attestation statement, run: - -``` -$ opgpcard attestation generate --key SIG --card 0006:01234567 -``` - -Supported values for `--key` are `SIG`, `DEC` and `AUT`. - -Generation of an attestation requires the User PIN. By default, it also requires touch confirmation -(the touch policy configuration for the attestation key slot is set to `On` by default). - -#### Viewing an attestation statement - -When the YubiKey generates an attestation statement, it gets stored in a `cardholder certificate` data object on the card. - -After an attestation statement has been generated, it can be read from the card and viewed in pem-encoded format: - -``` -$ opgpcard attestation statement --key SIG ------BEGIN CERTIFICATE----- -[...] ------END CERTIFICATE----- -``` - -Supported values for `--key` are `SIG`, `DEC` and `AUT`. diff --git a/tools/build.rs b/tools/build.rs deleted file mode 100644 index 5185c03..0000000 --- a/tools/build.rs +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::fs::File; -use std::path::Path; - -fn main() { - // Only generate test code from the subplot, if a virtual smart - // card is available. This is a kludge until Subplot can do - // conditional scenarios - // (https://gitlab.com/subplot/subplot/-/issues/20). - match option_env!("CARD_BASED_TESTS") { - Some(_) => { - subplot_build::codegen("subplot/opgpcard.subplot") - .expect("failed to generate code with Subplot"); - println!("cargo:warning=generated subplot tests"); - } - None => { - // If we're not generating code from the subplot, we should at - // least create an empty file so that the tests/opgpcard.rs - // file can include it. Otherwise the build will fail. - println!("cargo:warning=did not generate subplot tests"); - let out_dir = std::env::var("OUT_DIR").unwrap(); - let include = Path::new(&out_dir).join("opgpcard.rs"); - eprintln!("build.rs: include={}", include.display()); - if !include.exists() { - File::create(include).unwrap(); - } - } - } -} diff --git a/tools/cargo-test-in-docker b/tools/cargo-test-in-docker deleted file mode 100755 index b369ac8..0000000 --- a/tools/cargo-test-in-docker +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -# -# Run this to run opgpcard (tools directory) test suite inside a -# Docker container with a virtual smartcard running. The test suite -# contains tests that run the opgpcard binary and rely on a virtual -# smart card to be available. -# -# SPDX-FileCopyrightText: 2022 Lars Wirzenius -# SPDX-License-Identifier: MIT OR Apache-2.0 - -set -euo pipefail - -docker run --rm -it \ - -v root:/root \ - -v cargo:/cargo \ - -v $(pwd):/src \ - -e CARD_BASED_TESTS=true \ - registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps sh -c ' -apt install sq && -sed -i "s/timeout=20/timeout=60/" /home/jcardsim/run-card.sh && -/etc/init.d/pcscd start && -su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim && -cd /src/tools && -CARGO_TARGET_DIR=/cargo/ cargo update && -CARGO_TARGET_DIR=/cargo/ cargo build -vv && -CARGO_TARGET_DIR=/cargo/ cargo test -- --test-threads 1' diff --git a/tools/debian/build-deb b/tools/debian/build-deb deleted file mode 100755 index 11f8b1f..0000000 --- a/tools/debian/build-deb +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -S="$(dpkg-parsechangelog -SSource)" -V="$(dpkg-parsechangelog -SVersion | sed 's/-[^-]*$//')" - -git archive HEAD | gzip >"../${S}_${V}.orig.tar.gz" -dpkg-buildpackage -us -uc diff --git a/tools/debian/cargo-checksum.json b/tools/debian/cargo-checksum.json deleted file mode 100644 index e69de29..0000000 diff --git a/tools/debian/changelog b/tools/debian/changelog deleted file mode 100644 index 29b0f5e..0000000 --- a/tools/debian/changelog +++ /dev/null @@ -1,6 +0,0 @@ -openpgp-card-tool (0.0.11-1) unstable; urgency=medium - - * Initial packaging. This is not intended to be uploaded to Debian, so - not closing of an ITP bug. - - -- Lars Wirzenius Thu, 30 Sep 2021 09:51:32 +0300 diff --git a/tools/debian/compat b/tools/debian/compat deleted file mode 100644 index 021ea30..0000000 --- a/tools/debian/compat +++ /dev/null @@ -1,2 +0,0 @@ -10 - diff --git a/tools/debian/control b/tools/debian/control deleted file mode 100644 index e2c36a2..0000000 --- a/tools/debian/control +++ /dev/null @@ -1,23 +0,0 @@ -Source: openpgp-card-tool -Maintainer: Heiko Schaefer -Uploaders: Lars Wirzenius -Section: admin -Priority: optional -Standards-Version: 4.2.0 -Build-Depends: - debhelper (>= 10~), - dh-cargo, - libclang-dev, - libpcsclite-dev, - nettle-dev, - pkg-config, -Homepage: https://gitlab.com/openpgp-card/openpgp-card - -Package: openpgp-card-tool -Architecture: any -Depends: ${misc:Depends}, ${shlibs:Depends} -Built-Using: ${cargo:Built-Using} -Description: tool to manage OpenPGP hardware tokens - The opgpcard tool allows you to inspect, configure, administer, - factory reset, and generally manage OpenPGP cards (hardware tokens), - such as Gnuk, YubiKeys, Nitrokeys, and similar. diff --git a/tools/debian/copyright b/tools/debian/copyright deleted file mode 100644 index 40a5b06..0000000 --- a/tools/debian/copyright +++ /dev/null @@ -1,3 +0,0 @@ -Copyright 2021-2022 Heiko Schaefer - -# SPDX-License-Identifier: MIT OR Apache-2.0 diff --git a/tools/debian/docs b/tools/debian/docs deleted file mode 100644 index b43bf86..0000000 --- a/tools/debian/docs +++ /dev/null @@ -1 +0,0 @@ -README.md diff --git a/tools/debian/rules b/tools/debian/rules deleted file mode 100755 index bb6dbe1..0000000 --- a/tools/debian/rules +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ --buildsystem cargo - -override_dh_auto_clean: - echo auto clean - -override_dh_auto_configure: - echo auto configure - -override_dh_auto_build: - cargo --version - rustc --version - cargo build --release - -override_dh_auto_test: - true - -override_dh_auto_install: - install -d debian/openpgp-card-tool/bin - cargo install --locked --path=. --root=debian/openpgp-card-tool - find debian -name ".crates*" -delete diff --git a/tools/debian/source/format b/tools/debian/source/format deleted file mode 100644 index 163aaf8..0000000 --- a/tools/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/tools/prepare-card.py b/tools/prepare-card.py deleted file mode 100755 index ee153e9..0000000 --- a/tools/prepare-card.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python3 -# -# WARNING: This will wipe any information on a card. Do not use it unless -# you're very sure you don't mind. -# -# Prepare an OpenPGP card for use within a hypothetical organization: -# -# - factory reset the card -# - set card holder name, if desired -# - generate elliptic curve 25519 keys -# - write to stdout a JSON object with the card id, card holder, and -# key fingerprints -# -# Usage: run with --help. -# -# SPDX-FileCopyrightText: 2022 Lars Wirzenius -# SPDX-License-Identifier: MIT OR Apache-2.0 - - -import argparse -import json -import sys -from subprocess import run - - -tracing = False - - -def trace(msg): - if tracing: - sys.stderr.write(f"DEBUG: {msg}\n") - sys.stderr.flush() - - -def opgpcard_raw(args): - argv = ["opgpcard"] + args - trace(f"running {argv}") - p = run(argv, capture_output=True) - if p.returncode != 0: - raise Exception(f"opgpcard failed:\n{p.stderr}") - o = p.stdout - trace(f"opgpcard raw output: {o!r}") - return o - - -def opgpcard_json(args): - o = json.loads(opgpcard_raw(["--output-format=json"] + args)) - trace(f"opgpcard JSON output: {o}") - return o - - -def list_cards(): - return opgpcard_json(["list"])["idents"] - - -def pick_card(card): - cards = list_cards() - if card is None: - if not cards: - raise Exception("No cards found") - if len(cards) > 1: - raise Exception(f"Can't pick card automatically: found {len(cards)} cards") - return cards[0] - elif card in cards: - return card - else: - raise Exception(f"Can't find specified card {card}") - - -def factory_reset(card): - opgpcard_raw(["factory-reset", "--card", card]) - - -def set_card_holder(card, admin_pin, name): - trace(f"set card holder to {name!r}") - opgpcard_raw(["admin", "--card", card, "--admin-pin", admin_pin, "name", name]) - - -def generate_key(card, admin_pin, user_pin): - opgpcard_raw( - [ - "admin", - f"--card={card}", - f"--admin-pin={admin_pin}", - "generate", - f"--user-pin={user_pin}", - "--output=/dev/null", - "cv25519", - ] - ) - - -def status(card): - o = opgpcard_json(["status", f"--card={card}"]) - return { - "card_ident": o["ident"], - "cardholder_name": o["cardholder_name"], - "signature_key": o["signature_key"]["fingerprint"], - "decryption_key": o["signature_key"]["fingerprint"], - "authentication_key": o["signature_key"]["fingerprint"], - } - - -def card_is_empty(card): - o = status(card) - del o["card_ident"] - for key in o: - if o[key]: - return False - return True - - -def main(): - p = argparse.ArgumentParser() - p.add_argument("--force", action="store_true", help="prepare a card that has data") - p.add_argument( - "--verbose", action="store_true", help="produce debugging output to stderr" - ) - p.add_argument("--card", help="card identifier, default is to pick the only one") - p.add_argument("--card-holder", help="name of card holder", required=True) - p.add_argument( - "--admin-pin", action="store", help="set file with admin PIN", required=True - ) - p.add_argument( - "--user-pin", action="store", help="set file with user PIN", required=True - ) - args = p.parse_args() - - if args.verbose: - global tracing - tracing = True - - trace(f"args: {args}") - card = pick_card(args.card) - if not args.force and not card_is_empty(card): - raise Exception(f"card {card} has existing keys, not touching it") - factory_reset(card) - set_card_holder(card, args.admin_pin, args.card_holder) - key = generate_key(card, args.admin_pin, args.user_pin) - o = status(card) - print(json.dumps(o, indent=4)) - - -if __name__ == "__main__": - try: - main() - except Exception as e: - sys.stderr.write(f"ERROR: {e}\n") - sys.exit(1) diff --git a/tools/scripting.md b/tools/scripting.md deleted file mode 100644 index 8900a4e..0000000 --- a/tools/scripting.md +++ /dev/null @@ -1,61 +0,0 @@ - - -# Scripting around opgpcard - -The `opgpcard` tool can manipulate an OpenPGP smart card (also known -as hardware token). There are various commercial as well as Free Software-implementations of the standard. -Well known commercial products with OpenPGP card support include YubiKey and Nitrokey. This tool is meant to work with -any card that implements the OpenPGP smart card interface. - -`opgpcard` supports structured output as JSON and YAML. The default is -human-readable text. The structured output it meant to be consumed by -other programs, and is versioned. - -For example, to list all the OpenPGP cards connected to a system: - -~~~sh -$ opgpcard --output-format=json list -{ - "schema_version": "1.0.0", - "idents": [ - "ABCD:01234567" - ] -} -$ -~~~ - -The structured output is versioned (text output is not), using the -field name `schema_version`. The version numbering follows [semantic -versioning](https://semver.org/): - -* if a field is added, the minor level is incremented, and patch level - is set to zero -* if a field is removed, the major level is incremented, and minor and - patch level are set to zero -* if there are changed with no semantic impatc, the patch level is - incremented - -Each version of `opgpcard` supports only the latest minor version for -each major version. Consumers of the output have to be OK with added -fields. - -Thus, for example, if the `opgpcard list` command would add a new -field for when the command was run, the output might look like this: - -~~~sh -$ opgpcard --output-format=json list -{ - "schema_version": "1.1.0", - "date": "Tue, 18 Oct 2022 18:07:41 +0300", - "idents": [ - "ABCD:01234567" - ] -} -$ -~~~ - -A new field means the minor level in the schema version is -incremented. diff --git a/tools/src/cli.rs b/tools/src/cli.rs deleted file mode 100644 index 01e6221..0000000 --- a/tools/src/cli.rs +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use clap::{AppSettings, Parser}; - -use crate::commands; -use crate::{OutputFormat, OutputVersion}; - -pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)]; -pub const DEFAULT_OUTPUT_VERSION: OutputVersion = OutputVersion::new(0, 9, 0); - -#[derive(Parser, Debug)] -#[clap( - name = "opgpcard", - author = "Heiko Schäfer ", - version, - global_setting(AppSettings::DeriveDisplayOrder), - about = "A tool for inspecting and configuring OpenPGP cards." -)] -pub struct Cli { - /// Produce output in the chosen format. - #[clap(long, value_enum, default_value_t = OutputFormat::Text)] - pub output_format: OutputFormat, - - /// Pick output version to use, for non-textual formats. - #[clap(long, default_value_t = DEFAULT_OUTPUT_VERSION)] - pub output_version: OutputVersion, - - #[clap(subcommand)] - pub cmd: Command, -} - -#[derive(Parser, Debug)] -pub enum Command { - /// Enumerate available OpenPGP cards - List {}, - - /// Show information about the data on a card - Status(commands::status::StatusCommand), - - /// Show technical details about a card - Info(commands::info::InfoCommand), - - /// Show a card's authentication key as an SSH public key - Ssh(commands::ssh::SshCommand), - - /// Export the key data on a card as an OpenPGP public key - Pubkey(commands::pubkey::PubkeyCommand), - - /// Administer data on a card (including keys and metadata) - Admin(commands::admin::AdminCommand), - - /// PIN management (change PINs, reset blocked PINs) - #[clap( - long_about = indoc::indoc! { " - PIN management (change PINs, reset blocked PINs) - - OpenPGP cards use PINs (numerical passwords) to verify that a user is allowed to \ - perform an operation. There are two PINs for regular operation, User PIN and Admin \ - PIN, and one optional Resetting Code. - - The User PIN is required to use cryptographic operations on a card (such as \ - decryption or signing). - The Admin PIN is needed to configure a card (for example to import an OpenPGP key \ - into the card) or to unblock the User PIN. - The Resetting Code only allows unblocking the User PIN. This is useful if the user \ - doesn't have access to the Admin PIN. - - By default, on unconfigured (or factory reset) cards, the User PIN is typically set to - 123456, and the Admin PIN is set to 12345678." - }, - )] - Pin(commands::pin::PinCommand), - - /// Decrypt data using a card - Decrypt(commands::decrypt::DecryptCommand), - - /// Sign data using a card - /// - /// Currently, only detached signatures are supported. - Sign(commands::sign::SignCommand), - - /// Attestation management (Yubico only) - /// - /// Yubico implements a proprietary extension to the OpenPGP card standard to - /// cryptographically certify that a certain asymmetric key has been generated on device, and - /// not imported. - /// - /// This feature is available on YubiKey 5 devices with firmware version 5.2 or newer. - Attestation(commands::attestation::AttestationCommand), - - /// Completely reset a card (deletes all data including keys!) - FactoryReset(commands::factory_reset::FactoryResetCommand), - - /// Change identity (Nitrokey Start only) - /// - /// A Nitrokey Start device contains three distinct virtual OpenPGP cards, select the identity - /// of the virtual card to activate. - SetIdentity(commands::set_identity::SetIdentityCommand), - - /// Show supported output format versions - #[clap( - long_about = indoc::indoc! { " - Show supported output format versions for JSON and YAML output. - - Mark the currently chosen one with a star." - } - )] - OutputVersions {}, -} diff --git a/tools/src/commands/admin.rs b/tools/src/commands/admin.rs deleted file mode 100644 index 8fb686d..0000000 --- a/tools/src/commands/admin.rs +++ /dev/null @@ -1,568 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::PathBuf; - -use anyhow::{anyhow, Result}; -use clap::{Parser, ValueEnum}; -use openpgp_card_sequoia::state::{Admin, Open, Transaction}; -use openpgp_card_sequoia::types::AlgoSimple; -use openpgp_card_sequoia::util::public_key_material_to_key; -use openpgp_card_sequoia::{sq_util, PublicKey}; -use openpgp_card_sequoia::{types::KeyType, Card}; -use sequoia_openpgp::cert::prelude::ValidErasedKeyAmalgamation; -use sequoia_openpgp::packet::key::{SecretParts, UnspecifiedRole}; -use sequoia_openpgp::packet::Key; -use sequoia_openpgp::parse::Parse; -use sequoia_openpgp::policy::Policy; -use sequoia_openpgp::policy::StandardPolicy; -use sequoia_openpgp::serialize::SerializeInto; -use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm}; -use sequoia_openpgp::Cert; - -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; -use crate::{output, util, ENTER_ADMIN_PIN, ENTER_USER_PIN}; - -#[derive(Parser, Debug)] -pub struct AdminCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: String, - - #[clap( - name = "Admin PIN file", - short = 'P', - long = "admin-pin", - help = "Optionally, get Admin PIN from a file" - )] - pub admin_pin: Option, - - #[clap(subcommand)] - pub cmd: AdminSubCommand, -} - -#[derive(Parser, Debug)] -pub enum AdminSubCommand { - /// Set cardholder name - Name { - #[clap(help = "cardholder name to set on the card")] - name: String, - }, - - /// Set certificate URL - Url { - #[clap(help = "URL that provides the certificate for the key material on this card")] - url: String, - }, - - /// Import a Key onto the card. - /// - /// Most keys can be imported without specifying subkey fingerprints. However, if the key - /// contins more than one signing, decryption or authentication capable subkey, subkeys must be - /// explicitly selected. - /// - /// If any of the options is given, only the selected subkeys are imported into the selected - /// slots. - /// - /// Subkey capabilities must match the slot the key is imported into. The DEC slot can - /// only be used for encryption capable subkeys. The SIG and AUT slots can be used for signing, - /// certification and authentication capable subkeys. - Import { - #[clap(help = "File that contains the PGP private key")] - keyfile: PathBuf, - - /// Optionally, select the subkey to import in the SIG slot - #[clap(name = "SIG subkey fingerprint", short = 's', long = "sig-fp")] - sig_fp: Option, - - /// Optionally, select the subkey to import in the DEC slot - #[clap(name = "DEC subkey fingerprint", short = 'd', long = "dec-fp")] - dec_fp: Option, - - /// Optionally, select the subkey to import in the AUT slot - #[clap(name = "AUT subkey fingerprint", short = 'a', long = "aut-fp")] - aut_fp: Option, - }, - - /// Generate a Key on the card. - /// - /// A signing key is always created, decryption and authentication keys - /// are optional. - Generate(AdminGenerateCommand), - - /// Set the card's touch policy (if supported) - /// - /// A touch policy defines if cryptographic operations on the card require user interaction - /// with the card, for example by touching a button on the card. - /// - /// Only some cards support this feature at all, not all cards support all policies. - /// - /// Caution: Setting the ATT slot to Fixed or Cached-Fixed is permanent. Even a factory reset does - /// not undo this setting. - Touch { - /// Key slot to set the touch policy for - #[clap(name = "Key slot", short = 'k', long = "key", value_enum)] - key: BasePlusAttKeySlot, - - /// Touch policy to set on this key slot - #[clap( - name = "Policy", - short = 'p', - long = "policy", - value_enum, - long_help = "Touch policy to set on this key slot - -Off: No touch confirmation required. -On: Touch confirmation required for each operation. -Fixed: Like 'On', but the policy can only be changed by a reset. -Cached: Like 'On', but touch confirmation is valid for 15 seconds. -Cached-Fixed: Combines 'Cached' and 'Fixed'." - )] - policy: TouchPolicy, - }, -} - -#[derive(Parser, Debug)] -pub struct AdminGenerateCommand { - /// Output file - #[clap(name = "output", long = "output", short = 'o')] - output_file: PathBuf, - - /// Do not create a key in the DEC slot - #[clap(long = "no-dec", action = clap::ArgAction::SetFalse)] - decrypt: bool, - - /// Do not create a key in the AUT slot - #[clap(long = "no-aut", action = clap::ArgAction::SetFalse)] - auth: bool, - - /// Choose the algorithm for the key material to generate on the card. - /// - /// If the parameter is not given, use the algorithm currently set on the card. - /// - /// Specific cards support a set of algorithms that can differ between models. On modern cards, - /// use 'opgpcard info' to see the list of supported algorithms. - #[clap(name = "algorithm", value_enum)] - algo: Option, - - /// User ID to add to the exported certificate representation - #[clap(name = "User ID", short = 'u', long = "userid")] - user_ids: Vec, - - #[clap( - name = "User PIN file", - short = 'p', - long = "user-pin", - help = "Optionally, get User PIN from a file" - )] - user_pin: Option, -} - -#[derive(ValueEnum, Debug, Clone)] -#[clap(rename_all = "UPPER")] -pub enum BasePlusAttKeySlot { - Sig, - Dec, - Aut, - Att, -} - -impl From for KeyType { - fn from(ks: BasePlusAttKeySlot) -> Self { - match ks { - BasePlusAttKeySlot::Sig => KeyType::Signing, - BasePlusAttKeySlot::Dec => KeyType::Decryption, - BasePlusAttKeySlot::Aut => KeyType::Authentication, - BasePlusAttKeySlot::Att => KeyType::Attestation, - } - } -} - -#[derive(ValueEnum, Debug, Clone)] -pub enum TouchPolicy { - #[clap(name = "Off")] - Off, - #[clap(name = "On")] - On, - #[clap(name = "Fixed")] - Fixed, - #[clap(name = "Cached")] - Cached, - #[clap(name = "Cached-Fixed")] - CachedFixed, -} - -impl From for openpgp_card_sequoia::types::TouchPolicy { - fn from(tp: TouchPolicy) -> Self { - use openpgp_card_sequoia::types::TouchPolicy as OCTouchPolicy; - match tp { - TouchPolicy::On => OCTouchPolicy::On, - TouchPolicy::Off => OCTouchPolicy::Off, - TouchPolicy::Fixed => OCTouchPolicy::Fixed, - TouchPolicy::Cached => OCTouchPolicy::Cached, - TouchPolicy::CachedFixed => OCTouchPolicy::CachedFixed, - } - } -} - -#[derive(ValueEnum, Debug, Clone)] -#[clap(rename_all = "lower")] -pub enum Algo { - Rsa2048, - Rsa3072, - Rsa4096, - Nistp256, - Nistp384, - Nistp521, - Cv25519, -} - -impl From for AlgoSimple { - fn from(a: Algo) -> Self { - match a { - Algo::Rsa2048 => AlgoSimple::RSA2k, - Algo::Rsa3072 => AlgoSimple::RSA3k, - Algo::Rsa4096 => AlgoSimple::RSA4k, - Algo::Nistp256 => AlgoSimple::NIST256, - Algo::Nistp384 => AlgoSimple::NIST384, - Algo::Nistp521 => AlgoSimple::NIST521, - Algo::Cv25519 => AlgoSimple::Curve25519, - } - } -} - -pub fn admin( - output_format: OutputFormat, - output_version: OutputVersion, - command: AdminCommand, -) -> Result<(), Box> { - let backend = util::open_card(&command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - let admin_pin = util::get_pin(&mut card, command.admin_pin, ENTER_ADMIN_PIN)?; - - match command.cmd { - AdminSubCommand::Name { name } => { - name_command(&name, card, admin_pin.as_deref())?; - } - AdminSubCommand::Url { url } => { - url_command(&url, card, admin_pin.as_deref())?; - } - AdminSubCommand::Import { - keyfile, - sig_fp, - dec_fp, - aut_fp, - } => { - import_command(keyfile, sig_fp, dec_fp, aut_fp, card, admin_pin.as_deref())?; - } - AdminSubCommand::Generate(cmd) => { - generate_command( - output_format, - output_version, - card, - admin_pin.as_deref(), - cmd, - )?; - } - AdminSubCommand::Touch { key, policy } => { - touch_command(card, admin_pin.as_deref(), key, policy)?; - } - } - Ok(()) -} - -fn keys_pick_yolo<'a>( - key: &'a Cert, - policy: &'a dyn Policy, -) -> Result<[Option>; 3]> { - let key_by_type = |kt| sq_util::subkey_by_type(key, policy, kt); - - Ok([ - key_by_type(KeyType::Signing)?, - key_by_type(KeyType::Decryption)?, - key_by_type(KeyType::Authentication)?, - ]) -} - -fn keys_pick_explicit<'a>( - key: &'a Cert, - policy: &'a dyn Policy, - sig_fp: Option, - dec_fp: Option, - aut_fp: Option, -) -> Result<[Option>; 3]> { - let key_by_fp = |fp: Option| match fp { - Some(fp) => sq_util::private_subkey_by_fingerprint(key, policy, &fp), - None => Ok(None), - }; - - Ok([key_by_fp(sig_fp)?, key_by_fp(dec_fp)?, key_by_fp(aut_fp)?]) -} - -fn gen_subkeys( - admin: &mut Card, - decrypt: bool, - auth: bool, - algo: Option, -) -> Result<(PublicKey, Option, Option)> { - // We begin by generating the signing subkey, which is mandatory. - println!(" Generate subkey for Signing"); - let (pkm, ts) = admin.generate_key_simple(KeyType::Signing, algo)?; - let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?; - - // make decryption subkey (unless disabled), with the same algorithm as - // the sig key - let key_dec = if decrypt { - println!(" Generate subkey for Decryption"); - let (pkm, ts) = admin.generate_key_simple(KeyType::Decryption, algo)?; - Some(public_key_material_to_key( - &pkm, - KeyType::Decryption, - &ts, - Some(HashAlgorithm::SHA256), // FIXME - Some(SymmetricAlgorithm::AES128), // FIXME - )?) - } else { - None - }; - - // make authentication subkey (unless disabled), with the same - // algorithm as the sig key - let key_aut = if auth { - println!(" Generate subkey for Authentication"); - let (pkm, ts) = admin.generate_key_simple(KeyType::Authentication, algo)?; - - Some(public_key_material_to_key( - &pkm, - KeyType::Authentication, - &ts, - None, - None, - )?) - } else { - None - }; - - Ok((key_sig, key_dec, key_aut)) -} - -fn name_command( - name: &str, - mut card: Card, - admin_pin: Option<&[u8]>, -) -> Result<(), Box> { - let mut admin = util::verify_to_admin(&mut card, admin_pin)?; - - admin.set_name(name)?; - Ok(()) -} - -fn url_command( - url: &str, - mut card: Card, - admin_pin: Option<&[u8]>, -) -> Result<(), Box> { - let mut admin = util::verify_to_admin(&mut card, admin_pin)?; - - admin.set_url(url)?; - Ok(()) -} - -fn import_command( - keyfile: PathBuf, - sig_fp: Option, - dec_fp: Option, - aut_fp: Option, - mut card: Card, - admin_pin: Option<&[u8]>, -) -> Result<(), Box> { - let key = Cert::from_file(keyfile)?; - - let p = StandardPolicy::new(); - - // select the (sub)keys to upload - let [sig, dec, auth] = match (&sig_fp, &dec_fp, &aut_fp) { - // No fingerprint has been provided, try to autoselect keys - // (this fails if there is more than one (sub)key for any keytype). - (&None, &None, &None) => keys_pick_yolo(&key, &p)?, - - _ => keys_pick_explicit(&key, &p, sig_fp, dec_fp, aut_fp)?, - }; - - let mut pws: Vec = vec![]; - - // helper: true, if `pw` decrypts `key` - let pw_ok = |key: &Key, pw: &str| { - key.clone() - .decrypt_secret(&sequoia_openpgp::crypto::Password::from(pw)) - .is_ok() - }; - - // helper: if any password in `pws` decrypts `key`, return that password - let find_pw = |key: &Key, pws: &[String]| { - pws.iter().find(|pw| pw_ok(key, pw)).cloned() - }; - - // helper: check if we have the right password for `key` in `pws`, - // if so return it. otherwise ask the user for the password, - // add it to `pws` and return it. - let mut get_pw_for_key = |key: &Option>, - key_type: &str| - -> Result> { - if let Some(k) = key { - if !k.has_secret() { - // key has no secret key material, it can't be imported - return Err(anyhow!( - "(Sub)Key {} contains no private key material", - k.fingerprint() - )); - } - - if k.has_unencrypted_secret() { - // key is unencrypted, we need no password - return Ok(None); - } - - // key is encrypted, we need the password - - // do we already have the right password? - if let Some(pw) = find_pw(k, &pws) { - return Ok(Some(pw)); - } - - // no, we need to get the password from user - let pw = rpassword::prompt_password(format!( - "Enter password for {} (sub)key {}:", - key_type, - k.fingerprint() - ))?; - - if pw_ok(k, &pw) { - // remember pw for next subkeys - pws.push(pw.clone()); - - Ok(Some(pw)) - } else { - // this password doesn't work, error out - Err(anyhow!( - "Password not valid for (Sub)Key {}", - k.fingerprint() - )) - } - } else { - // we have no key for this slot, so we don't need a password - Ok(None) - } - }; - - // get passwords, if encrypted (try previous pw before asking for user input) - let sig_p = get_pw_for_key(&sig, "signing")?; - let dec_p = get_pw_for_key(&dec, "decryption")?; - let auth_p = get_pw_for_key(&auth, "authentication")?; - - // upload keys to card - let mut admin = util::verify_to_admin(&mut card, admin_pin)?; - - if let Some(sig) = sig { - println!("Uploading {} as signing key", sig.fingerprint()); - admin.upload_key(sig, KeyType::Signing, sig_p)?; - } - if let Some(dec) = dec { - println!("Uploading {} as decryption key", dec.fingerprint()); - admin.upload_key(dec, KeyType::Decryption, dec_p)?; - } - if let Some(auth) = auth { - println!("Uploading {} as authentication key", auth.fingerprint()); - admin.upload_key(auth, KeyType::Authentication, auth_p)?; - } - Ok(()) -} - -fn generate_command( - output_format: OutputFormat, - output_version: OutputVersion, - mut card: Card, - - admin_pin: Option<&[u8]>, - - cmd: AdminGenerateCommand, -) -> Result<()> { - let user_pin = util::get_pin(&mut card, cmd.user_pin, ENTER_USER_PIN)?; - - let mut output = output::AdminGenerate::default(); - output.ident(card.application_identifier()?.ident()); - - // 1) Interpret the user's choice of algorithm. - // - // Unset (None) means that the algorithm that is specified on the card - // should remain unchanged. - // - // For RSA, different cards use different exact algorithm - // specifications. In particular, the length of the value `e` differs - // between cards. Some devices use 32 bit length for e, others use 17 bit. - // In some cases, it's possible to get this information from the card, - // but I believe this information is not obtainable in all cases. - // Because of this, for generation of RSA keys, here we take the approach - // of first trying one variant, and then if that fails, try the other. - - let algo = cmd.algo.map(AlgoSimple::from); - log::info!(" Key generation will be attempted with algo: {:?}", algo); - output.algorithm(format!("{algo:?}")); - - // 2) Then, generate keys on the card. - // We need "admin" access to the card for this). - let (key_sig, key_dec, key_aut) = { - if let Ok(mut admin) = util::verify_to_admin(&mut card, admin_pin) { - gen_subkeys(&mut admin, cmd.decrypt, cmd.auth, algo)? - } else { - return Err(anyhow!("Failed to open card in admin mode.")); - } - }; - - // 3) Generate a Cert from the generated keys. For this, we - // need "signing" access to the card (to make binding signatures within - // the Cert). - let cert = crate::get_cert( - &mut card, - key_sig, - key_dec, - key_aut, - user_pin.as_deref(), - &cmd.user_ids, - &|| println!("Enter User PIN on card reader pinpad."), - )?; - - let armored = String::from_utf8(cert.armored().to_vec()?)?; - output.public_key(armored); - - // Write armored certificate to the output file - let mut handle = util::open_or_stdout(Some(&cmd.output_file))?; - handle.write_all(output.print(output_format, output_version)?.as_bytes())?; - let _ = handle.write(b"\n")?; - - Ok(()) -} - -fn touch_command( - mut card: Card, - admin_pin: Option<&[u8]>, - key: BasePlusAttKeySlot, - policy: TouchPolicy, -) -> Result<(), Box> { - let kt = KeyType::from(key); - - let pol = openpgp_card_sequoia::types::TouchPolicy::from(policy); - - let mut admin = util::verify_to_admin(&mut card, admin_pin)?; - - admin.set_uif(kt, pol)?; - Ok(()) -} diff --git a/tools/src/commands/attestation.rs b/tools/src/commands/attestation.rs deleted file mode 100644 index 40b0c20..0000000 --- a/tools/src/commands/attestation.rs +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::PathBuf; - -use anyhow::Result; -use clap::{Parser, ValueEnum}; -use openpgp_card_sequoia::types::KeyType; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; -use crate::ENTER_USER_PIN; -use crate::{output, pick_card_for_reading, util}; - -#[derive(Parser, Debug)] -pub struct AttestationCommand { - #[clap(subcommand)] - pub cmd: AttSubCommand, -} - -#[derive(Parser, Debug)] -pub enum AttSubCommand { - /// Print the card's attestation certificate - /// - /// New YubiKeys are preloaded with an attestation certificate issued by the Yubico CA. - Cert { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: Option, - }, - - /// Generate attestation statement for one of the key slots on the card - /// - /// An attestation statement can only be generated for key slots that contain keys that were - /// generated by the card. See 'opgpcard admin generate' and 'opgpcard status -v'. - Generate { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: String, - - /// Key slot to use - #[clap(name = "Key slot", short = 'k', long = "key", value_enum)] - key: BaseKeySlot, - - #[clap( - name = "User PIN file", - short = 'p', - long = "user-pin", - help = "Optionally, get User PIN from a file" - )] - user_pin: Option, - }, - - /// Print the attestation statement for one of the key slots on the card - /// - /// An attestation statement can only be printed after generating it. See 'opgpcard attestation - /// generate'. - Statement { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to reset" - )] - ident: Option, - - /// Key slot to use - #[clap(name = "Key slot", short = 'k', long = "key", value_enum)] - key: BaseKeySlot, - }, -} - -#[derive(ValueEnum, Debug, Clone)] -#[clap(rename_all = "UPPER")] -pub enum BaseKeySlot { - Sig, - Dec, - Aut, -} - -impl From for KeyType { - fn from(ks: BaseKeySlot) -> Self { - match ks { - BaseKeySlot::Sig => KeyType::Signing, - BaseKeySlot::Dec => KeyType::Decryption, - BaseKeySlot::Aut => KeyType::Authentication, - } - } -} - -pub fn attestation( - output_format: OutputFormat, - output_version: OutputVersion, - command: AttestationCommand, -) -> Result<(), Box> { - match command.cmd { - AttSubCommand::Cert { ident } => cert(output_format, output_version, ident), - AttSubCommand::Generate { - ident, - key, - user_pin, - } => generate(&ident, key, user_pin), - AttSubCommand::Statement { ident, key } => statement(ident, key), - } -} - -fn cert( - output_format: OutputFormat, - output_version: OutputVersion, - ident: Option, -) -> Result<(), Box> { - let mut output = output::AttestationCert::default(); - - let backend = pick_card_for_reading(ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - output.ident(card.application_identifier()?.ident()); - - if let Ok(ac) = card.attestation_certificate() { - let pem = util::pem_encode(ac); - output.attestation_cert(pem); - } - - println!("{}", output.print(output_format, output_version)?); - Ok(()) -} - -fn generate( - ident: &str, - key: BaseKeySlot, - user_pin: Option, -) -> Result<(), Box> { - let backend = util::open_card(ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - let user_pin = util::get_pin(&mut card, user_pin, ENTER_USER_PIN)?; - - let mut sign = util::verify_to_sign(&mut card, user_pin.as_deref())?; - - let kt = KeyType::from(key); - sign.generate_attestation(kt, &|| { - println!("Touch confirmation needed to generate an attestation") - })?; - Ok(()) -} - -fn statement(ident: Option, key: BaseKeySlot) -> Result<(), Box> { - let backend = pick_card_for_reading(ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - // Get cardholder certificate from card. - - let mut select_data_workaround = false; - // Use "select data" workaround if the card reports a - // yk firmware version number >= 5 and <= 5.4.3 - if let Ok(version) = card.firmware_version() { - if version.len() == 3 - && version[0] == 5 - && (version[1] < 4 || (version[1] == 4 && version[2] <= 3)) - { - select_data_workaround = true; - } - } - - // Select cardholder certificate - match key { - BaseKeySlot::Aut => card.select_data(0, &[0x7F, 0x21], select_data_workaround)?, - BaseKeySlot::Dec => card.select_data(1, &[0x7F, 0x21], select_data_workaround)?, - BaseKeySlot::Sig => card.select_data(2, &[0x7F, 0x21], select_data_workaround)?, - }; - - // Get DO "cardholder certificate" (returns the slot that was previously selected) - let cert = card.cardholder_certificate()?; - - if !cert.is_empty() { - let pem = util::pem_encode(cert); - println!("{pem}"); - } else { - println!("Cardholder certificate slot is empty"); - } - Ok(()) -} diff --git a/tools/src/commands/decrypt.rs b/tools/src/commands/decrypt.rs deleted file mode 100644 index d153cc4..0000000 --- a/tools/src/commands/decrypt.rs +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::PathBuf; - -use anyhow::{anyhow, Result}; -use clap::Parser; -use openpgp_card_sequoia::{state::Open, Card}; -use sequoia_openpgp::{ - parse::{stream::DecryptorBuilder, Parse}, - policy::StandardPolicy, -}; - -use crate::util; - -#[derive(Parser, Debug)] -pub struct DecryptCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: String, - - #[clap( - name = "User PIN file", - short = 'p', - long = "user-pin", - help = "Optionally, get User PIN from a file" - )] - pin_file: Option, - - /// Input file (stdin if unset) - #[clap(name = "input")] - input: Option, - - /// Output file (stdout if unset) - #[clap(name = "output", long = "output", short = 'o')] - pub output: Option, -} - -pub fn decrypt(command: DecryptCommand) -> Result<(), Box> { - let p = StandardPolicy::new(); - - let input = util::open_or_stdin(command.input.as_deref())?; - - let backend = util::open_card(&command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - if card.fingerprints()?.decryption().is_none() { - return Err(anyhow!("Can't decrypt: this card has no key in the decryption slot.").into()); - } - - let user_pin = util::get_pin(&mut card, command.pin_file, crate::ENTER_USER_PIN)?; - - let mut user = util::verify_to_user(&mut card, user_pin.as_deref())?; - let d = user.decryptor(&|| println!("Touch confirmation needed for decryption"))?; - - let db = DecryptorBuilder::from_reader(input)?; - let mut decryptor = db.with_policy(&p, None, d)?; - - let mut sink = util::open_or_stdout(command.output.as_deref())?; - std::io::copy(&mut decryptor, &mut sink)?; - - Ok(()) -} diff --git a/tools/src/commands/factory_reset.rs b/tools/src/commands/factory_reset.rs deleted file mode 100644 index 9379d3b..0000000 --- a/tools/src/commands/factory_reset.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::{anyhow, Result}; -use clap::Parser; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::util; - -#[derive(Parser, Debug)] -pub struct FactoryResetCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: String, -} - -pub fn factory_reset(command: FactoryResetCommand) -> Result<()> { - println!("Resetting Card {}", command.ident); - let backend = util::open_card(&command.ident)?; - let mut open: Card = backend.into(); - - let mut card = open.transaction()?; - card.factory_reset().map_err(|e| anyhow!(e)) -} diff --git a/tools/src/commands/info.rs b/tools/src/commands/info.rs deleted file mode 100644 index 732a747..0000000 --- a/tools/src/commands/info.rs +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::output; -use crate::pick_card_for_reading; -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; - -#[derive(Parser, Debug)] -pub struct InfoCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: Option, -} - -/// print metadata information about a card -pub fn print_info( - format: OutputFormat, - output_version: OutputVersion, - command: InfoCommand, -) -> Result<()> { - let mut output = output::Info::default(); - - let backend = pick_card_for_reading(command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - let ai = card.application_identifier()?; - - output.ident(ai.ident()); - - let version = ai.version().to_be_bytes(); - output.card_version(format!("{}.{}", version[0], version[1])); - - output.application_id(ai.to_string()); - output.manufacturer_id(format!("{:04X}", ai.manufacturer())); - output.manufacturer_name(ai.manufacturer_name().to_string()); - - if let Some(cc) = card.historical_bytes()?.card_capabilities() { - for line in cc.to_string().lines() { - let line = line.strip_prefix("- ").unwrap_or(line); - output.card_capability(line.to_string()); - } - } - if let Some(csd) = card.historical_bytes()?.card_service_data() { - for line in csd.to_string().lines() { - let line = line.strip_prefix("- ").unwrap_or(line); - output.card_service_data(line.to_string()); - } - } - - if let Some(eli) = card.extended_length_information()? { - for line in eli.to_string().lines() { - let line = line.strip_prefix("- ").unwrap_or(line); - output.extended_length_info(line.to_string()); - } - } - - let ec = card.extended_capabilities()?; - for line in ec.to_string().lines() { - let line = line.strip_prefix("- ").unwrap_or(line); - output.extended_capability(line.to_string()); - } - - // Algorithm information (list of supported algorithms) - // - // FIXME: this should be output in a more structured shape - // Algorithms should be grouped by key slot, and the format of the algorithm name should - // probably have a human readable, and an alternate machine readable format. - // Both formats should be output for machine readable formats. - if let Ok(Some(ai)) = card.algorithm_information() { - for line in ai.to_string().lines() { - let line = line.strip_prefix("- ").unwrap_or_else(|| line.trim()); - output.algorithm(line.to_string()); - } - } - - // FIXME: print KDF info - - // YubiKey specific (?) firmware version - if let Ok(ver) = card.firmware_version() { - let ver = ver.iter().map(u8::to_string).collect::>().join("."); - output.firmware_version(ver); - } - - println!("{}", output.print(format, output_version)?); - - Ok(()) -} diff --git a/tools/src/commands/mod.rs b/tools/src/commands/mod.rs deleted file mode 100644 index ba21c9a..0000000 --- a/tools/src/commands/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -pub mod admin; -pub mod attestation; -pub mod decrypt; -pub mod factory_reset; -pub mod info; -pub mod pin; -pub mod pubkey; -pub mod set_identity; -pub mod sign; -pub mod ssh; -pub mod status; diff --git a/tools/src/commands/pin.rs b/tools/src/commands/pin.rs deleted file mode 100644 index 2e7e5c8..0000000 --- a/tools/src/commands/pin.rs +++ /dev/null @@ -1,364 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::PathBuf; - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::{state::Open, state::Transaction, Card}; - -use crate::util; -use crate::util::{load_pin, print_gnuk_note}; -use crate::{ENTER_ADMIN_PIN, ENTER_USER_PIN}; - -#[derive(Parser, Debug)] -pub struct PinCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: String, - - #[clap(subcommand)] - pub cmd: PinSubCommand, -} - -#[derive(Parser, Debug)] -pub enum PinSubCommand { - /// Set User PIN - /// - /// Set a new User PIN by providing the current User PIN. - SetUser { - #[clap( - name = "User PIN file old", - short = 'p', - long = "user-pin-old", - help = "Optionally, get old User PIN from a file" - )] - user_pin_old: Option, - - #[clap( - name = "User PIN file new", - short = 'q', - long = "user-pin-new", - help = "Optionally, get new User PIN from a file" - )] - user_pin_new: Option, - }, - - /// Set Admin PIN - /// - /// Set a new Admin PIN by providing the current Admin PIN. - SetAdmin { - #[clap( - name = "Admin PIN file old", - short = 'P', - long = "admin-pin-old", - help = "Optionally, get old Admin PIN from a file" - )] - admin_pin_old: Option, - - #[clap( - name = "Admin PIN file new", - short = 'Q', - long = "admin-pin-new", - help = "Optionally, get new Admin PIN from a file" - )] - admin_pin_new: Option, - }, - - /// Reset User PIN with Admin PIN - /// - /// Set a new User PIN by providing the Admin PIN. This can also be used if the User PIN has - /// been blocked. - ResetUser { - #[clap( - name = "Admin PIN file", - short = 'P', - long = "admin-pin", - help = "Optionally, get Admin PIN from a file" - )] - admin_pin: Option, - - #[clap( - name = "User PIN file new", - short = 'p', - long = "user-pin-new", - help = "Optionally, get new User PIN from a file" - )] - user_pin_new: Option, - }, - - /// Reset User PIN with Resetting Code - /// - /// Set a new User PIN by providing the Resetting Code. This can also be used if the User PIN - /// has been blocked. - ResetUserRc { - #[clap( - name = "Resetting Code file", - short = 'r', - long = "reset-code", - help = "Optionally, get the Resetting Code from a file" - )] - reset_code: Option, - - #[clap( - name = "User PIN file new", - short = 'p', - long = "user-pin-new", - help = "Optionally, get new User PIN from a file" - )] - user_pin_new: Option, - }, - - /// Set Resetting Code - /// - /// Set a Resetting Code by providing the Admin PIN. - SetReset { - #[clap( - name = "Admin PIN file", - short = 'P', - long = "admin-pin", - help = "Optionally, get Admin PIN from a file" - )] - admin_pin: Option, - - #[clap( - name = "Resetting Code file", - short = 'r', - long = "reset-code", - help = "Optionally, get the Resetting Code from a file" - )] - reset_code: Option, - }, -} - -pub fn pin(ident: &str, cmd: PinSubCommand) -> Result<()> { - let backend = util::open_card(ident)?; - let mut open: Card = backend.into(); - let card = open.transaction()?; - - match cmd { - PinSubCommand::SetUser { - user_pin_old, - user_pin_new, - } => set_user(user_pin_old, user_pin_new, card), - - PinSubCommand::SetAdmin { - admin_pin_old, - admin_pin_new, - } => set_admin(admin_pin_old, admin_pin_new, card), - - PinSubCommand::ResetUser { - admin_pin, - user_pin_new, - } => reset_user(admin_pin, user_pin_new, card), - - PinSubCommand::SetReset { - admin_pin, - reset_code, - } => set_reset(admin_pin, reset_code, card), - - PinSubCommand::ResetUserRc { - reset_code, - user_pin_new, - } => reset_user_rc(reset_code, user_pin_new, card), - } -} - -fn set_user( - user_pin_old: Option, - user_pin_new: Option, - mut card: Card, -) -> Result<()> { - let pinpad_modify = card.feature_pinpad_modify(); - - let res = if !pinpad_modify { - // get current user pin - let user_pin1 = util::get_pin(&mut card, user_pin_old, ENTER_USER_PIN)? - .expect("this should never be None"); - - // verify pin - card.verify_user(&user_pin1)?; - println!("PIN was accepted by the card.\n"); - - let pin_new = match user_pin_new { - None => { - // ask user for new user pin - util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")? - } - Some(path) => load_pin(&path)?, - }; - - // set new user pin - card.change_user_pin(&user_pin1, &pin_new) - } else { - // set new user pin via pinpad - card.change_user_pin_pinpad(&|| { - println!("Enter old User PIN on card reader pinpad, then new User PIN (twice).") - }) - }; - - match res { - Err(err) => { - println!("\nFailed to change the User PIN!"); - println!("{err:?}"); - print_gnuk_note(err, &card)?; - } - Ok(_) => println!("\nUser PIN has been set."), - } - Ok(()) -} - -fn set_admin( - admin_pin_old: Option, - admin_pin_new: Option, - mut card: Card, -) -> Result<()> { - let pinpad_modify = card.feature_pinpad_modify(); - - if !pinpad_modify { - // get current admin pin - let admin_pin1 = util::get_pin(&mut card, admin_pin_old, ENTER_ADMIN_PIN)? - .expect("this should never be None"); - - // verify pin - card.verify_admin(&admin_pin1)?; - // (Verifying the PIN here fixes this class of problems: - // https://developers.yubico.com/PGP/PGP_PIN_Change_Behavior.html - // It is also just generally more user friendly than failing later) - println!("PIN was accepted by the card.\n"); - - let pin_new = match admin_pin_new { - None => { - // ask user for new admin pin - util::input_pin_twice("Enter new Admin PIN: ", "Repeat the new Admin PIN: ")? - } - Some(path) => load_pin(&path)?, - }; - - // set new admin pin - card.change_admin_pin(&admin_pin1, &pin_new)?; - } else { - // set new admin pin via pinpad - card.change_admin_pin_pinpad(&|| { - println!("Enter old Admin PIN on card reader pinpad, then new Admin PIN (twice).") - })?; - }; - - println!("\nAdmin PIN has been set."); - Ok(()) -} - -fn reset_user( - admin_pin: Option, - user_pin_new: Option, - mut card: Card, -) -> Result<()> { - // verify admin pin - match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? { - Some(admin_pin) => { - // verify pin - card.verify_admin(&admin_pin)?; - } - None => { - card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?; - } - } - println!("PIN was accepted by the card.\n"); - - // ask user for new user pin - let pin = match user_pin_new { - None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?, - Some(path) => load_pin(&path)?, - }; - - let res = if let Some(mut admin) = card.admin_card() { - admin.reset_user_pin(&pin) - } else { - return Err(anyhow::anyhow!("Failed to use card in admin-mode.")); - }; - - match res { - Err(err) => { - println!("\nFailed to change the User PIN!"); - print_gnuk_note(err, &card)?; - } - Ok(_) => println!("\nUser PIN has been set."), - } - Ok(()) -} - -fn set_reset( - admin_pin: Option, - reset_code: Option, - mut card: Card, -) -> Result<()> { - // verify admin pin - match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? { - Some(admin_pin) => { - // verify pin - card.verify_admin(&admin_pin)?; - } - None => { - card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?; - } - } - println!("PIN was accepted by the card.\n"); - - // ask user for new resetting code - let code = match reset_code { - None => util::input_pin_twice( - "Enter new resetting code: ", - "Repeat the new resetting code: ", - )?, - Some(path) => load_pin(&path)?, - }; - - if let Some(mut admin) = card.admin_card() { - admin.set_resetting_code(&code)?; - println!("\nResetting code has been set."); - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to use card in admin-mode.")) - } -} - -fn reset_user_rc( - reset_code: Option, - user_pin_new: Option, - mut card: Card, -) -> Result<()> { - // reset by presenting resetting code - - let rst = if let Some(path) = reset_code { - // load resetting code from file - load_pin(&path)? - } else { - // input resetting code - rpassword::prompt_password("Enter resetting code: ")? - .as_bytes() - .to_vec() - }; - - // ask user for new user pin - let pin = match user_pin_new { - None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?, - Some(path) => load_pin(&path)?, - }; - - // reset to new user pin - match card.reset_user_pin(&rst, &pin) { - Err(err) => { - println!("\nFailed to change the User PIN!"); - print_gnuk_note(err, &card) - } - Ok(_) => { - println!("\nUser PIN has been set."); - Ok(()) - } - } -} diff --git a/tools/src/commands/pubkey.rs b/tools/src/commands/pubkey.rs deleted file mode 100644 index 19b2e7a..0000000 --- a/tools/src/commands/pubkey.rs +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::PathBuf; - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::types::KeyType; -use openpgp_card_sequoia::util::public_key_material_and_fp_to_key; -use openpgp_card_sequoia::{state::Open, Card}; -use sequoia_openpgp::serialize::SerializeInto; - -use crate::output; -use crate::pick_card_for_reading; -use crate::util; -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; - -#[derive(Parser, Debug)] -pub struct PubkeyCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: Option, - - #[clap( - name = "User PIN file", - short = 'p', - long = "user-pin", - help = "Optionally, get User PIN from a file" - )] - user_pin: Option, - - /// User ID to add to the exported certificate representation - #[clap(name = "User ID", short = 'u', long = "userid")] - user_ids: Vec, -} - -pub fn print_pubkey( - format: OutputFormat, - output_version: OutputVersion, - command: PubkeyCommand, -) -> Result<()> { - let mut output = output::PublicKey::default(); - - let backend = pick_card_for_reading(command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - let ident = card.application_identifier()?.ident(); - output.ident(ident); - - let user_pin = util::get_pin(&mut card, command.user_pin, crate::ENTER_USER_PIN)?; - - let pkm = card.public_key_material(KeyType::Signing)?; - let times = card.key_generation_times()?; - let fps = card.fingerprints()?; - - let key_sig = public_key_material_and_fp_to_key( - &pkm, - KeyType::Signing, - times.signature().expect("Signature time is unset"), - fps.signature().expect("Signature fingerprint is unset"), - )?; - - let mut key_dec = None; - if let Ok(pkm) = card.public_key_material(KeyType::Decryption) { - if let Some(ts) = times.decryption() { - key_dec = Some(public_key_material_and_fp_to_key( - &pkm, - KeyType::Decryption, - ts, - fps.decryption().expect("Decryption fingerprint is unset"), - )?); - } - } - - let mut key_aut = None; - if let Ok(pkm) = card.public_key_material(KeyType::Authentication) { - if let Some(ts) = times.authentication() { - key_aut = Some(public_key_material_and_fp_to_key( - &pkm, - KeyType::Authentication, - ts, - fps.authentication() - .expect("Authentication fingerprint is unset"), - )?); - } - } - - let cert = crate::get_cert( - &mut card, - key_sig, - key_dec, - key_aut, - user_pin.as_deref(), - &command.user_ids, - &|| println!("Enter User PIN on card reader pinpad."), - )?; - - let armored = String::from_utf8(cert.armored().to_vec()?)?; - output.public_key(armored); - - println!("{}", output.print(format, output_version)?); - Ok(()) -} diff --git a/tools/src/commands/set_identity.rs b/tools/src/commands/set_identity.rs deleted file mode 100644 index 95f6544..0000000 --- a/tools/src/commands/set_identity.rs +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::Result; -use clap::{Parser, ValueEnum}; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::util; - -#[derive(Parser, Debug)] -pub struct SetIdentityCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - ident: String, - - /// Identity of the virtual card to activate - #[clap(name = "identity", value_enum)] - id: SetIdentityId, -} - -#[derive(ValueEnum, Debug, Clone)] -pub enum SetIdentityId { - #[clap(name = "0")] - Zero, - #[clap(name = "1")] - One, - #[clap(name = "2")] - Two, -} - -impl From for u8 { - fn from(id: SetIdentityId) -> Self { - match id { - SetIdentityId::Zero => 0, - SetIdentityId::One => 1, - SetIdentityId::Two => 2, - } - } -} - -pub fn set_identity(command: SetIdentityCommand) -> Result<(), Box> { - let backend = util::open_card(&command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - card.set_identity(u8::from(command.id))?; - Ok(()) -} diff --git a/tools/src/commands/sign.rs b/tools/src/commands/sign.rs deleted file mode 100644 index 6632ab8..0000000 --- a/tools/src/commands/sign.rs +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, Result}; -use clap::Parser; -use openpgp_card_sequoia::{state::Open, Card}; -use sequoia_openpgp::serialize::stream::{Armorer, Message, Signer}; - -use crate::util; - -#[derive(Parser, Debug)] -pub struct SignCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: String, - - #[clap( - name = "User PIN file", - short = 'p', - long = "user-pin", - help = "Optionally, get User PIN from a file" - )] - pub user_pin: Option, - - #[clap( - name = "detached", - short = 'd', - long = "detached", - help = "Create a detached signature" - )] - pub detached: bool, - - /// Input file (stdin if unset) - #[clap(name = "input")] - pub input: Option, - - /// Output file (stdout if unset) - #[clap(name = "output", long = "output", short = 'o')] - pub output: Option, -} - -pub fn sign(command: SignCommand) -> Result<(), Box> { - if command.detached { - sign_detached( - &command.ident, - command.user_pin, - command.input.as_deref(), - command.output.as_deref(), - ) - } else { - Err(anyhow::anyhow!("Only detached signatures are supported for now").into()) - } -} - -pub fn sign_detached( - ident: &str, - pin_file: Option, - input: Option<&Path>, - output: Option<&Path>, -) -> Result<(), Box> { - let mut input = util::open_or_stdin(input)?; - - let backend = util::open_card(ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - if card.fingerprints()?.signature().is_none() { - return Err(anyhow!("Can't sign: this card has no key in the signing slot.").into()); - } - - let user_pin = util::get_pin(&mut card, pin_file, crate::ENTER_USER_PIN)?; - - let mut sign = util::verify_to_sign(&mut card, user_pin.as_deref())?; - let s = sign.signer(&|| println!("Touch confirmation needed for signing"))?; - - let sink = util::open_or_stdout(output)?; - - let message = Armorer::new(Message::new(sink)).build()?; - let mut signer = Signer::new(message, s).detached().build()?; - - std::io::copy(&mut input, &mut signer)?; - signer.finalize()?; - - Ok(()) -} diff --git a/tools/src/commands/ssh.rs b/tools/src/commands/ssh.rs deleted file mode 100644 index 7999579..0000000 --- a/tools/src/commands/ssh.rs +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::types::KeyType; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::output; -use crate::pick_card_for_reading; -use crate::util; -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; - -#[derive(Parser, Debug)] -pub struct SshCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: Option, -} - -pub fn print_ssh( - format: OutputFormat, - output_version: OutputVersion, - command: SshCommand, -) -> Result<()> { - let mut output = output::Ssh::default(); - - let ident = command.ident; - - let backend = pick_card_for_reading(ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - let ident = card.application_identifier()?.ident(); - output.ident(ident.clone()); - - // Print fingerprint of authentication subkey - let fps = card.fingerprints()?; - - if let Some(fp) = fps.authentication() { - output.authentication_key_fingerprint(fp.to_string()); - } - - // Show authentication subkey as openssh public key string - if let Ok(pkm) = card.public_key_material(KeyType::Authentication) { - if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) { - output.ssh_public_key(ssh); - } - } - - println!("{}", output.print(format, output_version)?); - Ok(()) -} diff --git a/tools/src/commands/status.rs b/tools/src/commands/status.rs deleted file mode 100644 index 280bcd5..0000000 --- a/tools/src/commands/status.rs +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::types::KeyType; -use openpgp_card_sequoia::{state::Open, Card}; - -use crate::output; -use crate::pick_card_for_reading; -use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion}; - -#[derive(Parser, Debug)] -pub struct StatusCommand { - #[clap( - name = "card ident", - short = 'c', - long = "card", - help = "Identifier of the card to use" - )] - pub ident: Option, - - #[clap( - name = "verbose", - short = 'v', - long = "verbose", - help = "Use verbose output" - )] - pub verbose: bool, - - /// Print public key material for each key slot - #[clap(name = "pkm", short = 'K', long = "public-key-material")] - pub pkm: bool, -} - -pub fn print_status( - format: OutputFormat, - output_version: OutputVersion, - command: StatusCommand, -) -> Result<()> { - let mut output = output::Status::default(); - output.verbose(command.verbose); - output.pkm(command.pkm); - - let backend = pick_card_for_reading(command.ident)?; - let mut open: Card = backend.into(); - let mut card = open.transaction()?; - - output.ident(card.application_identifier()?.ident()); - - let ai = card.application_identifier()?; - let version = ai.version().to_be_bytes(); - output.card_version(format!("{}.{}", version[0], version[1])); - - // Cardholder Name - if let Some(name) = card.cardholder_name()? { - output.cardholder_name(name); - } - - // We ignore the Cardholder "Sex" field, because it's silly and mostly unhelpful - - // Certificate URL - let url = card.url()?; - if !url.is_empty() { - output.certificate_url(url); - } - - // Language Preference - if let Some(lang) = card.cardholder_related_data()?.lang() { - for lang in lang { - output.language_preference(format!("{lang}")); - } - } - - // key information (imported vs. generated on card) - let ki = card.key_information().ok().flatten(); - - let pws = card.pw_status_bytes()?; - - // information about subkeys - - let fps = card.fingerprints()?; - let kgt = card.key_generation_times()?; - - let mut signature_key = output::KeySlotInfo::default(); - if let Some(fp) = fps.signature() { - signature_key.fingerprint(fp.to_spaced_hex()); - } - signature_key.algorithm(format!("{}", card.algorithm_attributes(KeyType::Signing)?)); - if let Some(kgt) = kgt.signature() { - signature_key.creation_time(format!("{}", kgt.to_datetime())); - } - if let Some(uif) = card.uif_signing()? { - signature_key.touch_policy(format!("{}", uif.touch_policy())); - signature_key.touch_features(format!("{}", uif.features())); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) { - signature_key.status(format!("{ks}")); - } - - if let Ok(pkm) = card.public_key_material(KeyType::Signing) { - signature_key.public_key_material(pkm.to_string()); - } - - output.signature_key(signature_key); - - let sst = card.security_support_template()?; - output.signature_count(sst.signature_count()); - - let mut decryption_key = output::KeySlotInfo::default(); - if let Some(fp) = fps.decryption() { - decryption_key.fingerprint(fp.to_spaced_hex()); - } - decryption_key.algorithm(format!( - "{}", - card.algorithm_attributes(KeyType::Decryption)? - )); - if let Some(kgt) = kgt.decryption() { - decryption_key.creation_time(format!("{}", kgt.to_datetime())); - } - if let Some(uif) = card.uif_decryption()? { - decryption_key.touch_policy(format!("{}", uif.touch_policy())); - decryption_key.touch_features(format!("{}", uif.features())); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) { - decryption_key.status(format!("{ks}")); - } - if let Ok(pkm) = card.public_key_material(KeyType::Decryption) { - decryption_key.public_key_material(pkm.to_string()); - } - output.decryption_key(decryption_key); - - let mut authentication_key = output::KeySlotInfo::default(); - if let Some(fp) = fps.authentication() { - authentication_key.fingerprint(fp.to_spaced_hex()); - } - authentication_key.algorithm(format!( - "{}", - card.algorithm_attributes(KeyType::Authentication)? - )); - if let Some(kgt) = kgt.authentication() { - authentication_key.creation_time(format!("{}", kgt.to_datetime())); - } - if let Some(uif) = card.uif_authentication()? { - authentication_key.touch_policy(format!("{}", uif.touch_policy())); - authentication_key.touch_features(format!("{}", uif.features())); - } - if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) { - authentication_key.status(format!("{ks}")); - } - if let Ok(pkm) = card.public_key_material(KeyType::Authentication) { - authentication_key.public_key_material(pkm.to_string()); - } - output.authentication_key(authentication_key); - - let mut attestation_key = output::KeySlotInfo::default(); - if let Ok(Some(fp)) = card.attestation_key_fingerprint() { - attestation_key.fingerprint(fp.to_spaced_hex()); - } - if let Ok(Some(algo)) = card.attestation_key_algorithm_attributes() { - attestation_key.algorithm(format!("{algo}")); - } - if let Ok(Some(kgt)) = card.attestation_key_generation_time() { - attestation_key.creation_time(format!("{}", kgt.to_datetime())); - } - if let Some(uif) = card.uif_attestation()? { - attestation_key.touch_policy(format!("{}", uif.touch_policy())); - attestation_key.touch_features(format!("{}", uif.features())); - } - - // TODO: get public key data for the attestation key from the card - // if let Ok(pkm) = card.public_key(KeyType::Attestation) { - // attestation_key.public_key_material(pkm.to_string()); - // } - - // "Key-Ref = 0x81 is reserved for the Attestation key of Yubico" - // (see OpenPGP card spec 3.4.1 pg.43) - if let Some(ki) = ki.as_ref() { - if let Some(n) = (0..ki.num_additional()).find(|&n| ki.additional_ref(n) == 0x81) { - let ks = ki.additional_status(n); - attestation_key.status(format!("{ks}")); - } - }; - - output.attestation_key(attestation_key); - - // technical details about the card's state - output.user_pin_valid_for_only_one_signature(pws.pw1_cds_valid_once()); - - output.user_pin_remaining_attempts(pws.err_count_pw1()); - output.admin_pin_remaining_attempts(pws.err_count_pw3()); - output.reset_code_remaining_attempts(pws.err_count_rc()); - - if let Some(ki) = ki { - let num = ki.num_additional(); - for i in 0..num { - // 0x81 is the Yubico attestation key, it has already been used above -> skip here - if ki.additional_ref(i) != 0x81 { - output.additional_key_status( - ki.additional_ref(i), - ki.additional_status(i).to_string(), - ); - } - } - } - - if let Ok(fps) = card.ca_fingerprints() { - for fp in fps.iter().flatten() { - output.ca_fingerprint(fp.to_string()); - } - } - - // FIXME: print "Login Data" - - println!("{}", output.print(format, output_version)?); - - Ok(()) -} diff --git a/tools/src/opgpcard.rs b/tools/src/opgpcard.rs deleted file mode 100644 index 731f11a..0000000 --- a/tools/src/opgpcard.rs +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-FileCopyrightText: 2022 Nora Widdecke -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use anyhow::Result; -use clap::Parser; -use openpgp_card_sequoia::types::CardBackend; -use openpgp_card_sequoia::util::make_cert; -use openpgp_card_sequoia::PublicKey; -use openpgp_card_sequoia::{state::Open, state::Transaction, Card}; -use sequoia_openpgp::Cert; - -mod cli; -mod commands; -mod output; -mod util; -mod versioned_output; - -use cli::OUTPUT_VERSIONS; -use versioned_output::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -const ENTER_USER_PIN: &str = "Enter User PIN:"; -const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:"; - -fn main() -> Result<(), Box> { - env_logger::init(); - - let cli = cli::Cli::parse(); - - match cli.cmd { - cli::Command::OutputVersions {} => { - output_versions(cli.output_version); - } - cli::Command::List {} => { - list_cards(cli.output_format, cli.output_version)?; - } - cli::Command::Status(cmd) => { - commands::status::print_status(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::Info(cmd) => { - commands::info::print_info(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::Ssh(cmd) => { - commands::ssh::print_ssh(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::Pubkey(cmd) => { - commands::pubkey::print_pubkey(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::SetIdentity(cmd) => { - commands::set_identity::set_identity(cmd)?; - } - cli::Command::Decrypt(cmd) => { - commands::decrypt::decrypt(cmd)?; - } - cli::Command::Sign(cmd) => { - commands::sign::sign(cmd)?; - } - cli::Command::Attestation(cmd) => { - commands::attestation::attestation(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::FactoryReset(cmd) => { - commands::factory_reset::factory_reset(cmd)?; - } - cli::Command::Admin(cmd) => { - commands::admin::admin(cli.output_format, cli.output_version, cmd)?; - } - cli::Command::Pin(cmd) => { - commands::pin::pin(&cmd.ident, cmd.cmd)?; - } - } - - Ok(()) -} - -fn output_versions(chosen: OutputVersion) { - for v in OUTPUT_VERSIONS.iter() { - if v == &chosen { - println!("* {v}"); - } else { - println!(" {v}"); - } - } -} - -fn list_cards(format: OutputFormat, output_version: OutputVersion) -> Result<()> { - let cards = util::cards()?; - let mut output = output::List::default(); - if !cards.is_empty() { - for backend in cards { - let mut open: Card = backend.into(); - - output.push(open.transaction()?.application_identifier()?.ident()); - } - } - println!("{}", output.print(format, output_version)?); - Ok(()) -} - -/// Return a card for a read operation. If `ident` is None, and exactly one card -/// is plugged in, that card is returned. (We don't This -fn pick_card_for_reading(ident: Option) -> Result> { - if let Some(ident) = ident { - Ok(util::open_card(&ident)?) - } else { - let mut cards = util::cards()?; - if cards.len() == 1 { - Ok(cards.pop().unwrap()) - } else if cards.is_empty() { - Err(anyhow::anyhow!("No cards found")) - } else { - // The output version for OutputFormat::Text doesn't matter (it's ignored). - list_cards(OutputFormat::Text, OutputVersion::new(0, 0, 0))?; - - println!("Specify which card to use with '--card '\n"); - - Err(anyhow::anyhow!("Found more than one card")) - } - } -} - -fn get_cert( - card: &mut Card, - key_sig: PublicKey, - key_dec: Option, - key_aut: Option, - user_pin: Option<&[u8]>, - user_ids: &[String], - prompt: &dyn Fn(), -) -> Result { - if user_pin.is_none() && card.feature_pinpad_verify() { - println!( - "The public cert will now be generated.\n\n\ - You will need to enter your User PIN multiple times during this process.\n\n" - ); - } - - make_cert( - card, - key_sig, - key_dec, - key_aut, - user_pin, - prompt, - &|| println!("Touch confirmation needed for signing"), - user_ids, - ) -} diff --git a/tools/src/output/attest.rs b/tools/src/output/attest.rs deleted file mode 100644 index 1849d78..0000000 --- a/tools/src/output/attest.rs +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct AttestationCert { - ident: String, - attestation_cert: String, -} - -impl AttestationCert { - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn attestation_cert(&mut self, cert: String) { - self.attestation_cert = cert; - } - - fn text(&self) -> Result { - Ok(format!( - "OpenPGP card {}\n\n{}\n", - self.ident, self.attestation_cert, - )) - } - - fn v1(&self) -> Result { - Ok(AttestationCertV0 { - schema_version: AttestationCertV0::VERSION, - ident: self.ident.clone(), - attestation_cert: self.attestation_cert.clone(), - }) - } -} - -impl OutputBuilder for AttestationCert { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct AttestationCertV0 { - schema_version: OutputVersion, - ident: String, - attestation_cert: String, -} - -impl OutputVariant for AttestationCertV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/generate.rs b/tools/src/output/generate.rs deleted file mode 100644 index cc92843..0000000 --- a/tools/src/output/generate.rs +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct AdminGenerate { - ident: String, - algorithm: String, - public_key: String, -} - -impl AdminGenerate { - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn algorithm(&mut self, algorithm: String) { - self.algorithm = algorithm; - } - - pub fn public_key(&mut self, key: String) { - self.public_key = key; - } - - fn text(&self) -> Result { - // Do not print ident, as the file with the public_key must not contain anything else - Ok(self.public_key.to_string()) - } - - fn v1(&self) -> Result { - Ok(AdminGenerateV0 { - schema_version: AdminGenerateV0::VERSION, - ident: self.ident.clone(), - algorithm: self.algorithm.clone(), - public_key: self.public_key.clone(), - }) - } -} - -impl OutputBuilder for AdminGenerate { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct AdminGenerateV0 { - schema_version: OutputVersion, - ident: String, - algorithm: String, - public_key: String, -} - -impl OutputVariant for AdminGenerateV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/info.rs b/tools/src/output/info.rs deleted file mode 100644 index 9a6cacb..0000000 --- a/tools/src/output/info.rs +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct Info { - ident: String, - card_version: String, - application_id: String, - manufacturer_id: String, - manufacturer_name: String, - card_capabilities: Vec, - card_service_data: Vec, - extended_length_info: Vec, - extended_capabilities: Vec, - algorithms: Option>, - firmware_version: Option, -} - -impl Info { - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn card_version(&mut self, version: String) { - self.card_version = version; - } - - pub fn application_id(&mut self, id: String) { - self.application_id = id; - } - - pub fn manufacturer_id(&mut self, id: String) { - self.manufacturer_id = id; - } - - pub fn manufacturer_name(&mut self, name: String) { - self.manufacturer_name = name; - } - - pub fn card_capability(&mut self, capability: String) { - self.card_capabilities.push(capability); - } - - pub fn card_service_data(&mut self, data: String) { - self.card_service_data.push(data); - } - - pub fn extended_length_info(&mut self, info: String) { - self.extended_length_info.push(info); - } - - pub fn extended_capability(&mut self, capability: String) { - self.extended_capabilities.push(capability); - } - - pub fn algorithm(&mut self, algorithm: String) { - if let Some(ref mut algos) = self.algorithms { - algos.push(algorithm); - } else { - self.algorithms = Some(vec![algorithm]); - } - } - - pub fn firmware_version(&mut self, version: String) { - self.firmware_version = Some(version); - } - - fn text(&self) -> Result { - let mut s = format!("OpenPGP card {}\n\n", self.ident); - - s.push_str(&format!( - "Application Identifier: {}\n", - self.application_id - )); - s.push_str(&format!( - "Manufacturer [{}]: {}\n\n", - self.manufacturer_id, self.manufacturer_name - )); - - if !self.card_capabilities.is_empty() { - s.push_str("Card Capabilities:\n"); - for c in self.card_capabilities.iter() { - s.push_str(&format!("- {c}\n")); - } - s.push('\n'); - } - - if !self.card_service_data.is_empty() { - s.push_str("Card service data:\n"); - for c in self.card_service_data.iter() { - s.push_str(&format!("- {c}\n")); - } - s.push('\n'); - } - - if !self.extended_length_info.is_empty() { - s.push_str("Extended Length Info:\n"); - for c in self.extended_length_info.iter() { - s.push_str(&format!("- {c}\n")); - } - s.push('\n'); - } - - s.push_str("Extended Capabilities:\n"); - for c in self.extended_capabilities.iter() { - s.push_str(&format!("- {c}\n")); - } - s.push('\n'); - - if let Some(algos) = &self.algorithms { - s.push_str("Supported algorithms:\n"); - for c in algos.iter() { - s.push_str(&format!("- {c}\n")); - } - s.push('\n'); - } - - if let Some(v) = &self.firmware_version { - s.push_str(&format!("Firmware Version: {v}\n")); - } - - Ok(s) - } - - fn v1(&self) -> Result { - Ok(InfoV0 { - schema_version: InfoV0::VERSION, - ident: self.ident.clone(), - card_version: self.card_version.clone(), - application_id: self.application_id.clone(), - manufacturer_id: self.manufacturer_id.clone(), - manufacturer_name: self.manufacturer_name.clone(), - card_capabilities: self.card_capabilities.clone(), - card_service_data: self.card_service_data.clone(), - extended_length_info: self.extended_length_info.clone(), - extended_capabilities: self.extended_capabilities.clone(), - algorithms: self.algorithms.clone(), - firmware_version: self.firmware_version.clone(), - }) - } -} - -impl OutputBuilder for Info { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if InfoV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if InfoV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct InfoV0 { - schema_version: OutputVersion, - ident: String, - card_version: String, - application_id: String, - manufacturer_id: String, - manufacturer_name: String, - card_capabilities: Vec, - card_service_data: Vec, - extended_length_info: Vec, - extended_capabilities: Vec, - algorithms: Option>, - firmware_version: Option, -} - -impl OutputVariant for InfoV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/list.rs b/tools/src/output/list.rs deleted file mode 100644 index 8483f59..0000000 --- a/tools/src/output/list.rs +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Default, Debug, Serialize)] -pub struct List { - idents: Vec, -} - -impl List { - pub fn push(&mut self, idnet: String) { - self.idents.push(idnet); - } - - fn text(&self) -> Result { - let s = if self.idents.is_empty() { - "No OpenPGP cards found.".into() - } else { - let mut s = "Available OpenPGP cards:\n".to_string(); - for id in self.idents.iter() { - s.push_str(&format!(" {id}\n")); - } - s - }; - Ok(s) - } - - fn v1(&self) -> Result { - Ok(ListV0 { - schema_version: ListV0::VERSION, - idents: self.idents.clone(), - }) - } -} - -impl OutputBuilder for List { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if ListV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if ListV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct ListV0 { - schema_version: OutputVersion, - idents: Vec, -} - -impl OutputVariant for ListV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/mod.rs b/tools/src/output/mod.rs deleted file mode 100644 index 1534851..0000000 --- a/tools/src/output/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use crate::OutputVersion; - -#[derive(Debug, thiserror::Error)] -pub enum OpgpCardError { - #[error("unknown output version {0}")] - UnknownVersion(OutputVersion), - - #[error("failed to serialize JSON output with serde_json")] - SerdeJson(#[source] serde_json::Error), - - #[error("failed to serialize YAML output with serde_yaml")] - SerdeYaml(#[source] serde_yaml::Error), -} - -mod list; -pub use list::List; - -mod status; -pub use status::{KeySlotInfo, Status}; - -mod info; -pub use info::Info; - -mod ssh; -pub use ssh::Ssh; - -mod pubkey; -pub use pubkey::PublicKey; - -mod generate; -pub use generate::AdminGenerate; - -mod attest; -pub use attest::AttestationCert; diff --git a/tools/src/output/pubkey.rs b/tools/src/output/pubkey.rs deleted file mode 100644 index 0c9ddbd..0000000 --- a/tools/src/output/pubkey.rs +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct PublicKey { - ident: String, - public_key: String, -} - -impl PublicKey { - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn public_key(&mut self, key: String) { - self.public_key = key; - } - - fn text(&self) -> Result { - Ok(format!( - "OpenPGP card {}\n\n{}\n", - self.ident, self.public_key - )) - } - - fn v1(&self) -> Result { - Ok(PublicKeyV0 { - schema_version: PublicKeyV0::VERSION, - ident: self.ident.clone(), - public_key: self.public_key.clone(), - }) - } -} - -impl OutputBuilder for PublicKey { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct PublicKeyV0 { - schema_version: OutputVersion, - ident: String, - public_key: String, -} - -impl OutputVariant for PublicKeyV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/ssh.rs b/tools/src/output/ssh.rs deleted file mode 100644 index 5ba0467..0000000 --- a/tools/src/output/ssh.rs +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct Ssh { - ident: String, - authentication_key_fingerprint: Option, - ssh_public_key: Option, -} - -impl Ssh { - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn authentication_key_fingerprint(&mut self, fp: String) { - self.authentication_key_fingerprint = Some(fp); - } - - pub fn ssh_public_key(&mut self, key: String) { - self.ssh_public_key = Some(key); - } - - fn text(&self) -> Result { - let mut s = format!("OpenPGP card {}\n\n", self.ident); - - if let Some(fp) = &self.authentication_key_fingerprint { - s.push_str(&format!("Authentication key fingerprint:\n{fp}\n\n")); - } - if let Some(key) = &self.ssh_public_key { - s.push_str(&format!("SSH public key:\n{key}\n")); - } - - Ok(s) - } - - fn v1(&self) -> Result { - Ok(SshV0 { - schema_version: SshV0::VERSION, - ident: self.ident.clone(), - authentication_key_fingerprint: self.authentication_key_fingerprint.clone(), - ssh_public_key: self.ssh_public_key.clone(), - }) - } -} - -impl OutputBuilder for Ssh { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if SshV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if SshV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -struct SshV0 { - schema_version: OutputVersion, - ident: String, - authentication_key_fingerprint: Option, - ssh_public_key: Option, -} - -impl OutputVariant for SshV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} diff --git a/tools/src/output/status.rs b/tools/src/output/status.rs deleted file mode 100644 index e65e01b..0000000 --- a/tools/src/output/status.rs +++ /dev/null @@ -1,340 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use serde::Serialize; - -use crate::output::OpgpCardError; -use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion}; - -#[derive(Debug, Default, Serialize)] -pub struct Status { - verbose: bool, // show verbose text output? - pkm: bool, // include public key material in text output? - ident: String, - card_version: String, - cardholder_name: Option, - language_preferences: Vec, - certificate_url: Option, - signature_key: KeySlotInfo, - signature_count: u32, - user_pin_valid_for_only_one_signature: bool, - decryption_key: KeySlotInfo, - authentication_key: KeySlotInfo, - attestation_key: Option, - user_pin_remaining_attempts: u8, - admin_pin_remaining_attempts: u8, - reset_code_remaining_attempts: u8, - additional_key_statuses: Vec<(u8, String)>, - ca_fingerprints: Vec, -} - -impl Status { - pub fn verbose(&mut self, verbose: bool) { - self.verbose = verbose; - } - - pub fn pkm(&mut self, pkm: bool) { - self.pkm = pkm; - } - - pub fn ident(&mut self, ident: String) { - self.ident = ident; - } - - pub fn card_version(&mut self, card_version: String) { - self.card_version = card_version; - } - - pub fn cardholder_name(&mut self, card_holder: String) { - self.cardholder_name = Some(card_holder); - } - - pub fn language_preference(&mut self, pref: String) { - self.language_preferences.push(pref); - } - - pub fn certificate_url(&mut self, url: String) { - self.certificate_url = Some(url); - } - - pub fn signature_key(&mut self, key: KeySlotInfo) { - self.signature_key = key; - } - - pub fn signature_count(&mut self, count: u32) { - self.signature_count = count; - } - - pub fn user_pin_valid_for_only_one_signature(&mut self, sign_pin_valid_once: bool) { - self.user_pin_valid_for_only_one_signature = sign_pin_valid_once; - } - - pub fn decryption_key(&mut self, key: KeySlotInfo) { - self.decryption_key = key; - } - - pub fn authentication_key(&mut self, key: KeySlotInfo) { - self.authentication_key = key; - } - - pub fn attestation_key(&mut self, key: KeySlotInfo) { - self.attestation_key = Some(key); - } - - pub fn user_pin_remaining_attempts(&mut self, count: u8) { - self.user_pin_remaining_attempts = count; - } - - pub fn admin_pin_remaining_attempts(&mut self, count: u8) { - self.admin_pin_remaining_attempts = count; - } - - pub fn reset_code_remaining_attempts(&mut self, count: u8) { - self.reset_code_remaining_attempts = count; - } - - pub fn additional_key_status(&mut self, keyref: u8, status: String) { - self.additional_key_statuses.push((keyref, status)); - } - - pub fn ca_fingerprint(&mut self, fingerprint: String) { - self.ca_fingerprints.push(fingerprint); - } - - fn text(&self) -> Result { - let mut s = String::new(); - - s.push_str(&format!( - "OpenPGP card {} (card version {})\n\n", - self.ident, self.card_version - )); - - let mut nl = false; - if let Some(name) = &self.cardholder_name { - if !name.is_empty() { - s.push_str(&format!("Cardholder: {name}\n")); - nl = true; - } - } - - if let Some(url) = &self.certificate_url { - if !url.is_empty() { - s.push_str(&format!("Certificate URL: {url}\n")); - nl = true; - } - } - - if !self.language_preferences.is_empty() { - let prefs = self.language_preferences.to_vec().join(", "); - if !prefs.is_empty() { - s.push_str(&format!("Language preferences: '{prefs}'\n")); - nl = true; - } - } - - if nl { - s.push('\n'); - } - - s.push_str("Signature key:\n"); - for line in self.signature_key.format(self.verbose, self.pkm) { - s.push_str(&format!(" {line}\n")); - } - if self.verbose { - if self.user_pin_valid_for_only_one_signature { - s.push_str(" User PIN presentation is valid for only one signature\n"); - } else { - s.push_str(" User PIN presentation is valid for unlimited signatures\n"); - } - } - s.push_str(&format!(" Signatures made: {}\n", self.signature_count)); - s.push('\n'); - - s.push_str("Decryption key:\n"); - for line in self.decryption_key.format(self.verbose, self.pkm) { - s.push_str(&format!(" {line}\n")); - } - s.push('\n'); - - s.push_str("Authentication key:\n"); - for line in self.authentication_key.format(self.verbose, self.pkm) { - s.push_str(&format!(" {line}\n")); - } - s.push('\n'); - - if self.verbose { - if let Some(attestation_key) = &self.attestation_key { - if attestation_key.touch_policy.is_some() || attestation_key.algorithm.is_some() { - s.push_str("Attestation key:\n"); - for line in attestation_key.format(self.verbose, self.pkm) { - s.push_str(&format!(" {line}\n")); - } - s.push('\n'); - } - } - } - - s.push_str(&format!( - "Remaining PIN attempts: User: {}, Admin: {}, Reset Code: {}\n", - self.user_pin_remaining_attempts, - self.admin_pin_remaining_attempts, - self.reset_code_remaining_attempts - )); - - if self.verbose { - for (keyref, status) in self.additional_key_statuses.iter() { - s.push_str(&format!("Additional key status (#{keyref}): {status}\n")); - } - } - - Ok(s) - } - - fn v1(&self) -> Result { - Ok(StatusV0 { - schema_version: StatusV0::VERSION, - ident: self.ident.clone(), - card_version: self.card_version.clone(), - cardholder_name: self.cardholder_name.clone(), - language_preferences: self.language_preferences.clone(), - certificate_url: self.certificate_url.clone(), - signature_key: self.signature_key.clone(), - signature_count: self.signature_count, - decryption_key: self.decryption_key.clone(), - authentication_key: self.authentication_key.clone(), - attestation_key: self.attestation_key.clone(), - user_pin_valid_for_only_one_signature: self.user_pin_valid_for_only_one_signature, - user_pin_remaining_attempts: self.user_pin_remaining_attempts, - admin_pin_remaining_attempts: self.admin_pin_remaining_attempts, - reset_code_remaining_attempts: self.reset_code_remaining_attempts, - additional_key_statuses: self.additional_key_statuses.clone(), - // ca_fingerprints: self.ca_fingerprints.clone(), - }) - } -} - -impl OutputBuilder for Status { - type Err = OpgpCardError; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result { - match format { - OutputFormat::Json => { - let result = if StatusV0::VERSION.is_acceptable_for(&version) { - self.v1()?.json() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeJson) - } - OutputFormat::Yaml => { - let result = if StatusV0::VERSION.is_acceptable_for(&version) { - self.v1()?.yaml() - } else { - return Err(Self::Err::UnknownVersion(version)); - }; - result.map_err(Self::Err::SerdeYaml) - } - OutputFormat::Text => Ok(self.text()?), - } - } -} - -#[derive(Debug, Serialize)] -pub struct StatusV0 { - schema_version: OutputVersion, - ident: String, - card_version: String, - cardholder_name: Option, - language_preferences: Vec, - certificate_url: Option, - signature_key: KeySlotInfo, - signature_count: u32, - user_pin_valid_for_only_one_signature: bool, - decryption_key: KeySlotInfo, - authentication_key: KeySlotInfo, - attestation_key: Option, - user_pin_remaining_attempts: u8, - admin_pin_remaining_attempts: u8, - reset_code_remaining_attempts: u8, - additional_key_statuses: Vec<(u8, String)>, - // ca_fingerprints: Vec, // TODO: add to JSON output after clarifying the content -} - -impl OutputVariant for StatusV0 { - const VERSION: OutputVersion = OutputVersion::new(0, 9, 0); -} - -#[derive(Debug, Default, Clone, Serialize)] -pub struct KeySlotInfo { - fingerprint: Option, - creation_time: Option, - algorithm: Option, - touch_policy: Option, - touch_features: Option, - status: Option, - public_key_material: Option, -} - -impl KeySlotInfo { - pub fn fingerprint(&mut self, fingerprint: String) { - self.fingerprint = Some(fingerprint); - } - - pub fn algorithm(&mut self, algorithm: String) { - self.algorithm = Some(algorithm); - } - - pub fn creation_time(&mut self, created: String) { - self.creation_time = Some(created); - } - - pub fn touch_policy(&mut self, policy: String) { - self.touch_policy = Some(policy); - } - - pub fn touch_features(&mut self, features: String) { - self.touch_features = Some(features); - } - - pub fn status(&mut self, status: String) { - self.status = Some(status); - } - - pub fn public_key_material(&mut self, material: String) { - self.public_key_material = Some(material); - } - - fn format(&self, verbose: bool, pkm: bool) -> Vec { - let mut lines = vec![]; - - if let Some(fp) = &self.fingerprint { - lines.push(format!("Fingerprint: {fp}")); - } else { - lines.push("Fingerprint: [unset]".to_string()); - } - if let Some(ts) = &self.creation_time { - lines.push(format!("Creation Time: {ts}")); - } - if let Some(a) = &self.algorithm { - lines.push(format!("Algorithm: {a}")); - } - - if verbose { - if let Some(policy) = &self.touch_policy { - if let Some(features) = &self.touch_features { - lines.push(format!("Touch policy: {policy} (features: {features})")); - } - } - if let Some(status) = &self.status { - lines.push(format!("Key Status: {status}")); - } - } - if pkm { - if let Some(material) = &self.public_key_material { - lines.push(format!("Public key material: {material}")); - } - } - - lines - } -} diff --git a/tools/src/util.rs b/tools/src/util.rs deleted file mode 100644 index a9aa949..0000000 --- a/tools/src/util.rs +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, Context, Result}; -use openpgp_card_pcsc::PcscBackend; -use openpgp_card_sequoia::state::{Admin, Sign, Transaction, User}; -use openpgp_card_sequoia::types::{ - Algo, CardBackend, Curve, EccType, Error, PublicKeyMaterial, StatusBytes, -}; -use openpgp_card_sequoia::Card; - -pub(crate) fn cards() -> Result>, Error> { - PcscBackend::cards(None).map(|cards| cards.into_iter().map(|c| c.into()).collect()) -} - -pub(crate) fn open_card(ident: &str) -> Result, Error> { - Ok(PcscBackend::open_by_ident(ident, None)?.into()) -} - -/// Get pin from file. Or via user input, if no file and no pinpad is available. -/// -/// If a pinpad is available, return Null (the pinpad will be used to get access to the card). -/// -/// `msg` is the message to show when asking the user to enter a PIN. -pub(crate) fn get_pin( - card: &mut Card>, - pin_file: Option, - msg: &str, -) -> Result>> { - if let Some(path) = pin_file { - // we have a pin file - Ok(Some(load_pin(&path).context(format!( - "Failed to read PIN file {}", - path.display() - ))?)) - } else if !card.feature_pinpad_verify() { - // we have no pin file and no pinpad - let pin = rpassword::prompt_password(msg).context("Failed to read PIN")?; - Ok(Some(pin.into_bytes())) - } else { - // we have a pinpad - Ok(None) - } -} - -/// Let the user input a PIN twice, return PIN if both entries match, error otherwise -pub(crate) fn input_pin_twice(msg1: &str, msg2: &str) -> Result> { - // get new user pin - let newpin1 = rpassword::prompt_password(msg1)?; - let newpin2 = rpassword::prompt_password(msg2)?; - - if newpin1 != newpin2 { - Err(anyhow::anyhow!("PINs do not match.")) - } else { - Ok(newpin1.as_bytes().to_vec()) - } -} - -pub(crate) fn verify_to_user<'app, 'open>( - card: &'open mut Card>, - pin: Option<&[u8]>, -) -> Result>, Box> { - if let Some(pin) = pin { - card.verify_user(pin)?; - } else { - if !card.feature_pinpad_verify() { - return Err(anyhow!("No user PIN file provided, and no pinpad found").into()); - }; - - card.verify_user_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?; - } - - card.user_card() - .ok_or_else(|| anyhow!("Couldn't get user access").into()) -} - -pub(crate) fn verify_to_sign<'app, 'open>( - card: &'open mut Card>, - pin: Option<&[u8]>, -) -> Result>, Box> { - if let Some(pin) = pin { - card.verify_user_for_signing(pin)?; - } else { - if !card.feature_pinpad_verify() { - return Err(anyhow!("No user PIN file provided, and no pinpad found").into()); - } - card.verify_user_for_signing_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?; - } - card.signing_card() - .ok_or_else(|| anyhow!("Couldn't get sign access").into()) -} - -pub(crate) fn verify_to_admin<'app, 'open>( - card: &'open mut Card>, - pin: Option<&[u8]>, -) -> Result>, Box> { - if let Some(pin) = pin { - card.verify_admin(pin)?; - } else { - if !card.feature_pinpad_verify() { - return Err(anyhow!("No admin PIN file provided, and no pinpad found").into()); - } - - card.verify_admin_pinpad(&|| println!("Enter admin PIN on card reader pinpad."))?; - } - card.admin_card() - .ok_or_else(|| anyhow!("Couldn't get admin access").into()) -} - -pub(crate) fn load_pin(pin_file: &Path) -> Result> { - let pin = std::fs::read_to_string(pin_file)?; - Ok(pin.trim().as_bytes().to_vec()) -} - -pub(crate) fn open_or_stdin(f: Option<&Path>) -> Result> { - match f { - Some(f) => Ok(Box::new( - std::fs::File::open(f).context("Failed to open input file")?, - )), - None => Ok(Box::new(std::io::stdin())), - } -} - -pub(crate) fn open_or_stdout(f: Option<&Path>) -> Result> { - match f { - Some(f) => Ok(Box::new( - std::fs::File::create(f).context("Failed to open input file")?, - )), - None => Ok(Box::new(std::io::stdout())), - } -} - -fn get_ssh_pubkey(pkm: &PublicKeyMaterial, ident: String) -> Result { - let cardname = format!("opgpcard:{ident}"); - - let (key_type, kind) = match pkm { - PublicKeyMaterial::R(rsa) => { - let key_type = sshkeys::KeyType::from_name("ssh-rsa")?; - - let kind = sshkeys::PublicKeyKind::Rsa(sshkeys::RsaPublicKey { - e: rsa.v().to_vec(), - n: rsa.n().to_vec(), - }); - - Ok((key_type, kind)) - } - PublicKeyMaterial::E(ecc) => { - if let Algo::Ecc(ecc_attrs) = ecc.algo() { - match ecc_attrs.ecc_type() { - EccType::EdDSA => { - let key_type = sshkeys::KeyType::from_name("ssh-ed25519")?; - - let kind = sshkeys::PublicKeyKind::Ed25519(sshkeys::Ed25519PublicKey { - key: ecc.data().to_vec(), - sk_application: None, - }); - - Ok((key_type, kind)) - } - EccType::ECDSA => { - let (curve, name) = match ecc_attrs.curve() { - Curve::NistP256r1 => Ok(( - sshkeys::Curve::from_identifier("nistp256")?, - "ecdsa-sha2-nistp256", - )), - Curve::NistP384r1 => Ok(( - sshkeys::Curve::from_identifier("nistp384")?, - "ecdsa-sha2-nistp384", - )), - Curve::NistP521r1 => Ok(( - sshkeys::Curve::from_identifier("nistp521")?, - "ecdsa-sha2-nistp521", - )), - _ => Err(anyhow!("Unexpected ECDSA curve {:?}", ecc_attrs.curve())), - }?; - - let key_type = sshkeys::KeyType::from_name(name)?; - - let kind = sshkeys::PublicKeyKind::Ecdsa(sshkeys::EcdsaPublicKey { - curve, - key: ecc.data().to_vec(), - sk_application: None, - }); - - Ok((key_type, kind)) - } - _ => Err(anyhow!("Unexpected EccType {:?}", ecc_attrs.ecc_type())), - } - } else { - Err(anyhow!("Unexpected Algo in EccPub {:?}", ecc)) - } - } - _ => Err(anyhow!("Unexpected PublicKeyMaterial type {:?}", pkm)), - }?; - - let pk = sshkeys::PublicKey { - key_type, - comment: Some(cardname), - kind, - }; - - Ok(pk) -} - -/// Return a String representation of an ssh public key, in a form like: -/// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAuTuxILMTvzTIRvaRqqUM3aRDoEBgz/JAoWKsD1ECxy opgpcard:FFFE:43194240" -pub(crate) fn get_ssh_pubkey_string(pkm: &PublicKeyMaterial, ident: String) -> Result { - let pk = get_ssh_pubkey(pkm, ident)?; - - let mut v = vec![]; - pk.write(&mut v)?; - - let s = String::from_utf8_lossy(&v).to_string(); - - Ok(s.trim().into()) -} - -/// Gnuk doesn't allow the User password (pw1) to be changed while no -/// private key material exists on the card. -/// -/// This fn checks for Gnuk's Status code and the case that no keys exist -/// on the card, and prints a note to the user, pointing out that the -/// absence of keys on the card might be the reason for the error they get. -pub(crate) fn print_gnuk_note(err: Error, card: &Card) -> Result<()> { - if matches!( - err, - Error::CardStatus(StatusBytes::ConditionOfUseNotSatisfied) - ) { - // check if no keys exist on the card - let fps = card.fingerprints()?; - if fps.signature().is_none() && fps.decryption().is_none() && fps.authentication().is_none() - { - println!( - "\nNOTE: Some cards (e.g. Gnuk) don't allow \ - User PIN change while no keys exist on the card." - ); - } - } - Ok(()) -} - -pub(crate) fn pem_encode(data: Vec) -> String { - const PEM_TAG: &str = "CERTIFICATE"; - - let pem = pem::Pem { - tag: String::from(PEM_TAG), - contents: data, - }; - - pem::encode(&pem) -} diff --git a/tools/src/versioned_output.rs b/tools/src/versioned_output.rs deleted file mode 100644 index b0c3804..0000000 --- a/tools/src/versioned_output.rs +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use std::str::FromStr; - -use clap::ValueEnum; -use semver::Version; -use serde::{Serialize, Serializer}; - -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -pub enum OutputFormat { - Json, - Text, - Yaml, -} - -#[derive(Debug, Clone)] -pub struct OutputVersion { - version: Version, -} - -impl OutputVersion { - pub const fn new(major: u64, minor: u64, patch: u64) -> Self { - Self { - version: Version::new(major, minor, patch), - } - } - - /// Does this version fulfill the needs of the version that is requested? - pub fn is_acceptable_for(&self, wanted: &Self) -> bool { - self.version.major == wanted.version.major - && (self.version.minor > wanted.version.minor - || (self.version.minor == wanted.version.minor - && self.version.patch >= wanted.version.patch)) - } -} - -impl std::fmt::Display for OutputVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.version) - } -} - -impl FromStr for OutputVersion { - type Err = semver::Error; - - fn from_str(s: &str) -> Result { - let v = Version::parse(s)?; - Ok(Self::new(v.major, v.minor, v.patch)) - } -} - -impl PartialEq for &OutputVersion { - fn eq(&self, other: &Self) -> bool { - self.version == other.version - } -} - -impl Serialize for OutputVersion { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub trait OutputBuilder { - type Err; - - fn print(&self, format: OutputFormat, version: OutputVersion) -> Result; -} - -pub trait OutputVariant: Serialize { - const VERSION: OutputVersion; - fn json(&self) -> Result { - serde_json::to_string_pretty(self) - } - fn yaml(&self) -> Result { - serde_yaml::to_string(self) - } -} diff --git a/tools/subplot/opgpcard.md b/tools/subplot/opgpcard.md deleted file mode 100644 index 9dedc5e..0000000 --- a/tools/subplot/opgpcard.md +++ /dev/null @@ -1,238 +0,0 @@ - - -# Introduction - -This document describes the requirements and acceptance criteria for -the `opgpcard` tool, and also how to verify that they are met. This -document is meant to be read and understood by all stakeholders, and -processed by the [Subplot](https://subplot.tech/) tool, which also -generates code to automatically perform the verification. - -## Note about running the tests described here - -The verification scenarios in this document assume the availability of -a virtual smart card. Specifically one described in -. The -`openpgp-card/tools` crate is set up to generate tests only if the -environment variable `CARD_BASED_TESTS` is set (to any value), -and the `openpgp-card` repository `.gitlab-ci.yml` file is set up to -set that environment variable when the repository is tested in GitLab CI. - -This means that if you run `cargo test`, no test code is normally -generated from this document. To run the tests locally, outside of -GitLab CI, use the script `tools/cargo-test-in-docker`. - -# Acceptance criteria - -These scenarios mainly test the JSON output format of the tool. That -format is meant for consumption by other tools, and it is thus more -important that it stays stable. The text output that is meant for -human consumption may change at will, so it's not worth testing. - -## Smoke test - -_Requirement: The tool can report its version._ - -Justification: This is useful mainly to make sure the tool can be run -at all. As such, it acts as a simple [smoke -test](https://en.wikipedia.org/wiki/Smoke_testing_(software)). -However, if this fails, then nothing else has a chance to work. - -Note that this is not in JSON format, as it is output by the `clap` -library, and `opgpcard` doesn't affect what it looks like. - -~~~scenario -given an installed opgpcard -when I run opgpcard --version -then stdout matches regex ^opgpcard \d+\.\d+\.\d+$ -~~~ - -## List cards: `opgpcard list` - -_Requirement: The tool lists available cards._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -when I run opgpcard --output-format=json list -then stdout, as JSON, matches embedded file list.json -~~~ - -~~~{#list.json .file .json} -{ - "idents": ["AFAF:00001234"] -} -~~~ - -## Card status: `opgpcard status` - -_Requirement: The tool shows status of available cards._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -when I run opgpcard --output-format=json status -then stdout, as JSON, matches embedded file status.json -~~~ - -~~~{#status.json .file .json} -{ - "card_version": "2.0", - "ident": "AFAF:00001234" -} -~~~ - -## Card information: `opgpcard info` - -_Requirement: The tool shows information about available cards._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -when I run opgpcard --output-format=json info -then stdout, as JSON, matches embedded file info.json -~~~ - -~~~{#info.json .file .json} -{ - "card_version": "2.0", - "application_id": "D276000124 01 0200 AFAF 00001234 0000", - "manufacturer_id": "AFAF", - "manufacturer_name": "Unknown", - "card_service_data": [], - "ident": "AFAF:00001234" -} -~~~ - -## Key generation: `opgpcard generate` and `opgpcard decrypt` - -_Requirement: The tool is able to generate keys and use them for decryption._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -given file admin.pin -given file user.pin -when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin generate --user-pin user.pin --output certfile -then file certfile contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" -then file certfile contains "-----END PGP PUBLIC KEY BLOCK-----" - -given file message -when I run sq encrypt message --recipient-cert certfile --output message.enc -and I run opgpcard decrypt --card AFAF:00001234 --user-pin user.pin message.enc --output message.dec -then files message and message.dec match -~~~ - -~~~{#admin.pin .file} -12345678 -~~~ - -~~~{#user.pin .file} -123456 -~~~ - -~~~{#message .file} -Hello World! -~~~ - -## Key generation: `opgpcard generate` and `opgpcard sign` - -_Requirement: The tool is able to generate keys and use them for signing._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -given file admin.pin -given file user.pin -when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin generate --user-pin user.pin --output certfile -then file certfile contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" -then file certfile contains "-----END PGP PUBLIC KEY BLOCK-----" - -given file message -when I run opgpcard sign message --card AFAF:00001234 --user-pin user.pin --detached --output message.sig -when I run sq verify message --detached message.sig --signer-cert certfile -then stderr contains "1 good signature." -~~~ - -## Key import: `opgpcard import` and `opgpcard decrypt` - -_Requirement: The tool is able to import keys and use them for decryption._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -given file admin.pin -given file user.pin -given file nist256key -when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin import nist256key -then stdout contains "CCCFFFAAC77C9F9D3BB2D2CA3C93515DA813C03F" -then stdout contains "360EC3C59A7D8E51DCE9FA1171858B15EE7F4BCA" -then stdout contains "6D186AC7C6761FC22BE07557D2BE4918C44C74D9" - -given file message -when I run sq encrypt message --recipient-cert nist256key --output message.enc -and I run opgpcard decrypt --card AFAF:00001234 --user-pin user.pin message.enc --output message.dec -then files message and message.dec match -~~~ - -## Key import: `opgpcard import` and `opgpcard sign` - -_Requirement: The tool is able to import keys and use them for signing._ - -This is not at all a thorough test, but it exercises the simple happy -paths of the subcommand. - -~~~scenario -given an installed opgpcard -given file admin.pin -given file nist256key -when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin import nist256key -then stdout contains "CCCFFFAAC77C9F9D3BB2D2CA3C93515DA813C03F" -then stdout contains "360EC3C59A7D8E51DCE9FA1171858B15EE7F4BCA" -then stdout contains "6D186AC7C6761FC22BE07557D2BE4918C44C74D9" - -given file user.pin -given file message -when I run opgpcard sign message --card AFAF:00001234 --user-pin user.pin --detached --output message.sig -when I run sq verify message --detached message.sig --signer-cert nist256key -then stderr contains "1 good signature." -~~~ - -~~~{#nist256key .file} ------BEGIN PGP PRIVATE KEY BLOCK----- - -lHcEYPP/JxMIKoZIzj0DAQcCAwT5a9JIM6BX1zxvFkNr2LMGLyTw72+iXsUZlA8X -w3Bn91jVRpSSIITjibHKliS2e2kZlaoHOZvlXmZ3nqOANjV+AAEAzCPG24MzHigZ -qyoaNr+7o6u/D8DndXHhsrERqm9cCgcOybQfTklTVCBQMjU2IDxuaXN0MjU2QGV4 -YW1wbGUub3JnPoiQBBMTCAA4FiEEzM//qsd8n507stLKPJNRXagTwD8FAmDz/ycC -GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQPJNRXagTwD+bZAD/fu4NjabH -GKHB1dIpqX+opDt8E3RFes58P+p4wh8W+xEBAMcPs6HLYvcLLkqtpV06wKYngPY+ -Ln/wcpQOagwO+EgfnHsEYPP/JxIIKoZIzj0DAQcCAwTtyP4rOGNlU+Tzpa7UYv5h -jR/T9DzMVUntaFhb3Cm0ung7IEGNAOcbgpCx/fdm7BPL+9MJB+qwpsz8bQa4DfnE -AwEIBwABALvh9XLpqe1MqwPodYlWKgw4me/tR2FNKmLXPC1gl3g7EAeIeAQYEwgA -IBYhBMzP/6rHfJ+dO7LSyjyTUV2oE8A/BQJg8/8nAhsMAAoJEDyTUV2oE8A/SMMA -/3DuQU8hb+U9U2nX93bHwpTBQfAONsEn/vUeZ6u4NdX4AP9ABH//08SFfFttiWHm -TTAR9e57Rw0DhI/wb6qqWABIyZx3BGDz/zkTCCqGSM49AwEHAgMEJz+bbG6RHQag -BoULLuklPRUtQauVTxM9WZZG3PEAnIZuu4LKkHn/JPAN04iSV+K3lBWN+HALVZSV -kFweNSOX6gAA/RD5JKvdwS3CofhQY+czewkb8feXGLQIaPS9rIWP7QX4En2IeAQY -EwgAIBYhBMzP/6rHfJ+dO7LSyjyTUV2oE8A/BQJg8/85AhsgAAoJEDyTUV2oE8A/ -CSkA/2WnUoIwtv4ZBhuCpJY/GIFqRJEPgQ7DW1bXTrYsoTehAQD1wDkG0vD6Jnfu -QIPHexNllmYakW7WNqu1gobPuNEQyw== -=E2Hb ------END PGP PRIVATE KEY BLOCK----- -~~~ diff --git a/tools/subplot/opgpcard.rs b/tools/subplot/opgpcard.rs deleted file mode 100644 index 0297eb4..0000000 --- a/tools/subplot/opgpcard.rs +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use subplotlib::file::SubplotDataFile; -use subplotlib::steplibrary::runcmd::Runcmd; - -use serde_json::Value; -use std::path::Path; - -#[derive(Debug, Default)] -struct SubplotContext {} - -impl ContextElement for SubplotContext {} - -#[step] -#[context(SubplotContext)] -#[context(Runcmd)] -fn install_opgpcard(context: &ScenarioContext) { - let target_exe = env!("CARGO_BIN_EXE_opgpcard"); - let target_path = Path::new(target_exe); - let target_path = target_path.parent().ok_or("No parent?")?; - context.with_mut( - |context: &mut Runcmd| { - context.prepend_to_path(target_path); - Ok(()) - }, - false, - )?; -} - -#[step] -#[context(Runcmd)] -fn stdout_matches_json_file(context: &ScenarioContext, file: SubplotDataFile) { - let expected: Value = serde_json::from_slice(file.data())?; - println!("expecting JSON: {:#?}", expected); - - let stdout = context.with(|runcmd: &Runcmd| Ok(runcmd.stdout_as_string()), false)?; - let actual: Value = serde_json::from_str(&stdout)?; - println!("stdout JSON: {:#?}", actual); - - println!("fuzzy checking JSON values"); - assert!(json_eq(&actual, &expected)); -} - -// Fuzzy match JSON values. For objects, anything in expected must be -// in actual, but it's OK for there to be extra things. -fn json_eq(actual: &Value, expected: &Value) -> bool { - match actual { - Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { - println!("simple value"); - println!("actual ={:?}", actual); - println!("expected={:?}", expected); - let eq = actual == expected; - println!("simple value eq={}", eq); - return eq; - } - Value::Array(a_values) => { - if let Value::Array(e_values) = expected { - println!("both actual and equal are arrays"); - for (a_value, e_value) in a_values.iter().zip(e_values.iter()) { - println!("comparing corresponding array elements"); - if !json_eq(a_value, e_value) { - println!("array elements differ"); - return false; - } - } - println!("arrays match"); - return true; - } else { - println!("actual is array, expected is not"); - return false; - } - } - Value::Object(a_obj) => { - if let Value::Object(e_obj) = expected { - println!("both actual and equal are objects"); - for key in e_obj.keys() { - println!("checking key {}", key); - if !a_obj.contains_key(key) { - println!("key {} is missing from actual", key); - return false; - } - let a_value = a_obj.get(key).unwrap(); - let e_value = e_obj.get(key).unwrap(); - let eq = json_eq(a_value, e_value); - println!("values for {} eq={}", key, eq); - if !eq { - return false; - } - } - println!("objects match"); - return true; - } else { - println!("actual is object, expected is not"); - return false; - } - } - } -} diff --git a/tools/subplot/opgpcard.subplot b/tools/subplot/opgpcard.subplot deleted file mode 100644 index 855d350..0000000 --- a/tools/subplot/opgpcard.subplot +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Lars Wirzenius -# SPDX-License-Identifier: MIT OR Apache-2.0 - -title: "opgpcard acceptance tests" -markdowns: - - opgpcard.md -bindings: - - opgpcard.yaml - - lib/files.yaml - - lib/runcmd.yaml -impls: - rust: - - opgpcard.rs -classes: - - json diff --git a/tools/subplot/opgpcard.yaml b/tools/subplot/opgpcard.yaml deleted file mode 100644 index 1bdc70b..0000000 --- a/tools/subplot/opgpcard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Lars Wirzenius -# SPDX-License-Identifier: MIT OR Apache-2.0 - -- given: an installed opgpcard - impl: - rust: - function: install_opgpcard - -- then: "stdout, as JSON, matches embedded file {file:file}" - impl: - rust: - function: stdout_matches_json_file diff --git a/tools/subplot/test-in-docker.sh b/tools/subplot/test-in-docker.sh deleted file mode 100755 index c24ae9a..0000000 --- a/tools/subplot/test-in-docker.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# -# Run "cargo test" inside a Docker container with virtual cards -# installed and running. This will allow at least rudimentary testing -# of actual card functionality of opgpcard. -# -# SPDX-FileCopyrightText: 2022 Lars Wirzenius -# SPDX-License-Identifier: MIT OR Apache-2.0 - -set -euo pipefail - -image="registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps" - -src="$(cd .. && pwd)" - -if ! [ -e Cargo.toml ] || ! grep '^name.*openpgp-card-tools' Cargo.toml; then - echo "MUST run this in the root of the openpgp-card-tool crate" 1>&2 - exit 1 -fi - -cargo build -docker run --rm -t \ - --volume "cargo:/cargo" \ - --volume "dotcargo:/root/.cargo" \ - --volume "$src:/opt" "$image" \ - bash -c ' -/etc/init.d/pcscd start && \ -su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim && \ -cd /opt/tools && env CARGO_TARGET_DIR=/cargo cargo test' diff --git a/tools/tests/opgpcard.rs b/tools/tests/opgpcard.rs deleted file mode 100644 index 72f3554..0000000 --- a/tools/tests/opgpcard.rs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Lars Wirzenius -// SPDX-License-Identifier: MIT OR Apache-2.0 - -#![allow(clippy::needless_return)] - -include!(concat!(env!("OUT_DIR"), "/opgpcard.rs"));