This commit is contained in:
Heiko Schaefer 2023-03-17 13:55:42 +01:00
parent 971a19b4f2
commit 7a28d36e93
No known key found for this signature in database
GPG key ID: 4A849A1904CCBD7D
48 changed files with 9 additions and 5003 deletions

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-FileCopyrightText: 2021-2022 Nora Widdecke <mail@nora.pink>
# SPDX-License-Identifier: CC0-1.0
@ -131,33 +131,6 @@ cargo-test-debian-bookworm:
# 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
image: registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps
@ -165,8 +138,8 @@ run_cardtest_smartpgp:
- *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:
@ -185,8 +158,8 @@ run_cardtest_opcard_rs:
- *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:
@ -205,8 +178,8 @@ run_cardtest_ykneo:
- *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:
@ -225,8 +198,8 @@ run_cardtest_fluffypgp:
- *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:

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

@ -8,6 +8,5 @@ members = [
"pcsc",
"scdc",
"openpgp-card-examples",
"tools",
"card-functionality",
]

View file

@ -1,51 +0,0 @@
# SPDX-FileCopyrightText: 2021-2023 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.2"
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,64 +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>,
#[clap(long, help = "Only print the ssh public key")]
pub key_only: bool,
}
pub fn print_ssh(
format: OutputFormat,
output_version: OutputVersion,
command: SshCommand,
) -> Result<()> {
let mut output = output::Ssh::default();
output.key_only(command.key_only);
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,97 +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 {
key_only: bool, // only print ssh public key, in text mode
ident: String,
authentication_key_fingerprint: Option<String>,
ssh_public_key: Option<String>,
}
impl Ssh {
pub fn key_only(&mut self, ssh_key_only: bool) {
self.key_only = ssh_key_only;
}
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> {
if !self.key_only {
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)
} else {
Ok(self.ssh_public_key.clone().unwrap_or("".to_string()))
}
}
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

View file

@ -1,29 +0,0 @@
#!/usr/bin/env bash
#
# Run "cargo test" inside a Docker container with virtual cards
# installed and running. This will allow at least rudimentary testing
# of actual card functionality of opgpcard.
#
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
# SPDX-License-Identifier: MIT OR Apache-2.0
set -euo pipefail
image="registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps"
src="$(cd .. && pwd)"
if ! [ -e Cargo.toml ] || ! grep '^name.*openpgp-card-tools' Cargo.toml; then
echo "MUST run this in the root of the openpgp-card-tool crate" 1>&2
exit 1
fi
cargo build
docker run --rm -t \
--volume "cargo:/cargo" \
--volume "dotcargo:/root/.cargo" \
--volume "$src:/opt" "$image" \
bash -c '
/etc/init.d/pcscd start && \
su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim && \
cd /opt/tools && env CARGO_TARGET_DIR=/cargo cargo test'

View file

@ -1,6 +0,0 @@
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
// SPDX-License-Identifier: MIT OR Apache-2.0
#![allow(clippy::needless_return)]
include!(concat!(env!("OUT_DIR"), "/opgpcard.rs"));