Merge branch 'main' of gitlab.com:openpgp-card/openpgp-card

This commit is contained in:
Seán C McCord 2024-02-05 14:10:34 -05:00
commit 10484aeeb4
Signed by: scm
GPG key ID: FC678714ACA347CB
102 changed files with 3989 additions and 8003 deletions

View file

@ -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

View file

@ -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

View file

@ -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",
]

View file

@ -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
View 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
View 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
View 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),
}

View file

@ -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"

View 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",
]

View file

@ -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",

View file

@ -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)?)
}
}
}
}

View file

@ -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?}");
}

View file

@ -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");

View file

@ -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());
}

View file

@ -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, &[])?;
// ---

View file

@ -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)
}

View file

@ -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())?;

View file

@ -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"

View file

@ -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"

View file

@ -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();

View file

@ -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();

View file

@ -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]

View file

@ -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**

View file

@ -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());

View file

@ -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

View file

@ -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(),
}
}

View file

@ -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.

View file

@ -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).
///

View file

@ -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};

View file

@ -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();

View file

@ -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"

View file

@ -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**

View file

@ -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)

View file

@ -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)
}

View file

@ -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])
}
}

View file

@ -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()
}
}

View file

@ -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> {

View file

@ -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)))
])
);
}

View file

@ -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.",

View file

@ -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]> {

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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),
}

View file

@ -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
View 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],
}
}
}

View file

@ -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() {

View file

@ -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"

View file

@ -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.

View file

@ -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)?))
}
}

View file

@ -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"] }

View file

@ -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)

View file

@ -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
}
}

View file

@ -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"]

View file

@ -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`.

View file

@ -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();
}
}
}
}

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -1,2 +0,0 @@
10

View file

@ -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.

View file

@ -1,3 +0,0 @@
Copyright 2021-2022 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: MIT OR Apache-2.0

View file

@ -1 +0,0 @@
README.md

View file

@ -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

View file

@ -1 +0,0 @@
3.0 (quilt)

View file

@ -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)

View file

@ -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.

View file

@ -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 {},
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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))
}

View file

@ -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(())
}

View file

@ -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;

View file

@ -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(())
}
}
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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,
)
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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-----
~~~

View file

@ -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;
}
}
}
}

View file

@ -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

View file

@ -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