Merge branch 'main' of gitlab.com:openpgp-card/openpgp-card
This commit is contained in:
commit
10484aeeb4
102 changed files with 3989 additions and 8003 deletions
179
.gitlab-ci.yml
179
.gitlab-ci.yml
|
@ -1,11 +1,12 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2022 Nora Widdecke <mail@nora.pink>
|
||||
# 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
|
||||
|
|
|
@ -10,7 +10,3 @@ License: CC0-1.0
|
|||
Files: card-functionality/data/*
|
||||
Copyright: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: tools/debian/*
|
||||
Copyright: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
License: CC0-1.0
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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",
|
||||
]
|
||||
|
|
36
README.md
36
README.md
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
|
@ -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 <br/> (pcsclite backend)"] --> OC
|
||||
OS["openpgp-card-scdc <br/> (scdaemon backend)"] --> OC["openpgp-card <br/> (low level API)"]
|
||||
CB["card-backend <br/> (shared trait)"] --> OP
|
||||
CB --> OS
|
||||
OP["card-backend-pcsc <br/> (pcsclite backend)"] --> OC
|
||||
OS["card-backend-scdc <br/> (scdaemon backend)"] --> OC["openpgp-card <br/> (low level API)"]
|
||||
OC --> OCS["openpgp-card-sequoia <br/> (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:
|
||||
|
|
15
card-backend/Cargo.toml
Normal file
15
card-backend/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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"
|
12
card-backend/README.md
Normal file
12
card-backend/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# 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.
|
202
card-backend/src/lib.rs
Normal file
202
card-backend/src/lib.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Box<dyn CardTransaction + Send + Sync + '_>, 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<Vec<u8>, SmartcardError>;
|
||||
|
||||
/// Select `application` on the card
|
||||
fn select(&mut self, application: &[u8]) -> Result<Vec<u8>, 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<usize> {
|
||||
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<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError>;
|
||||
|
||||
/// Modify the PIN `pin` via the reader pinpad
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, 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),
|
||||
}
|
|
@ -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"
|
||||
|
|
17
card-functionality/ci/virt-canokey.toml
Normal file
17
card-functionality/ci/virt-canokey.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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",
|
||||
]
|
|
@ -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",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<ShareMode> = Some(ShareMode::Shared);
|
||||
// const SHARE_MODE: Option<ShareMode> = 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<Box<dyn CardBackend + Send + Sync>> {
|
||||
pub fn get_card(&self) -> Result<openpgp_card_sequoia::Card<Open>> {
|
||||
self.tc.open()
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ pub enum TestCard {
|
|||
}
|
||||
|
||||
impl TestCard {
|
||||
pub fn open(&self) -> Result<Box<dyn CardBackend + Send + Sync>> {
|
||||
pub fn open(&self) -> Result<openpgp_card_sequoia::Card<Open>> {
|
||||
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<Box<dyn CardBackend + Send + Sync>, Error> = loop {
|
||||
let res = PcscBackend::open_by_ident(ident, SHARE_MODE);
|
||||
let card: Result<openpgp_card_sequoia::Card<Open>, 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>::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::<Open>::new(backend)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Open> = 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?}");
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Open> = 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");
|
||||
|
|
|
@ -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<Open> = backend.into();
|
||||
let mut card: Card<Open> = Card::<Open>::new(backend?)?;
|
||||
|
||||
println!(" {}", card.transaction()?.application_identifier()?.ident());
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Open> = 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, &[])?;
|
||||
|
||||
// ---
|
||||
|
||||
|
|
|
@ -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<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
|
||||
pub fn test_decrypt(tx: &mut Card<Transaction>, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
assert_eq!(
|
||||
param.len(),
|
||||
2,
|
||||
|
@ -63,13 +60,9 @@ pub fn test_decrypt(pgp: &mut OpenPgp, param: &[&str]) -> Result<TestOutput, Tes
|
|||
|
||||
let msg = param[1].to_string();
|
||||
|
||||
pgpt.verify_pw1_user(b"123456")?;
|
||||
|
||||
let p = StandardPolicy::new();
|
||||
|
||||
let mut transaction = Card::<Transaction>::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<TestOutput, Tes
|
|||
}
|
||||
|
||||
/// Run after each "upload keys", if key *was* uploaded (?)
|
||||
pub fn test_sign(pgp: &mut OpenPgp, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
|
||||
pub fn test_sign(tx: &mut Card<Transaction>, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
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::<Transaction>::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<TestOutput, TestEr
|
|||
}
|
||||
|
||||
fn check_key_upload_metadata(
|
||||
pgpt: &mut OpenPgpTransaction,
|
||||
admin: &mut Card<Admin>,
|
||||
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<TestOutput, TestError> {
|
||||
pub fn test_print_caps(
|
||||
pgp: &mut openpgp_card::Card,
|
||||
_param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput,
|
|||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub fn test_print_algo_info(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
pub fn test_print_algo_info(
|
||||
pgp: &mut openpgp_card::Card,
|
||||
_param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
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<TestOu
|
|||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub fn test_upload_keys(pgp: &mut OpenPgp, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
|
||||
pub fn test_upload_keys(
|
||||
tx: &mut Card<Transaction>,
|
||||
param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput,
|
|||
}
|
||||
|
||||
/// Generate keys for each of the three KeyTypes
|
||||
pub fn test_keygen(pgp: &mut OpenPgp, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let pgpt = pgp.transaction()?;
|
||||
|
||||
let mut transaction = Card::<Transaction>::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<Transaction>, param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput, Test
|
|||
let alg = AlgoSimple::try_from(algo)?;
|
||||
|
||||
println!(" Generate subkey for Signing");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Signing, Some(alg))?;
|
||||
admin.set_algorithm(KeyType::Signing, alg)?;
|
||||
let (pkm, ts) = admin.generate_key(KeyType::Signing)?;
|
||||
let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?;
|
||||
|
||||
println!(" Generate subkey for Decryption");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Decryption, Some(alg))?;
|
||||
let key_dec = public_key_material_to_key(
|
||||
&pkm,
|
||||
KeyType::Decryption,
|
||||
&ts,
|
||||
Some(HashAlgorithm::SHA256),
|
||||
Some(SymmetricAlgorithm::AES128),
|
||||
)?;
|
||||
admin.set_algorithm(KeyType::Decryption, alg)?;
|
||||
let (pkm, ts) = admin.generate_key(KeyType::Decryption)?;
|
||||
let key_dec = public_key_material_to_key(&pkm, KeyType::Decryption, &ts, None, None)?;
|
||||
|
||||
println!(" Generate subkey for Authentication");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Authentication, Some(alg))?;
|
||||
admin.set_algorithm(KeyType::Authentication, alg)?;
|
||||
let (pkm, ts) = admin.generate_key(KeyType::Authentication)?;
|
||||
let key_aut = public_key_material_to_key(&pkm, KeyType::Authentication, &ts, None, None)?;
|
||||
|
||||
tx.reload_ard()?;
|
||||
|
||||
// Generate a Cert for this set of generated keys
|
||||
let cert = make_cert(
|
||||
&mut transaction,
|
||||
tx,
|
||||
key_sig,
|
||||
Some(key_dec),
|
||||
Some(key_aut),
|
||||
Some(b"123456"),
|
||||
Some("123456"),
|
||||
&|| {},
|
||||
&|| {},
|
||||
&[],
|
||||
|
@ -262,16 +252,15 @@ pub fn test_keygen(pgp: &mut OpenPgp, param: &[&str]) -> Result<TestOutput, Test
|
|||
}
|
||||
|
||||
/// Construct public key based on data from the card
|
||||
pub fn test_get_pub(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
pub fn test_get_pub(mut card: Card<Open>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput, Te
|
|||
|
||||
// --
|
||||
|
||||
let dec = pgpt.public_key(KeyType::Decryption)?;
|
||||
let dec = transaction.public_key_material(KeyType::Decryption)?;
|
||||
let ts = times.decryption().unwrap().get().into();
|
||||
let key = public_key_material_and_fp_to_key(
|
||||
&dec,
|
||||
|
@ -293,7 +282,7 @@ pub fn test_get_pub(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, Te
|
|||
|
||||
// --
|
||||
|
||||
let auth = pgpt.public_key(KeyType::Authentication)?;
|
||||
let auth = transaction.public_key_material(KeyType::Authentication)?;
|
||||
let ts = times.authentication().unwrap().get().into();
|
||||
let key = public_key_material_and_fp_to_key(
|
||||
&auth,
|
||||
|
@ -307,10 +296,8 @@ pub fn test_get_pub(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, Te
|
|||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub fn test_reset(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
|
||||
pgpt.factory_reset()?;
|
||||
pub fn test_reset(tx: &mut Card<Transaction>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
tx.factory_reset()?;
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
|
@ -319,25 +306,29 @@ pub fn test_reset(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, Test
|
|||
///
|
||||
/// Returns an empty TestOutput, throws errors for unexpected Status codes
|
||||
/// and for unequal field values.
|
||||
pub fn test_set_user_data(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
|
||||
pgpt.verify_pw3(b"12345678")?;
|
||||
pub fn test_set_user_data(
|
||||
tx: &mut Card<Transaction>,
|
||||
_param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
let mut admin = tx.to_admin_card("12345678")?;
|
||||
|
||||
// name
|
||||
pgpt.set_name(b"Bar<<Foo")?;
|
||||
admin.set_cardholder_name("Bar<<Foo")?;
|
||||
|
||||
// lang
|
||||
pgpt.set_lang(&[['d', 'e'].into(), ['e', 'n'].into()])?;
|
||||
admin.set_lang(&[['d', 'e'].into(), ['e', 'n'].into()])?;
|
||||
|
||||
// sex
|
||||
pgpt.set_sex(Sex::Female)?;
|
||||
admin.set_sex(Sex::Female)?;
|
||||
|
||||
// url
|
||||
pgpt.set_url(b"https://duckduckgo.com/")?;
|
||||
admin.set_url("https://duckduckgo.com/")?;
|
||||
|
||||
// read all the fields back again, expect equal data
|
||||
let ch = pgpt.cardholder_related_data()?;
|
||||
// reload application releated data
|
||||
tx.reload_ard()?;
|
||||
|
||||
// compare the reloaded fields, expect equal data
|
||||
let ch = tx.cardholder_related_data()?;
|
||||
|
||||
assert_eq!(ch.name(), Some("Bar<<Foo".as_bytes()));
|
||||
assert_eq!(
|
||||
|
@ -346,44 +337,61 @@ pub fn test_set_user_data(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutp
|
|||
);
|
||||
assert_eq!(ch.sex(), Some(Sex::Female));
|
||||
|
||||
let url = pgpt.url()?;
|
||||
assert_eq!(&url, b"https://duckduckgo.com/");
|
||||
let url = tx.url()?;
|
||||
assert_eq!(&url, "https://duckduckgo.com/");
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub fn test_private_data(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
pub fn test_set_login_data(
|
||||
tx: &mut Card<Transaction>,
|
||||
_params: &[&str],
|
||||
) -> std::result::Result<TestOutput, TestError> {
|
||||
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<Open>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
// 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<TestOutpu
|
|||
// Ok(out)
|
||||
// }
|
||||
|
||||
pub fn test_pw_status(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
pub fn test_pw_status(mut card: Card<Open>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput,
|
|||
/// Outputs:
|
||||
/// - verify pw3 (check) -> Status
|
||||
/// - verify pw1 (check) -> Status
|
||||
pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
pub fn test_verify(mut card: Card<Open>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut transaction = card.transaction()?;
|
||||
|
||||
// Steps:
|
||||
//
|
||||
|
@ -489,7 +497,8 @@ pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, Tes
|
|||
let mut out = vec![];
|
||||
|
||||
// try to set name without verify, assert result is not ok!
|
||||
let res = pgpt.set_name("Notverified<<Hello".as_bytes());
|
||||
let mut admin = transaction.to_admin_card(None)?;
|
||||
let res = admin.set_cardholder_name("Notverified<<Hello");
|
||||
|
||||
if let Err(Error::CardStatus(s)) = res {
|
||||
assert_eq!(s, StatusBytes::SecurityStatusNotSatisfied);
|
||||
|
@ -497,9 +506,9 @@ pub fn test_verify(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, Tes
|
|||
panic!("Status should be 'SecurityStatusNotSatisfied'");
|
||||
}
|
||||
|
||||
pgpt.verify_pw3(b"12345678")?;
|
||||
transaction.verify_admin_pin("12345678")?;
|
||||
|
||||
match pgpt.check_pw3() {
|
||||
match transaction.check_admin_verified() {
|
||||
Err(Error::CardStatus(s)) => {
|
||||
// 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<TestOutput, Tes
|
|||
Ok(_) => out.push(TestResult::StatusOk),
|
||||
}
|
||||
|
||||
pgpt.set_name(b"Admin<<Hello")?;
|
||||
let mut admin = transaction.to_admin_card(None)?;
|
||||
|
||||
let cardholder = pgpt.cardholder_related_data()?;
|
||||
admin.set_cardholder_name("Admin<<Hello")?;
|
||||
|
||||
transaction.reload_ard()?;
|
||||
|
||||
let cardholder = transaction.cardholder_related_data()?;
|
||||
assert_eq!(cardholder.name(), Some("Admin<<Hello".as_bytes()));
|
||||
|
||||
pgpt.verify_pw1_user(b"123456")?;
|
||||
transaction.verify_user_pin("123456")?;
|
||||
|
||||
match pgpt.check_pw3() {
|
||||
match transaction.check_user_verified() {
|
||||
Err(Error::CardStatus(s)) => {
|
||||
// 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<TestOutput, Tes
|
|||
Ok(_) => out.push(TestResult::StatusOk),
|
||||
}
|
||||
|
||||
pgpt.set_name(b"There<<Hello")?;
|
||||
let mut admin = transaction.to_admin_card(None)?;
|
||||
|
||||
let cardholder = pgpt.cardholder_related_data()?;
|
||||
admin.set_cardholder_name("There<<Hello")?;
|
||||
|
||||
transaction.reload_ard()?;
|
||||
let cardholder = transaction.cardholder_related_data()?;
|
||||
assert_eq!(cardholder.name(), Some("There<<Hello".as_bytes()));
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn test_change_pw(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
let mut pgpt = pgp.transaction()?;
|
||||
pub fn test_change_pw(mut card: Card<Open>, _param: &[&str]) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput,
|
|||
}
|
||||
|
||||
println!("verify good pw1");
|
||||
pgpt.verify_pw1_user(b"abcdef")?;
|
||||
transaction.verify_user_pin("abcdef")?;
|
||||
|
||||
println!("verify bad pw3");
|
||||
match pgpt.verify_pw3(b"00000000") {
|
||||
match transaction.verify_admin_pin("00000000") {
|
||||
Err(Error::CardStatus(StatusBytes::SecurityStatusNotSatisfied)) => {
|
||||
// this is expected
|
||||
}
|
||||
|
@ -582,36 +598,36 @@ pub fn test_change_pw(pgp: &mut OpenPgp, _param: &[&str]) -> Result<TestOutput,
|
|||
}
|
||||
|
||||
println!("verify good pw3");
|
||||
pgpt.verify_pw3(b"abcdefgh")?;
|
||||
transaction.verify_admin_pin("abcdefgh")?;
|
||||
|
||||
println!("change pw3 back to default");
|
||||
pgpt.change_pw3(b"abcdefgh", b"12345678")?;
|
||||
transaction.change_admin_pin("abcdefgh", "12345678")?;
|
||||
|
||||
println!("change pw1 back to default");
|
||||
pgpt.change_pw1(b"abcdef", b"123456")?;
|
||||
transaction.change_user_pin("abcdef", "123456")?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn test_reset_retry_counter(
|
||||
pgp: &mut OpenPgp,
|
||||
mut card: Card<Open>,
|
||||
_param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
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<TestOutput, TestError>,
|
||||
card: &mut Card<Transaction>,
|
||||
t: fn(&mut Card<Transaction>, &[&str]) -> Result<TestOutput, TestError>,
|
||||
param: &[&str],
|
||||
) -> Result<TestOutput, TestError> {
|
||||
let card = tc.get_card()?;
|
||||
let mut pgp = OpenPgp::new(card);
|
||||
t(&mut pgp, param)
|
||||
t(card, param)
|
||||
}
|
||||
|
|
|
@ -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<Admin>,
|
||||
cert: &Cert,
|
||||
policy: &dyn Policy,
|
||||
) -> Result<Vec<(String, KeyGenerationTime)>> {
|
||||
|
@ -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<String> {
|
|||
|
||||
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())?;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2021 Wiktor Kwapisiewicz <wiktor@metacode.biz>
|
||||
// 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<dyn std::error::Error>> {
|
|||
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<Open> = backend.into();
|
||||
let mut card: Card<Open> = Card::<Open>::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();
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2021 Wiktor Kwapisiewicz <wiktor@metacode.biz>
|
||||
// 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<dyn std::error::Error>> {
|
|||
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<Open> = backend.into();
|
||||
let mut card: Card<Open> = Card::<Open>::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();
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
|
@ -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**
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<dyn Error>> {
|
|||
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>::open_by_ident(cards, &test_card_ident)?;
|
||||
|
||||
let mut card: Card<Open> = 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<<Foo")?;
|
||||
println!("set name - ok");
|
||||
|
||||
admin.set_sex(Sex::NotApplicable)?;
|
||||
println!("set sex - ok");
|
||||
|
||||
admin.set_lang(&[['e', 'n'].into()])?;
|
||||
println!("set lang - ok");
|
||||
|
||||
admin.set_url("https://keys.openpgp.org")?;
|
||||
println!("set url - ok");
|
||||
|
||||
let cert = Cert::from_file(TEST_KEY_PATH)?;
|
||||
let p = StandardPolicy::new();
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Signing)? {
|
||||
println!("Upload signing key");
|
||||
admin.upload_key(vka, KeyType::Signing, None)?;
|
||||
}
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Decryption)? {
|
||||
println!("Upload decryption key");
|
||||
admin.upload_key(vka, KeyType::Decryption, None)?;
|
||||
}
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Authentication)? {
|
||||
println!("Upload auth key");
|
||||
admin.upload_key(vka, KeyType::Authentication, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
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(b"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.admin_card().expect("just verified");
|
||||
|
||||
println!();
|
||||
|
||||
admin.set_name("Bar<<Foo")?;
|
||||
println!("set name - ok");
|
||||
|
||||
admin.set_sex(Sex::NotApplicable)?;
|
||||
println!("set sex - ok");
|
||||
|
||||
admin.set_lang(&[['e', 'n'].into()])?;
|
||||
println!("set lang - ok");
|
||||
|
||||
admin.set_url("https://keys.openpgp.org")?;
|
||||
println!("set url - ok");
|
||||
|
||||
let cert = Cert::from_file(TEST_KEY_PATH)?;
|
||||
let p = StandardPolicy::new();
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Signing)? {
|
||||
println!("Upload signing key");
|
||||
admin.upload_key(vka, KeyType::Signing, None)?;
|
||||
}
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Decryption)? {
|
||||
println!("Upload decryption key");
|
||||
admin.upload_key(vka, KeyType::Decryption, None)?;
|
||||
}
|
||||
|
||||
if let Some(vka) = sq_util::subkey_by_type(&cert, &p, KeyType::Authentication)? {
|
||||
println!("Upload auth key");
|
||||
admin.upload_key(vka, KeyType::Authentication, None)?;
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// -----------------------------
|
||||
// Open fresh Card for decrypt
|
||||
// -----------------------------
|
||||
let backend = PcscBackend::open_by_ident(&test_card_ident, None)?;
|
||||
let cards = PcscBackend::card_backends(None)?;
|
||||
let mut card = Card::<Open>::open_by_ident(cards, &test_card_ident)?;
|
||||
|
||||
let mut card: Card<Open> = 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>::open_by_ident(cards, &test_card_ident)?;
|
||||
|
||||
let mut card: Card<Open> = 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<dyn Error>> {
|
|||
println!("The following OpenPGP cards are connected to your system:");
|
||||
|
||||
for backend in PcscBackend::cards(None)? {
|
||||
let mut card: Card<Open> = backend.into();
|
||||
let mut card: Card<Open> = Card::<Open>::new(backend?)?;
|
||||
let open = card.transaction()?;
|
||||
|
||||
println!(" {}", open.application_identifier()?.ident());
|
||||
|
|
|
@ -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)?)
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -234,11 +234,11 @@ impl EccKey for SqEccKey {
|
|||
}
|
||||
|
||||
fn private(&self) -> Vec<u8> {
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
///
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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<PublicKey>,
|
||||
key_aut: Option<PublicKey>,
|
||||
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<Signature>| {
|
||||
// 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::<Signature, anyhow::Error>(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<HashAlgorithm>,
|
||||
sym: Option<SymmetricAlgorithm>,
|
||||
) -> Result<Fingerprint, Error> {
|
||||
// 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();
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
|
@ -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**
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Self, Self::Error> {
|
||||
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<AlgorithmAttributes, Error> {
|
||||
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<AlgoInfo>,
|
||||
) -> Result<Algo, crate::Error> {
|
||||
algorithm_attributes: AlgorithmAttributes,
|
||||
algo_info: Option<AlgorithmInformation>,
|
||||
) -> Result<AlgorithmAttributes, Error> {
|
||||
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<u8>),
|
||||
}
|
||||
|
||||
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<Vec<u8>, 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<Vec<u8>, Error> {
|
||||
fn rsa_algo_attrs(algo_attrs: &RsaAttributes) -> Result<Vec<u8>, 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<u8>,
|
||||
}
|
||||
|
||||
impl EccAttrs {
|
||||
impl EccAttributes {
|
||||
pub fn new(ecc_type: EccType, curve: Curve, import_format: Option<u8>) -> 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<u8>),
|
||||
}
|
||||
|
||||
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<Self, Self::Error> {
|
||||
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)
|
||||
|
|
|
@ -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<C>(
|
||||
card_tx: &mut C,
|
||||
cmd: Command,
|
||||
card_caps: Option<CardCaps>,
|
||||
expect_reply: bool,
|
||||
) -> Result<RawResponse, Error>
|
||||
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<C>(
|
||||
card_tx: &mut C,
|
||||
cmd: Command,
|
||||
card_caps: Option<CardCaps>,
|
||||
expect_response: Expect,
|
||||
) -> Result<Vec<u8>, 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)
|
||||
}
|
||||
|
|
|
@ -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<u8> {
|
||||
if !ext_len {
|
||||
assert!(
|
||||
len <= 0xff,
|
||||
"{}",
|
||||
"unexpected: len = {len:x?}, but ext says Short"
|
||||
);
|
||||
fn make_lc(len: u16, ext_len: bool) -> Result<Vec<u8>, 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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<bool> {
|
||||
// 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<Algo, Error> {
|
||||
pub fn algorithm_attributes(&self, key_type: KeyType) -> Result<AlgorithmAttributes, Error> {
|
||||
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<Option<UIF>, Error> {
|
||||
pub fn uif_pso_cds(&self) -> Result<Option<UserInteractionFlag>, Error> {
|
||||
let uif = self.0.find(Tags::UifSig);
|
||||
|
||||
match uif {
|
||||
|
@ -201,7 +204,7 @@ impl ApplicationRelatedData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn uif_pso_dec(&self) -> Result<Option<UIF>, Error> {
|
||||
pub fn uif_pso_dec(&self) -> Result<Option<UserInteractionFlag>, Error> {
|
||||
let uif = self.0.find(Tags::UifDec);
|
||||
|
||||
match uif {
|
||||
|
@ -210,7 +213,7 @@ impl ApplicationRelatedData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn uif_pso_aut(&self) -> Result<Option<UIF>, Error> {
|
||||
pub fn uif_pso_aut(&self) -> Result<Option<UserInteractionFlag>, 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<Option<Algo>, Error> {
|
||||
pub fn attestation_key_algorithm_attributes(
|
||||
&mut self,
|
||||
) -> Result<Option<AlgorithmAttributes>, 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<Option<KeyGenerationTime>, Error> {
|
||||
pub fn attestation_key_generation_time(&self) -> Result<Option<KeyGenerationTime>, Error> {
|
||||
match self.0.find(Tags::GenerationTimeAttestation) {
|
||||
None => Ok(None),
|
||||
Some(data) => {
|
||||
|
@ -260,7 +267,7 @@ impl ApplicationRelatedData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn uif_attestation(&self) -> Result<Option<UIF>, Error> {
|
||||
pub fn uif_attestation(&self) -> Result<Option<UserInteractionFlag>, 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<Vec<u8>> for UIF {
|
||||
impl TryFrom<Vec<u8>> for UserInteractionFlag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
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<u8> 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<u8> 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<u8>);
|
||||
|
||||
impl From<Vec<u8>> 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<Vec<u8>>,
|
||||
|
@ -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 <https://en.wikipedia.org/wiki/ISO/IEC_5218>
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
|
@ -863,7 +872,8 @@ impl From<u8> 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 <https://en.wikipedia.org/wiki/ISO_639-1>
|
||||
#[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<O>(result: nom::IResult<&[u8], O>) -> Result<O, Error> {
|
|||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A KeySet binds together a triple of information about each Key slot on a card
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct KeySet<T> {
|
||||
signature: Option<T>,
|
||||
decryption: Option<T>,
|
||||
authentication: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> From<(Option<T>, Option<T>, Option<T>)> for KeySet<T> {
|
||||
fn from(tuple: (Option<T>, Option<T>, Option<T>)) -> Self {
|
||||
Self {
|
||||
signature: tuple.0,
|
||||
decryption: tuple.1,
|
||||
authentication: tuple.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> KeySet<T> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u8>> {
|
||||
|
@ -99,7 +102,7 @@ fn default_import_format(input: &[u8]) -> nom::IResult<&[u8], Option<u8>> {
|
|||
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<Self, crate::Error> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Self, Self::Error> {
|
||||
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)))
|
||||
])
|
||||
);
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ impl ApplicationIdentifier {
|
|||
|
||||
/// Mapping of manufacturer id to a name, data from:
|
||||
/// <https://en.wikipedia.org/wiki/OpenPGP_card> [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.",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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]> {
|
||||
|
|
|
@ -9,19 +9,101 @@ use crate::card_do::ExtendedCapabilities;
|
|||
use crate::Error;
|
||||
|
||||
impl ExtendedCapabilities {
|
||||
pub fn max_len_special_do(&self) -> Option<u16> {
|
||||
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<u16> {
|
||||
/// 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<u16> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<u16> {
|
||||
self.max_cmd_len
|
||||
}
|
||||
|
||||
pub fn max_resp_len(&self) -> Option<u16> {
|
||||
/// 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<u16> {
|
||||
self.max_resp_len
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ impl From<KeyGenerationTime> for DateTime<Utc> {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<u8>) -> Command {
|
|||
}
|
||||
}
|
||||
|
||||
/// PUT DO Login Data
|
||||
pub(crate) fn put_login_data(login_data: Vec<u8>) -> Command {
|
||||
put_data(Tags::LoginData, login_data)
|
||||
}
|
||||
|
||||
/// PUT DO Name
|
||||
pub(crate) fn put_name(name: Vec<u8>) -> Command {
|
||||
put_data(Tags::Name, name)
|
|
@ -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<u8>,
|
||||
algo: Algo,
|
||||
algo: AlgorithmAttributes,
|
||||
}
|
||||
|
||||
impl EccPub {
|
||||
pub fn new(data: Vec<u8>, algo: Algo) -> Self {
|
||||
pub fn new(data: Vec<u8>, 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<StatusBytes> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SmartcardError> 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),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Fingerprint, Error>,
|
||||
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<PublicKeyMaterial, crate::Error> {
|
||||
fn tlv_to_pubkey(tlv: &Tlv, algo: &AlgorithmAttributes) -> Result<PublicKeyMaterial, crate::Error> {
|
||||
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<PublicKeyMaterial, crate::Err
|
|||
/// This runs the low level key generation primitive on the card.
|
||||
/// (This does not set algorithm attributes, creation time or fingerprint)
|
||||
pub(crate) fn generate_asymmetric_key_pair(
|
||||
card_tx: &mut OpenPgpTransaction,
|
||||
card_tx: &mut Transaction,
|
||||
key_type: KeyType,
|
||||
) -> Result<Tlv, Error> {
|
||||
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<PublicKeyMaterial, Error> {
|
||||
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<dyn CardUploadableKey>,
|
||||
key_type: KeyType,
|
||||
algo_info: Option<AlgoInfo>,
|
||||
) -> 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<dyn RSAKey>,
|
||||
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<dyn EccKey>,
|
||||
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<AlgoInfo>,
|
||||
) -> Result<RsaAttrs, crate::Error> {
|
||||
algo_attr: AlgorithmAttributes,
|
||||
algo_info: Option<AlgorithmInformation>,
|
||||
) -> Result<RsaAttributes, Error> {
|
||||
// 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<AlgoInfo>,
|
||||
) -> Result<EccAttrs, crate::Error> {
|
||||
algo_info: Option<AlgorithmInformation>,
|
||||
) -> Result<EccAttributes, crate::Error> {
|
||||
// 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<RsaAttrs, Error> {
|
||||
fn card_algo_rsa(
|
||||
algo_info: AlgorithmInformation,
|
||||
key_type: KeyType,
|
||||
rsa_bits: u16,
|
||||
) -> Result<RsaAttributes, Error> {
|
||||
// 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<EccAttrs> {
|
||||
fn check_card_algo_ecc(
|
||||
algo_info: AlgorithmInformation,
|
||||
key_type: KeyType,
|
||||
oid: &[u8],
|
||||
) -> Vec<EccAttributes> {
|
||||
// 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<dyn RSAKey>,
|
||||
rsa_attrs: &RsaAttrs,
|
||||
rsa_attrs: &RsaAttributes,
|
||||
) -> Result<Command, Error> {
|
||||
// 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<dyn EccKey>,
|
||||
ecc_attrs: &EccAttrs,
|
||||
ecc_attrs: &EccAttributes,
|
||||
) -> Result<Command, Error> {
|
||||
let private = ecc_key.private();
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
259
openpgp-card/src/tags.rs
Normal file
259
openpgp-card/src/tags.rs
Normal file
|
@ -0,0 +1,259 @@
|
|||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Tags> for Vec<u8> {
|
||||
fn from(t: Tags) -> Self {
|
||||
ShortTag::from(t).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tags> for Tag {
|
||||
fn from(t: Tags) -> Self {
|
||||
ShortTag::from(t).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tags> 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<ShortTag> 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<ShortTag> for Vec<u8> {
|
||||
fn from(t: ShortTag) -> Self {
|
||||
match t {
|
||||
ShortTag::One(t0) => vec![t0],
|
||||
ShortTag::Two(t0, t1) => vec![t0, t1],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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() {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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"
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# 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.
|
||||
|
||||
|
|
437
pcsc/src/lib.rs
437
pcsc/src/lib.rs
|
@ -1,39 +1,37 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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>) -> 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<CardCaps>,
|
||||
reader_caps: HashMap<u8, Tlv>,
|
||||
|
||||
// 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<PcscBackend> for Box<dyn CardBackend + Sync + Send> {
|
|||
/// <https://docs.microsoft.com/en-us/windows/win32/api/winscard/nf-winscard-scardbegintransaction?redirectedfrom=MSDN#remarks>)
|
||||
pub struct PcscTransaction<'b> {
|
||||
tx: pcsc::Transaction<'b>,
|
||||
card_caps: Option<CardCaps>, // FIXME: manual copy from PcscCard
|
||||
reader_caps: HashMap<u8, Tlv>, // FIXME: manual copy from PcscCard
|
||||
reader_caps: HashMap<u8, Tlv>, // 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<Self, Error> {
|
||||
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<Self, SmartcardError> {
|
||||
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 <dyn CardTransaction>::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<ApplicationRelatedData, Error> {
|
||||
<dyn CardTransaction>::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<Vec<Tlv>, Error> {
|
||||
fn features(&mut self) -> Result<Vec<Tlv>, 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<u8, Error> {
|
||||
if let Some(card_caps) = self.card_caps {
|
||||
fn max_pin_len(
|
||||
&self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<u8, SmartcardError> {
|
||||
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<Vec<u8>, Error> {
|
||||
fn transmit(&mut self, cmd: &[u8], buf_size: usize) -> Result<Vec<u8>, 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<Vec<u8>, Error> {
|
||||
fn pinpad_verify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, 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<Vec<u8>, Error> {
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
pin: PinType,
|
||||
card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, 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<Vec<pcsc::Card>, SmartcardError> {
|
||||
/// A list of "raw" opened PCSC Cards and reader names
|
||||
/// (No application is selected)
|
||||
fn raw_pcsc_cards(mode: pcsc::ShareMode) -> Result<Vec<(pcsc::Card, String)>, 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<Vec<Self>, Error> {
|
||||
let mut cards: Vec<Self> = 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<pcsc::ShareMode>) -> Result<Vec<Self>, 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<pcsc::ShareMode>,
|
||||
) -> Result<impl Iterator<Item = Result<Self, SmartcardError>>, 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<pcsc::ShareMode>) -> Result<Self, Error> {
|
||||
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<pcsc::ShareMode>,
|
||||
) -> Result<
|
||||
impl Iterator<Item = Result<Box<dyn CardBackend + Send + Sync>, 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<dyn CardBackend + Send + Sync>),
|
||||
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<Self, Error> {
|
||||
/// 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<Self, SmartcardError> {
|
||||
log::trace!("pcsc initialize_card");
|
||||
|
||||
let mut h: HashMap<u8, Tlv> = 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)
|
||||
<dyn CardTransaction>::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<CardCaps> {
|
||||
self.card_caps
|
||||
}
|
||||
fn reader_caps(&self) -> HashMap<u8, Tlv> {
|
||||
self.reader_caps.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl CardBackend for PcscBackend {
|
||||
/// Get a TxClient for this PcscCard (this starts a transaction)
|
||||
fn transaction(&mut self) -> Result<Box<dyn CardTransaction + Send + Sync + '_>, 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<Box<dyn CardTransaction + Send + Sync + '_>, SmartcardError> {
|
||||
Ok(Box::new(PcscTransaction::new(self, reselect_application)?))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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"] }
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
**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)
|
||||
|
|
233
scdc/src/lib.rs
233
scdc/src/lib.rs
|
@ -1,17 +1,17 @@
|
|||
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<CardCaps>,
|
||||
}
|
||||
|
||||
/// Boxing helper (for easier consumption of PcscBackend in openpgp_card and openpgp_card_sequoia)
|
||||
impl From<ScdBackend> for Box<dyn CardBackend + Sync + Send> {
|
||||
fn from(backend: ScdBackend) -> Box<dyn CardBackend + Sync + Send> {
|
||||
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<Agent>, serial: &str) -> Result<Self, Error> {
|
||||
/// 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<Agent>, serial: &str) -> Result<Self, SmartcardError> {
|
||||
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<Agent>) -> Result<Self, Error> {
|
||||
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<Agent>) -> Result<Self, SmartcardError> {
|
||||
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<Agent>) -> Result<(), Error> {
|
||||
pub fn shutdown_scd(agent: Option<Agent>) -> 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<Agent>, init: bool) -> Result<Self, Error> {
|
||||
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<Agent>, init: bool) -> Result<Self, SmartcardError> {
|
||||
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<Box<dyn CardTransaction + Send + Sync + '_>, 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<Box<dyn CardTransaction + Send + Sync + '_>, 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<Vec<u8>, Error> {
|
||||
fn transmit(&mut self, cmd: &[u8], _: usize) -> Result<Vec<u8>, 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<Vec<u8>, Error> {
|
||||
/// Not implemented yet
|
||||
fn pinpad_verify(
|
||||
&mut self,
|
||||
_id: PinType,
|
||||
_card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// FIXME: not implemented yet
|
||||
fn pinpad_modify(&mut self, _id: PinType) -> Result<Vec<u8>, Error> {
|
||||
/// Not implemented yet
|
||||
fn pinpad_modify(
|
||||
&mut self,
|
||||
_id: PinType,
|
||||
_card_caps: &Option<CardCaps>,
|
||||
) -> Result<Vec<u8>, SmartcardError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Not implemented here
|
||||
fn was_reset(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
# SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
# 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 <heiko@schaefer.name>"]
|
||||
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"]
|
736
tools/README.md
736
tools/README.md
|
@ -1,736 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.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 <alice@example.org>"
|
||||
```
|
||||
|
||||
|
||||
#### 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 <admin-pin-file>`,
|
||||
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/<username>.gpg`) or github (`https://github.com/<username>.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 <admin-pin-file> 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 <output-cert-file> 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 `<output-cert-file>` 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 <alice@example.org>" --output <output-cert-file> 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 <input-file>
|
||||
```
|
||||
|
||||
### Decrypting
|
||||
|
||||
Decryption using a card (if no input file is set, stdin is read):
|
||||
|
||||
```
|
||||
$ opgpcard decrypt --card ABCD:01234567 <input-file>
|
||||
```
|
||||
|
||||
### 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 <admin-pin-file> name "Alice Adams"`
|
||||
|
||||
- Importing a key to the card:
|
||||
|
||||
`$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> import key.priv`
|
||||
|
||||
- Generating key material on the card:
|
||||
|
||||
`$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> generate -p <user-pin-file> --output <output-cert-file> cv25519`
|
||||
|
||||
- Creating a detached signature:
|
||||
|
||||
`$ opgpcard sign --detached --card ABCD:01234567 -p <user-pin-file> <input-file>`
|
||||
|
||||
**Examples of non-interactive PIN management**
|
||||
|
||||
- Setting a new User PIN:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-user -p <old-user-pin-file> -q <new-user-pin-file>`
|
||||
|
||||
- Setting a new Admin PIN:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-admin -P <old-admin-pin-file> -Q <new-admin-pin-file>`
|
||||
|
||||
- Setting a new User PIN based on the Admin PIN (and unblocking the card, if needed):
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 reset-user -P <admin-pin-file> -p <new-user-pin-file>`
|
||||
|
||||
- Setting the resetting code:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-reset -P <admin-pin-file> -r <resetting-code-file>`
|
||||
|
||||
- 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 <resetting-code-file> -p <new-user-pin-file>`
|
||||
|
||||
#### 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 <output-cert-file> 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`.
|
|
@ -1,31 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fir>
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <liw@liw.fi>
|
||||
# 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'
|
|
@ -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
|
|
@ -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 <liw@liw.fi> Thu, 30 Sep 2021 09:51:32 +0300
|
|
@ -1,2 +0,0 @@
|
|||
10
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Source: openpgp-card-tool
|
||||
Maintainer: Heiko Schaefer <heiko@schaefer.name>
|
||||
Uploaders: Lars Wirzenius <liw@liw.fi>
|
||||
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.
|
|
@ -1,3 +0,0 @@
|
|||
Copyright 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
@ -1 +0,0 @@
|
|||
README.md
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
3.0 (quilt)
|
|
@ -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 <liw@liw.fi>
|
||||
# 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)
|
|
@ -1,61 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.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.
|
111
tools/src/cli.rs
111
tools/src/cli.rs
|
@ -1,111 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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 <heiko@schaefer.name>",
|
||||
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 {},
|
||||
}
|
|
@ -1,568 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<PathBuf>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
/// Optionally, select the subkey to import in the DEC slot
|
||||
#[clap(name = "DEC subkey fingerprint", short = 'd', long = "dec-fp")]
|
||||
dec_fp: Option<String>,
|
||||
|
||||
/// Optionally, select the subkey to import in the AUT slot
|
||||
#[clap(name = "AUT subkey fingerprint", short = 'a', long = "aut-fp")]
|
||||
aut_fp: Option<String>,
|
||||
},
|
||||
|
||||
/// 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<Algo>,
|
||||
|
||||
/// User ID to add to the exported certificate representation
|
||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
||||
user_ids: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
user_pin: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
#[clap(rename_all = "UPPER")]
|
||||
pub enum BasePlusAttKeySlot {
|
||||
Sig,
|
||||
Dec,
|
||||
Aut,
|
||||
Att,
|
||||
}
|
||||
|
||||
impl From<BasePlusAttKeySlot> 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<TouchPolicy> 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<Algo> 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<dyn std::error::Error>> {
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = 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<ValidErasedKeyAmalgamation<'a, SecretParts>>; 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<String>,
|
||||
dec_fp: Option<String>,
|
||||
aut_fp: Option<String>,
|
||||
) -> Result<[Option<ValidErasedKeyAmalgamation<'a, SecretParts>>; 3]> {
|
||||
let key_by_fp = |fp: Option<String>| 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<Admin>,
|
||||
decrypt: bool,
|
||||
auth: bool,
|
||||
algo: Option<AlgoSimple>,
|
||||
) -> Result<(PublicKey, Option<PublicKey>, Option<PublicKey>)> {
|
||||
// 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<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
admin.set_name(name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url_command(
|
||||
url: &str,
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
admin.set_url(url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_command(
|
||||
keyfile: PathBuf,
|
||||
sig_fp: Option<String>,
|
||||
dec_fp: Option<String>,
|
||||
aut_fp: Option<String>,
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<String> = vec![];
|
||||
|
||||
// helper: true, if `pw` decrypts `key`
|
||||
let pw_ok = |key: &Key<SecretParts, UnspecifiedRole>, 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<SecretParts, UnspecifiedRole>, 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<ValidErasedKeyAmalgamation<SecretParts>>,
|
||||
key_type: &str|
|
||||
-> Result<Option<String>> {
|
||||
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<Transaction>,
|
||||
|
||||
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<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
key: BasePlusAttKeySlot,
|
||||
policy: TouchPolicy,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<String>,
|
||||
},
|
||||
|
||||
/// 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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// 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<BaseKeySlot> 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<dyn std::error::Error>> {
|
||||
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<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut output = output::AttestationCert::default();
|
||||
|
||||
let backend = pick_card_for_reading(ident)?;
|
||||
let mut open: Card<Open> = 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<PathBuf>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = 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<String>, key: BaseKeySlot) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = pick_card_for_reading(ident)?;
|
||||
let mut open: Card<Open> = 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(())
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<PathBuf>,
|
||||
|
||||
/// Input file (stdin if unset)
|
||||
#[clap(name = "input")]
|
||||
input: Option<PathBuf>,
|
||||
|
||||
/// Output file (stdout if unset)
|
||||
#[clap(name = "output", long = "output", short = 'o')]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn decrypt(command: DecryptCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<Open> = 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(())
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<Open> = backend.into();
|
||||
|
||||
let mut card = open.transaction()?;
|
||||
card.factory_reset().map_err(|e| anyhow!(e))
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<Open> = 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::<Vec<_>>().join(".");
|
||||
output.firmware_version(ver);
|
||||
}
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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;
|
|
@ -1,364 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<PathBuf>,
|
||||
|
||||
#[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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<PathBuf>,
|
||||
|
||||
#[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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<PathBuf>,
|
||||
|
||||
#[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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<PathBuf>,
|
||||
|
||||
#[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<PathBuf>,
|
||||
},
|
||||
|
||||
/// 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<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "Resetting Code file",
|
||||
short = 'r',
|
||||
long = "reset-code",
|
||||
help = "Optionally, get the Resetting Code from a file"
|
||||
)]
|
||||
reset_code: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn pin(ident: &str, cmd: PinSubCommand) -> Result<()> {
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = 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<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> 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<PathBuf>,
|
||||
admin_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> 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<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> 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<PathBuf>,
|
||||
reset_code: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> 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<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<String>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
user_pin: Option<PathBuf>,
|
||||
|
||||
/// User ID to add to the exported certificate representation
|
||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
||||
user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
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<Open> = 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(())
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<SetIdentityId> 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<dyn std::error::Error>> {
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
card.set_identity(u8::from(command.id))?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<PathBuf>,
|
||||
|
||||
#[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<PathBuf>,
|
||||
|
||||
/// Output file (stdout if unset)
|
||||
#[clap(name = "output", long = "output", short = 'o')]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn sign(command: SignCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<PathBuf>,
|
||||
input: Option<&Path>,
|
||||
output: Option<&Path>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut input = util::open_or_stdin(input)?;
|
||||
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = 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(())
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<String>,
|
||||
}
|
||||
|
||||
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<Open> = 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(())
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<String>,
|
||||
|
||||
#[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<Open> = 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(())
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// 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<dyn std::error::Error>> {
|
||||
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<Open> = 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<String>) -> Result<Box<dyn CardBackend + Send + Sync>> {
|
||||
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 <card ident>'\n");
|
||||
|
||||
Err(anyhow::anyhow!("Found more than one card"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cert(
|
||||
card: &mut Card<Transaction>,
|
||||
key_sig: PublicKey,
|
||||
key_dec: Option<PublicKey>,
|
||||
key_aut: Option<PublicKey>,
|
||||
user_pin: Option<&[u8]>,
|
||||
user_ids: &[String],
|
||||
prompt: &dyn Fn(),
|
||||
) -> Result<Cert> {
|
||||
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,
|
||||
)
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String, OpgpCardError> {
|
||||
Ok(format!(
|
||||
"OpenPGP card {}\n\n{}\n",
|
||||
self.ident, self.attestation_cert,
|
||||
))
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<AttestationCertV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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);
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String, OpgpCardError> {
|
||||
// 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<AdminGenerateV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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);
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String>,
|
||||
card_service_data: Vec<String>,
|
||||
extended_length_info: Vec<String>,
|
||||
extended_capabilities: Vec<String>,
|
||||
algorithms: Option<Vec<String>>,
|
||||
firmware_version: Option<String>,
|
||||
}
|
||||
|
||||
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<String, OpgpCardError> {
|
||||
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<InfoV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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<String>,
|
||||
card_service_data: Vec<String>,
|
||||
extended_length_info: Vec<String>,
|
||||
extended_capabilities: Vec<String>,
|
||||
algorithms: Option<Vec<String>>,
|
||||
firmware_version: Option<String>,
|
||||
}
|
||||
|
||||
impl OutputVariant for InfoV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn push(&mut self, idnet: String) {
|
||||
self.idents.push(idnet);
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
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<ListV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl OutputVariant for ListV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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;
|
|
@ -1,75 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String, OpgpCardError> {
|
||||
Ok(format!(
|
||||
"OpenPGP card {}\n\n{}\n",
|
||||
self.ident, self.public_key
|
||||
))
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<PublicKeyV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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);
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String>,
|
||||
ssh_public_key: Option<String>,
|
||||
}
|
||||
|
||||
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<String, OpgpCardError> {
|
||||
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<SshV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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<String>,
|
||||
ssh_public_key: Option<String>,
|
||||
}
|
||||
|
||||
impl OutputVariant for SshV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,340 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<String>,
|
||||
language_preferences: Vec<String>,
|
||||
certificate_url: Option<String>,
|
||||
signature_key: KeySlotInfo,
|
||||
signature_count: u32,
|
||||
user_pin_valid_for_only_one_signature: bool,
|
||||
decryption_key: KeySlotInfo,
|
||||
authentication_key: KeySlotInfo,
|
||||
attestation_key: Option<KeySlotInfo>,
|
||||
user_pin_remaining_attempts: u8,
|
||||
admin_pin_remaining_attempts: u8,
|
||||
reset_code_remaining_attempts: u8,
|
||||
additional_key_statuses: Vec<(u8, String)>,
|
||||
ca_fingerprints: Vec<String>,
|
||||
}
|
||||
|
||||
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<String, OpgpCardError> {
|
||||
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<StatusV0, OpgpCardError> {
|
||||
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<String, Self::Err> {
|
||||
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<String>,
|
||||
language_preferences: Vec<String>,
|
||||
certificate_url: Option<String>,
|
||||
signature_key: KeySlotInfo,
|
||||
signature_count: u32,
|
||||
user_pin_valid_for_only_one_signature: bool,
|
||||
decryption_key: KeySlotInfo,
|
||||
authentication_key: KeySlotInfo,
|
||||
attestation_key: Option<KeySlotInfo>,
|
||||
user_pin_remaining_attempts: u8,
|
||||
admin_pin_remaining_attempts: u8,
|
||||
reset_code_remaining_attempts: u8,
|
||||
additional_key_statuses: Vec<(u8, String)>,
|
||||
// ca_fingerprints: Vec<String>, // 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<String>,
|
||||
creation_time: Option<String>,
|
||||
algorithm: Option<String>,
|
||||
touch_policy: Option<String>,
|
||||
touch_features: Option<String>,
|
||||
status: Option<String>,
|
||||
public_key_material: Option<String>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// 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<Vec<Box<dyn CardBackend + Send + Sync>>, Error> {
|
||||
PcscBackend::cards(None).map(|cards| cards.into_iter().map(|c| c.into()).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn open_card(ident: &str) -> Result<Box<dyn CardBackend + Send + Sync>, 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<Transaction<'_>>,
|
||||
pin_file: Option<PathBuf>,
|
||||
msg: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
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<Vec<u8>> {
|
||||
// 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<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<User<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
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<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<Sign<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
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<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<Admin<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
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<Vec<u8>> {
|
||||
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<Box<dyn std::io::Read + Send + Sync>> {
|
||||
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<Box<dyn std::io::Write + Send + Sync>> {
|
||||
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<sshkeys::PublicKey> {
|
||||
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<String> {
|
||||
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<Transaction>) -> 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<u8>) -> String {
|
||||
const PEM_TAG: &str = "CERTIFICATE";
|
||||
|
||||
let pem = pem::Pem {
|
||||
tag: String::from(PEM_TAG),
|
||||
contents: data,
|
||||
};
|
||||
|
||||
pem::encode(&pem)
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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<Self, Self::Err> {
|
||||
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OutputBuilder {
|
||||
type Err;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err>;
|
||||
}
|
||||
|
||||
pub trait OutputVariant: Serialize {
|
||||
const VERSION: OutputVersion;
|
||||
fn json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string_pretty(self)
|
||||
}
|
||||
fn yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.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
|
||||
<https://gitlab.com/openpgp-card/virtual-cards>. 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-----
|
||||
~~~
|
|
@ -1,99 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# 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
|
|
@ -1,12 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# 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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue