openpgp-card-tools moved to https://codeberg.org/openpgp-card/openpgp-card-tools
This commit is contained in:
parent
971a19b4f2
commit
7a28d36e93
48 changed files with 9 additions and 5003 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,6 +8,5 @@ members = [
|
|||
"pcsc",
|
||||
"scdc",
|
||||
"openpgp-card-examples",
|
||||
"tools",
|
||||
"card-functionality",
|
||||
]
|
||||
|
|
|
@ -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"]
|
736
tools/README.md
736
tools/README.md
|
@ -1,736 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# OpenPGP card tools
|
||||
|
||||
This crate contains the `opgpcard` tool for inspecting, configuring and using OpenPGP
|
||||
cards.
|
||||
|
||||
# Install
|
||||
|
||||
One easy way to install this crate is via the "cargo" tool.
|
||||
|
||||
The following build dependencies are needed for current Debian:
|
||||
|
||||
```
|
||||
# apt install rustc cargo clang pkg-config nettle-dev libpcsclite-dev
|
||||
```
|
||||
|
||||
And for current Fedora:
|
||||
|
||||
```
|
||||
# dnf install rustc cargo clang nettle-devel pcsc-lite-devel
|
||||
```
|
||||
|
||||
Afterwards, you can install this crate by running:
|
||||
|
||||
```
|
||||
$ cargo install openpgp-card-tools
|
||||
```
|
||||
|
||||
Finally, add `$HOME/.cargo/bin` to your PATH to be able to run the installed
|
||||
binaries.
|
||||
|
||||
`opgpcard` uses the PC/SC framework. So on Linux-based systems, you need to make sure the `pcscd`
|
||||
service is running, to be able to access your OpenPGP cards.
|
||||
|
||||
## opgpcard
|
||||
|
||||
A tool to inspect, configure and use OpenPGP cards.
|
||||
|
||||
This tool is designed to be equally convenient for regular interactive use, as well as from scripts.
|
||||
To this end, all functionality of this tool is alternatively usable in a non-interactive manner (see below).
|
||||
|
||||
When using the tool in interactive contexts, two methods of PIN entry are supported:
|
||||
in most cases, PINs can (and must) be entered via the host computer.
|
||||
When a pin pad is available on the smartcard reader, PIN entry will be requested via this pin pad.
|
||||
|
||||
### List cards
|
||||
|
||||
List idents of all currently connected cards:
|
||||
|
||||
```
|
||||
$ opgpcard list
|
||||
Available OpenPGP cards:
|
||||
ABCD:01234567
|
||||
0007:87654321
|
||||
```
|
||||
|
||||
### Inspect card status
|
||||
|
||||
Print status information about the data on a card.
|
||||
The card is implicitly selected (if exactly one card is connected):
|
||||
|
||||
```
|
||||
$ opgpcard status
|
||||
OpenPGP card ABCD:01234567 (card version 3.4)
|
||||
|
||||
Cardholder: Alice Adams
|
||||
Language preferences: 'en'
|
||||
|
||||
Signature key
|
||||
Fingerprint: 034B 348C EDA2 064C AA22 74E4 7563 E86F 5CAB C2A4
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Ed25519 (EdDSA)
|
||||
Signatures made: 11
|
||||
|
||||
Decryption key
|
||||
Fingerprint: 338B EE09 3950 D831 A76F 0EB9 13D6 2DF6 8C9E 5176
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Cv25519 (ECDH)
|
||||
|
||||
Authentication key
|
||||
Fingerprint: 4881 A22E 7EC6 26D1 1202 50B0 A7D7 F0D5 0C8D F719
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Ed25519 (EdDSA)
|
||||
|
||||
Remaining PIN attempts: User: 3, Admin: 3, Reset Code: 0
|
||||
```
|
||||
|
||||
Explicitly print the status information for a specific card (this command syntax is needed, when more than one card
|
||||
is plugged in):
|
||||
|
||||
```
|
||||
$ opgpcard status --card ABCD:01234567
|
||||
```
|
||||
|
||||
Add `-v` for more verbose card status:
|
||||
|
||||
```
|
||||
OpenPGP card ABCD:01234567 (card version 3.4)
|
||||
|
||||
Cardholder: Alice Adams
|
||||
Language preferences: 'en'
|
||||
|
||||
Signature key
|
||||
Fingerprint: 034B 348C EDA2 064C AA22 74E4 7563 E86F 5CAB C2A4
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Ed25519 (EdDSA)
|
||||
Touch policy: Cached (features: Button)
|
||||
Key Status: generated
|
||||
User PIN presentation is valid for unlimited signatures
|
||||
Signatures made: 11
|
||||
|
||||
Decryption key
|
||||
Fingerprint: 338B EE09 3950 D831 A76F 0EB9 13D6 2DF6 8C9E 5176
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Cv25519 (ECDH)
|
||||
Touch policy: Off (features: Button)
|
||||
Key Status: generated
|
||||
|
||||
Authentication key
|
||||
Fingerprint: 4881 A22E 7EC6 26D1 1202 50B0 A7D7 F0D5 0C8D F719
|
||||
Creation Time: 2022-05-21 13:15:19 UTC
|
||||
Algorithm: Ed25519 (EdDSA)
|
||||
Touch policy: Off (features: Button)
|
||||
Key Status: generated
|
||||
|
||||
Attestation key:
|
||||
Algorithm: RSA 2048 [e 17]
|
||||
Touch policy: Cached (features: Button)
|
||||
|
||||
Remaining PIN attempts: User: 3, Admin: 3, Reset Code: 0
|
||||
Key Status (#129): imported
|
||||
```
|
||||
|
||||
The `--public-key-material` flag additionally outputs the raw public key data for each key slot.
|
||||
|
||||
### Get an OpenPGP public key representation from a card
|
||||
|
||||
This command returns an OpenPGP public key representation of the keys on a card.
|
||||
|
||||
To bind the decryption and authentication subkeys (if any) to the signing key, the user pin needs to be provided.
|
||||
|
||||
```
|
||||
$ opgpcard pubkey
|
||||
OpenPGP card ABCD:01234567
|
||||
Enter User PIN:
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: F9C7 97CB 1AF2 1C68 AEEC 8D4D 1002 89F5 5EF6 B2D4
|
||||
Comment: Alice Adams
|
||||
|
||||
xjMEYkOmahYJKwYBBAHaRw8BAQdADwHIuuSgboyzgcLci8Hc0Q15YHKfDP8/CZG4
|
||||
uumYosXNA2JhesLABgQTFgoAeAWCYkjTagWJAAAAAAkQEAKJ9V72stRHFAAAAAAA
|
||||
HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnifpLw5yhNlKffk7V+P9g
|
||||
idnIM3j6l3k34+p7tMQmCPoCmwMWIQT5x5fLGvIcaK7sjU0QAon1Xvay1AAAhJkB
|
||||
AIEhZTDuc9xARVK8ta51SOpX3mZs/UYA5a+UrB6vpmZ3AP4k14gFQ6q/cl/SOhPR
|
||||
FpCAvYlqL8rb3gc2sFIZDfYUDM4zBGJDpmoWCSsGAQQB2kcPAQEHQDRodITykZoi
|
||||
hIIPZcFZ2bMXvo20YEv+I1eg2kFQ2qSqwsAGBBgWCgB4BYJiSNNqBYkAAAAACRAQ
|
||||
Aon1Xvay1EcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcI
|
||||
5rVHhWA5cGdYlyQJYRXv4osAyFlyznFiUOATnoT6LgKbIBYhBPnHl8sa8hxoruyN
|
||||
TRACifVe9rLUAADpTwD/a+AlBGryfLsqFzIhdJRpGkoOl0H+xcgk3vcaPUQq0pcA
|
||||
/3TtUmaJ5w60qb/Px7/Q+MTymHH54elRY4lvwIfbvkUIzjgEYkOmahIKKwYBBAGX
|
||||
VQEFAQEHQO5KBZ7cMwwjsXGOWWMqgAkCyNdw7smcx/+jBEk0m38dAwEKCcLABgQY
|
||||
FgoAeAWCYkjTagWJAAAAAAkQEAKJ9V72stRHFAAAAAAAHgAgc2FsdEBub3RhdGlv
|
||||
bnMuc2VxdW9pYS1wZ3Aub3Jn9IwQkbcw9W0jfrduv1q4qNhsOgJWkGTMbVyvQCug
|
||||
YpcCmwwWIQT5x5fLGvIcaK7sjU0QAon1Xvay1AAAfTwBAPSQq/hGcGjAWNePHoLH
|
||||
5zA/ePu1vaY1nh2dPhqtUg8+AP0TDG96MJxlM8SJUQXtQsJCAEo4qT9GnGi7MyTU
|
||||
nvraDw==
|
||||
=es4l
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
|
||||
You can query a specific card
|
||||
|
||||
```
|
||||
$ opgpcard pubkey --card ABCD:01234567
|
||||
```
|
||||
|
||||
In the process of exporting the key material on a card as a certificate (public key), one or more User IDs can be
|
||||
bound to the certificate:
|
||||
|
||||
```
|
||||
$ opgpcard pubkey --userid "Alice Adams <alice@example.org>"
|
||||
```
|
||||
|
||||
|
||||
#### Caution: the exported public key material isn't always what you want
|
||||
|
||||
The result of exporting public key material from a card is only an approximation of the original public key, since
|
||||
some metadata is not available on OpenPGP cards. This missing metadata includes expiration dates.
|
||||
|
||||
Also, if your card only contains subkeys, but not the original primary key, then the exported certificate will use the
|
||||
signing subkey from the card as the primary key for the exported certificate.
|
||||
|
||||
One way to safely process this exported public key material from a card is via `sq key adopt`.
|
||||
|
||||
You can use this approach when you have access to your private primary key material (in the following example, we
|
||||
assume this key is available in `key.pgp`). Then you can bind the public key material from a card to your key:
|
||||
|
||||
```
|
||||
opgpcard pubkey > public.key
|
||||
sq key adopt key.pgp public.pgp
|
||||
```
|
||||
|
||||
In that process, you will be able to manually set any relevant flags.
|
||||
|
||||
|
||||
### Using a card for ssh auth
|
||||
|
||||
To use an OpenPGP card for ssh login authentication, a PGP authentication key needs to exist on the card.
|
||||
|
||||
`opgpcard ssh` then shows the ssh public key string representation of the PGP authentication
|
||||
key on the card, like this:
|
||||
|
||||
```
|
||||
$ opgpcard ssh
|
||||
OpenPGP card ABCD:01234567
|
||||
|
||||
Authentication key fingerprint:
|
||||
59A5CD3EA88F8707D887EAAE13545F404E11BE1C
|
||||
|
||||
SSH public key:
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567
|
||||
```
|
||||
|
||||
To allow login to a remote machine, that ssh public key can be added to
|
||||
`.ssh/authorized_keys` on that remote machine.
|
||||
|
||||
In the example output above, this string is the ssh public key:
|
||||
|
||||
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2dcYBqMCamidT5MpE3Cl3MIKcYMBekGXbK2aaN6JaH opgpcard:ABCD:01234567`
|
||||
|
||||
### Show OpenPGP card metadata
|
||||
|
||||
Print information about the capabilities of a card, including the list of supported algorithms (if the card returns
|
||||
that list).
|
||||
|
||||
Most of the output is probably not of interest to regular users.
|
||||
|
||||
```
|
||||
$ opgpcard info
|
||||
OpenPGP card FFFE:12345678 (card version 2.0)
|
||||
|
||||
Application Identifier: D276000124 01 0200 FFFE 12345678 0000
|
||||
Manufacturer [FFFE]: Range reserved for randomly assigned serial numbers.
|
||||
|
||||
Card Capabilities:
|
||||
- command chaining
|
||||
|
||||
Card service data:
|
||||
- Application Selection by full DF name
|
||||
- EF.DIR and EF.ATR/INFO access services by the GET DATA command (BER-TLV): 010
|
||||
|
||||
Extended Capabilities:
|
||||
- get challenge
|
||||
- key import
|
||||
- PW Status changeable
|
||||
- algorithm attributes changeable
|
||||
- KDF-DO
|
||||
- maximum length of challenge: 32
|
||||
- maximum length cardholder certificates: 2048
|
||||
- maximum command length: 255
|
||||
- maximum response length: 256
|
||||
|
||||
Supported algorithms:
|
||||
- SIG: RSA 2048 [e 32]
|
||||
- SIG: RSA 4096 [e 32]
|
||||
- SIG: Secp256k1 (ECDSA)
|
||||
- SIG: Ed25519 (EdDSA)
|
||||
- SIG: Ed448 (EdDSA)
|
||||
- DEC: RSA 2048 [e 32]
|
||||
- DEC: RSA 4096 [e 32]
|
||||
- DEC: Secp256k1 (ECDSA)
|
||||
- DEC: Cv25519 (ECDH)
|
||||
- DEC: X448 (ECDH)
|
||||
- AUT: RSA 2048 [e 32]
|
||||
- AUT: RSA 4096 [e 32]
|
||||
- AUT: Secp256k1 (ECDSA)
|
||||
- AUT: Ed25519 (EdDSA)
|
||||
- AUT: Ed448 (EdDSA)
|
||||
```
|
||||
|
||||
Or to query a specific card:
|
||||
|
||||
```
|
||||
$ opgpcard info --card ABCD:01234567
|
||||
```
|
||||
|
||||
### Admin commands
|
||||
|
||||
All `admin` commands need the Admin PIN. It can be provided as a file, with `-P <admin-pin-file>`,
|
||||
for non-interactive use (see below).
|
||||
|
||||
By default, the PIN must be entered interactively on the host computer, or via a pin pad if the OpenPGP card is
|
||||
used in a smart card reader that has a pin pad.
|
||||
|
||||
#### Set touch policy
|
||||
|
||||
Cards can require confirmation by the user before cryptographic operations are performed
|
||||
(this confirmation feature is often implemented as a mechanical button on the card).
|
||||
|
||||
However, not all cards implement this feature.
|
||||
|
||||
Rationale: when a card requires touch confirmation, an attacker who gains control of the user's host computer
|
||||
cannot perform cryptographic operations on the card at will - even after they learn the user's PINs.
|
||||
|
||||
This feature is configured per key slot. The user can choose to require (or not require) touch confirmation separately
|
||||
for signing, decryption, authentication and attestation operations.
|
||||
|
||||
E.g., when the touch policy is set to `On` for the `SIG` key slot, then every signing operation requires a touch button
|
||||
confirmation:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 touch --key SIG --policy On
|
||||
```
|
||||
|
||||
Valid values for the key slot are: `SIG`, `DEC`, `AUT`, `ATT` (some cards only support the first three).
|
||||
|
||||
Available policies can include: `Off`, `On`, `Fixed`, `Cached`, `CachedFixed`.
|
||||
Some cards only support a subset of these.
|
||||
|
||||
- `Off` means that no touch confirmation is required.
|
||||
- `On` means that each operation requires on touch confirmation.
|
||||
- The `Fixed` policies are like `On`, but the policies cannot be changed without performing a factory reset on the card.
|
||||
- With the `Cached` policies, a touch confirmation is valid for multiple operations within 15 seconds.
|
||||
|
||||
|
||||
#### Set cardholder name
|
||||
|
||||
Set the (informational) cardholder name:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 name "Alice Adams"
|
||||
```
|
||||
|
||||
#### Set certificate URL
|
||||
|
||||
The URL field on OpenPGP cards is intended to point to the certificate (or "public key") the corresponds to the keys
|
||||
that are present on the card.
|
||||
|
||||
It can be set like this:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 url "https://key.url.example"
|
||||
```
|
||||
|
||||
##### Using `keys.openpgp.org` for the URL
|
||||
|
||||
If you have uploaded (or plan to upload) your certificate (your public key) to the `keys.openpgp.org` keyserver,
|
||||
you can point the URL field on your card there:
|
||||
|
||||
If the fingerprint of your certificate is `0123456789ABCDEF0123456789ABCDEF01234567`, then you can set the URL
|
||||
as follows:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card FFFE:12345678 url "https://keys.openpgp.org/vks/v1/by-fingerprint/0123456789ABCDEF0123456789ABCDEF01234567"
|
||||
```
|
||||
|
||||
##### Other common options for certificate URLs
|
||||
|
||||
You can use any URL that serves your certificate ("public key"), including links to:
|
||||
|
||||
- gitlab (`https://gitlab.com/<username>.gpg`) or github (`https://github.com/<username>.gpg`)
|
||||
- any other keyserver, such as https://keyserver.ubuntu.com/,
|
||||
- a WKD server,
|
||||
- a copy of your certificate on your personal website, ...
|
||||
|
||||
|
||||
#### Import keys
|
||||
|
||||
Import private key onto a card. This works if at most one (sub)key per role
|
||||
(sign, decrypt, auth) exists in `key.priv`:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 import key.priv
|
||||
```
|
||||
|
||||
Import private key onto a card while explicitly selecting subkeys. Explicitly
|
||||
specified fingerprints are necessary if more than one subkey exists
|
||||
in `key.priv` for any role (spaces in fingerprints are ignored).
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> import key.priv \
|
||||
--sig-fp "F290 DBBF 21DB 8634 3C96 157B 87BE 15B7 F548 D97C" \
|
||||
--dec-fp "3C6E 08F6 7613 8935 8B8D 7666 73C7 F1A9 EEDA C360" \
|
||||
--auth-fp "D6AA 48EF 39A2 6F26 C42D 5BCB AAD2 14D5 5332 C838"
|
||||
```
|
||||
|
||||
When fingerprints are only specified for a subset of the roles, no keys will
|
||||
be imported for the other roles.
|
||||
|
||||
If the private (sub)keys in the import file are password protected, the user will be prompted to enter
|
||||
the password. If (sub)keys are encrypted with different passwords, the user will be prompted multiple times.
|
||||
(Background: OpenPGP keys can be password protected when they are stored in files, but on an OpenPGP card
|
||||
the keys always exist in unencrypted form. Therefore, they need to be decrypted for import.)
|
||||
|
||||
(NOTE: There is currently no mechanism to non-interactively provide passwords to import password protected
|
||||
OpenPGP keys)
|
||||
|
||||
#### Generate Keys on the card
|
||||
|
||||
This command generates new keys on an OpenPGP card. It creates the corresponding certificate ("public key")
|
||||
representation in an output file.
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 generate --output <output-cert-file> cv25519
|
||||
```
|
||||
|
||||
Note that key generation needs both the Admin PIN and the User PIN (the User PIN is needed to export the new key as
|
||||
a public key).
|
||||
|
||||
Output will look like:
|
||||
|
||||
```
|
||||
Enter Admin PIN:
|
||||
Enter User PIN:
|
||||
Generate subkey for Signing
|
||||
Generate subkey for Decryption
|
||||
Generate subkey for Authentication
|
||||
```
|
||||
|
||||
The `<output-cert-file>` will contain the corresponding certificate ("public key").
|
||||
|
||||
As part of the process of generating key material on a card, one or more User IDs can be included with the exported
|
||||
certificate:
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 generate --userid "Alice Adams <alice@example.org>" --output <output-cert-file> cv25519
|
||||
```
|
||||
|
||||
|
||||
### Signing
|
||||
|
||||
For now, this tool only supports creating detached signatures, like this
|
||||
(if no input file is set, stdin is read):
|
||||
|
||||
```
|
||||
$ opgpcard sign --detached --card ABCD:01234567 <input-file>
|
||||
```
|
||||
|
||||
### Decrypting
|
||||
|
||||
Decryption using a card (if no input file is set, stdin is read):
|
||||
|
||||
```
|
||||
$ opgpcard decrypt --card ABCD:01234567 <input-file>
|
||||
```
|
||||
|
||||
### PIN management
|
||||
|
||||
OpenPGP cards use PINs (numerical passwords) to verify that a user is allowed to perform an operation.
|
||||
|
||||
To use cryptographic operations on a card (such as decryption or signing), the *User PIN* is required.
|
||||
|
||||
To configure a card (for example to import OpenPGP key material into the card's key slots), the *Admin PIN* is needed.
|
||||
|
||||
By default, on unconfigured (or factory reset) cards, the User PIN is typically set to `123456`,
|
||||
and the Admin PIN is set to `12345678`.
|
||||
|
||||
#### Blocked cards and resetting
|
||||
|
||||
When a user has entered a wrong User PIN too often, the card goes into a blocked state, in which presenting the
|
||||
User PIN successfully is not possible anymore. The purpose of this is to prevent attackers from trying all possible
|
||||
PINs (e.g. after stealing a card).
|
||||
|
||||
To be able to use the card again, the User PIN must be "reset".
|
||||
|
||||
A User PIN reset can be performed by presenting the Admin PIN.
|
||||
|
||||
#### The resetting code
|
||||
|
||||
OpenPGP cards offer an additional, optional, *Resetting Code* mechanism.
|
||||
|
||||
The resetting code may be configured on a card and used to reset the User PIN if it has been forgotten or blocked.
|
||||
When unblocking a card with the Resetting Code, the Admin PIN is not needed.
|
||||
|
||||
The Resetting Code mechanism is only useful in scenarios where a user doesn't have access to (or prefers not to use)
|
||||
the Admin PIN (e.g. in some corporate settings, users might not be given the Admin PIN for
|
||||
their cards. Instead, an admin may define a resetting code and give that code to the user).
|
||||
|
||||
On un-configured (or factory reset) cards, the Resetting Code is typically unset.
|
||||
|
||||
|
||||
#### Set a new User PIN
|
||||
|
||||
Setting a new User PIN requires the Admin PIN:
|
||||
|
||||
```
|
||||
$ opgpcard pin --card ABCD:01234567 set-user
|
||||
```
|
||||
|
||||
#### Set new Admin PIN
|
||||
|
||||
This requires the (previous) Admin PIN.
|
||||
|
||||
```
|
||||
$ opgpcard pin --card ABCD:01234567 set-admin
|
||||
```
|
||||
|
||||
#### Reset User PIN with Admin PIN
|
||||
|
||||
The User PIN can be reset to a different (or the same) PIN by providing the Admin PIN.
|
||||
This is possible at any time, including when a wrong User PIN has been entered too often,
|
||||
and the card refuses to accept the User PIN anymore.
|
||||
|
||||
```
|
||||
$ opgpcard pin --card ABCD:01234567 reset-user
|
||||
```
|
||||
|
||||
#### Configuring the resetting code
|
||||
|
||||
The resetting code is an alternative mechanism to recover from a lost or locked User PIN.
|
||||
|
||||
You can set the resetting code after verifying the Admin PIN. Once a resetting code is configured on your card,
|
||||
you can use that code to reset the User PIN without needing the Admin PIN.
|
||||
|
||||
```
|
||||
$ opgpcard pin --card ABCD:01234567 set-reset
|
||||
```
|
||||
|
||||
#### Reset User PIN with the resetting code
|
||||
|
||||
If a resetting code is configured on a card, you can use that code to reset the User PIN:
|
||||
|
||||
```
|
||||
$ opgpcard pin --card ABCD:01234567 reset-user-rc
|
||||
Enter resetting code:
|
||||
Enter new User PIN:
|
||||
Repeat the new User PIN:
|
||||
|
||||
User PIN has been set.
|
||||
```
|
||||
|
||||
### Factory reset
|
||||
|
||||
A factory reset erases all data on your card, including the private key material that the card stores.
|
||||
|
||||
```
|
||||
$ opgpcard factory-reset --card ABCD:01234567
|
||||
```
|
||||
|
||||
NOTE: you do not need a PIN to reset a card!
|
||||
|
||||
### Directly entering PINs on card readers with pin pad
|
||||
|
||||
If your OpenPGP card is inserted in a card reader with a pin pad, this tool
|
||||
offers you the option to use the pin pad to enter the User- or Admin PINs.
|
||||
To do this, you can omit the `-p` and/or `-P` parameters. Then you will
|
||||
be prompted to enter the user or Admin PINs where needed.
|
||||
|
||||
|
||||
### Machine-readable Output (JSON, YAML)
|
||||
|
||||
This tool is can optionally provide its output in JSON (or YAML) format.
|
||||
The functionality is intended for scripting use.
|
||||
|
||||
For all commands that return relevant output, the parameter `--output-format json` chooses JSON as the output format.
|
||||
|
||||
For example, with the `status` command:
|
||||
|
||||
```
|
||||
$ opgpcard --output-format json status
|
||||
{
|
||||
"schema_version": "0.9.0",
|
||||
"ident": "ABCD:01234567",
|
||||
"card_version": "3.4",
|
||||
"cardholder_name": "Alice Adams",
|
||||
"language_preferences": [],
|
||||
"certificate_url": "http://alice.example/alice.pgp",
|
||||
"signature_key": {
|
||||
"fingerprint": "A393 4505 BC51 1177 2E0B 845A 142C C9AB 7126 5C00",
|
||||
"creation_time": "2022-10-31 13:45:35 UTC",
|
||||
"algorithm": "Ed25519 (EdDSA)",
|
||||
"touch_policy": "Off",
|
||||
"touch_features": "Button",
|
||||
"status": "generated",
|
||||
"public_key_material": "ECC [Ed25519 (EdDSA)], data: 3A2B88EF788FA59575E3C4DB89EE367DBD0D9E93B6CE26B7686D32E94958F32A"
|
||||
},
|
||||
"signature_count": 3,
|
||||
"user_pin_valid_for_only_one_signature": false,
|
||||
"decryption_key": {
|
||||
"fingerprint": "0643 F2A9 6605 4158 CCFA B11F C7D2 0DBA DA64 84E0",
|
||||
"creation_time": "2022-10-31 13:45:35 UTC",
|
||||
"algorithm": "Cv25519 (ECDH)",
|
||||
"touch_policy": "Off",
|
||||
"touch_features": "Button",
|
||||
"status": "generated",
|
||||
"public_key_material": "ECC [Cv25519 (ECDH)], data: AF97CA49B2D89998605985AEDAA19097A0CE7E5CC681B1ABD1C8610933FDB320"
|
||||
},
|
||||
"authentication_key": {
|
||||
"fingerprint": "2BA3 3B42 90DE 337D 1DF8 54B3 2E20 E550 3ABC 57A9",
|
||||
"creation_time": "2022-10-31 13:45:35 UTC",
|
||||
"algorithm": "Ed25519 (EdDSA)",
|
||||
"touch_policy": "Off",
|
||||
"touch_features": "Button",
|
||||
"status": "generated",
|
||||
"public_key_material": "ECC [Ed25519 (EdDSA)], data: 80178ECE7F16ACDFDB0A645C81E72287761F03488CE3AE01F74279AA88A9018C"
|
||||
},
|
||||
"attestation_key": {
|
||||
"fingerprint": null,
|
||||
"creation_time": null,
|
||||
"algorithm": "RSA 2048 [e 17]",
|
||||
"touch_policy": "Off",
|
||||
"touch_features": "Button",
|
||||
"status": null,
|
||||
"public_key_material": null
|
||||
},
|
||||
"user_pin_remaining_attempts": 3,
|
||||
"admin_pin_remaining_attempts": 3,
|
||||
"reset_code_remaining_attempts": 0
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Non-interactive use
|
||||
|
||||
All commands that require PIN entry can be used non-interactively by providing PINs via files
|
||||
(see the section "Using file-descriptors to provide PINs" for a variation on this).
|
||||
|
||||
In almost all contexts, `-p` is used to provide the User PIN and `-P` to provide the Admin PIN
|
||||
(the exception is when changing a PIN on the card, then a different parameter is used to provide the new PIN).
|
||||
|
||||
**Examples of non-interactive use**
|
||||
|
||||
- Setting the cardholder name:
|
||||
|
||||
`$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> name "Alice Adams"`
|
||||
|
||||
- Importing a key to the card:
|
||||
|
||||
`$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> import key.priv`
|
||||
|
||||
- Generating key material on the card:
|
||||
|
||||
`$ opgpcard admin --card ABCD:01234567 -P <admin-pin-file> generate -p <user-pin-file> --output <output-cert-file> cv25519`
|
||||
|
||||
- Creating a detached signature:
|
||||
|
||||
`$ opgpcard sign --detached --card ABCD:01234567 -p <user-pin-file> <input-file>`
|
||||
|
||||
**Examples of non-interactive PIN management**
|
||||
|
||||
- Setting a new User PIN:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-user -p <old-user-pin-file> -q <new-user-pin-file>`
|
||||
|
||||
- Setting a new Admin PIN:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-admin -P <old-admin-pin-file> -Q <new-admin-pin-file>`
|
||||
|
||||
- Setting a new User PIN based on the Admin PIN (and unblocking the card, if needed):
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 reset-user -P <admin-pin-file> -p <new-user-pin-file>`
|
||||
|
||||
- Setting the resetting code:
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 set-reset -P <admin-pin-file> -r <resetting-code-file>`
|
||||
|
||||
- Setting a new User ID based on the resetting code (and unblocking the card, if needed):
|
||||
|
||||
`$ opgpcard pin --card ABCD:01234567 reset-user-rc -r <resetting-code-file> -p <new-user-pin-file>`
|
||||
|
||||
#### Using file-descriptors to provide PINs
|
||||
|
||||
When using a shell like
|
||||
[bash](https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Strings)
|
||||
, you can pass User- and/or Admin PINs via file-descriptors (instead of from a file on disk):
|
||||
|
||||
```
|
||||
$ opgpcard sign --detached --card ABCD:01234567 -p /dev/fd/3 3<<<123456
|
||||
```
|
||||
|
||||
```
|
||||
$ opgpcard admin --card ABCD:01234567 -P /dev/fd/3 generate -p /dev/fd/4 --output <output-cert-file> cv25519 3<<<12345678 4<<<123456
|
||||
```
|
||||
|
||||
### Attestation
|
||||
|
||||
Yubico implements a [proprietary extension](https://developers.yubico.com/PGP/Attestation.html) to the OpenPGP card
|
||||
standard to *"cryptographically certify that a certain asymmetric key has been generated on device, and not imported"*.
|
||||
|
||||
This feature is available on YubiKey 5 devices with firmware version 5.2 or newer.
|
||||
|
||||
#### Attestation key/certificate
|
||||
|
||||
*"The YubiKey is preloaded with an attestation certificate and matching attestation key issued by the Yubico CA.
|
||||
The template and key are replaceable, which permits an individual or organization to issue attestations verifiable
|
||||
with their own CA if they prefer. If replaced, the Yubico template can never be restored."*
|
||||
|
||||
This tool does not currently support replacing the attestation key on a YubiKey.
|
||||
It only supports use of the Yubico-provided attestation key to generate "attestation statements".
|
||||
|
||||
The attestation certificate on a card can be inspected as follows:
|
||||
|
||||
```
|
||||
$ opgpcard attestation cert
|
||||
-----BEGIN CERTIFICATE-----
|
||||
[...]
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
#### Generating an attestation statement
|
||||
|
||||
For any key slot on the card you can generate an attestation statement,
|
||||
if the key material in that key slot has been generated on the card.
|
||||
|
||||
It's not possible to generate attestation statements for key material that was imported to the card
|
||||
(the attestation statement certifies that the key has been generated on the card).
|
||||
|
||||
To generate an attestation statement, run:
|
||||
|
||||
```
|
||||
$ opgpcard attestation generate --key SIG --card 0006:01234567
|
||||
```
|
||||
|
||||
Supported values for `--key` are `SIG`, `DEC` and `AUT`.
|
||||
|
||||
Generation of an attestation requires the User PIN. By default, it also requires touch confirmation
|
||||
(the touch policy configuration for the attestation key slot is set to `On` by default).
|
||||
|
||||
#### Viewing an attestation statement
|
||||
|
||||
When the YubiKey generates an attestation statement, it gets stored in a `cardholder certificate` data object on the card.
|
||||
|
||||
After an attestation statement has been generated, it can be read from the card and viewed in pem-encoded format:
|
||||
|
||||
```
|
||||
$ opgpcard attestation statement --key SIG
|
||||
-----BEGIN CERTIFICATE-----
|
||||
[...]
|
||||
-----END CERTIFICATE-----
|
||||
```
|
||||
|
||||
Supported values for `--key` are `SIG`, `DEC` and `AUT`.
|
|
@ -1,31 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fir>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
// Only generate test code from the subplot, if a virtual smart
|
||||
// card is available. This is a kludge until Subplot can do
|
||||
// conditional scenarios
|
||||
// (https://gitlab.com/subplot/subplot/-/issues/20).
|
||||
match option_env!("CARD_BASED_TESTS") {
|
||||
Some(_) => {
|
||||
subplot_build::codegen("subplot/opgpcard.subplot")
|
||||
.expect("failed to generate code with Subplot");
|
||||
println!("cargo:warning=generated subplot tests");
|
||||
}
|
||||
None => {
|
||||
// If we're not generating code from the subplot, we should at
|
||||
// least create an empty file so that the tests/opgpcard.rs
|
||||
// file can include it. Otherwise the build will fail.
|
||||
println!("cargo:warning=did not generate subplot tests");
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
let include = Path::new(&out_dir).join("opgpcard.rs");
|
||||
eprintln!("build.rs: include={}", include.display());
|
||||
if !include.exists() {
|
||||
File::create(include).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run this to run opgpcard (tools directory) test suite inside a
|
||||
# Docker container with a virtual smartcard running. The test suite
|
||||
# contains tests that run the opgpcard binary and rely on a virtual
|
||||
# smart card to be available.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
docker run --rm -it \
|
||||
-v root:/root \
|
||||
-v cargo:/cargo \
|
||||
-v $(pwd):/src \
|
||||
-e CARD_BASED_TESTS=true \
|
||||
registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps sh -c '
|
||||
apt install sq &&
|
||||
sed -i "s/timeout=20/timeout=60/" /home/jcardsim/run-card.sh &&
|
||||
/etc/init.d/pcscd start &&
|
||||
su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim &&
|
||||
cd /src/tools &&
|
||||
CARGO_TARGET_DIR=/cargo/ cargo update &&
|
||||
CARGO_TARGET_DIR=/cargo/ cargo build -vv &&
|
||||
CARGO_TARGET_DIR=/cargo/ cargo test -- --test-threads 1'
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
S="$(dpkg-parsechangelog -SSource)"
|
||||
V="$(dpkg-parsechangelog -SVersion | sed 's/-[^-]*$//')"
|
||||
|
||||
git archive HEAD | gzip >"../${S}_${V}.orig.tar.gz"
|
||||
dpkg-buildpackage -us -uc
|
|
@ -1,6 +0,0 @@
|
|||
openpgp-card-tool (0.0.11-1) unstable; urgency=medium
|
||||
|
||||
* Initial packaging. This is not intended to be uploaded to Debian, so
|
||||
not closing of an ITP bug.
|
||||
|
||||
-- Lars Wirzenius <liw@liw.fi> Thu, 30 Sep 2021 09:51:32 +0300
|
|
@ -1,2 +0,0 @@
|
|||
10
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Source: openpgp-card-tool
|
||||
Maintainer: Heiko Schaefer <heiko@schaefer.name>
|
||||
Uploaders: Lars Wirzenius <liw@liw.fi>
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Standards-Version: 4.2.0
|
||||
Build-Depends:
|
||||
debhelper (>= 10~),
|
||||
dh-cargo,
|
||||
libclang-dev,
|
||||
libpcsclite-dev,
|
||||
nettle-dev,
|
||||
pkg-config,
|
||||
Homepage: https://gitlab.com/openpgp-card/openpgp-card
|
||||
|
||||
Package: openpgp-card-tool
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}
|
||||
Built-Using: ${cargo:Built-Using}
|
||||
Description: tool to manage OpenPGP hardware tokens
|
||||
The opgpcard tool allows you to inspect, configure, administer,
|
||||
factory reset, and generally manage OpenPGP cards (hardware tokens),
|
||||
such as Gnuk, YubiKeys, Nitrokeys, and similar.
|
|
@ -1,3 +0,0 @@
|
|||
Copyright 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
@ -1 +0,0 @@
|
|||
README.md
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --buildsystem cargo
|
||||
|
||||
override_dh_auto_clean:
|
||||
echo auto clean
|
||||
|
||||
override_dh_auto_configure:
|
||||
echo auto configure
|
||||
|
||||
override_dh_auto_build:
|
||||
cargo --version
|
||||
rustc --version
|
||||
cargo build --release
|
||||
|
||||
override_dh_auto_test:
|
||||
true
|
||||
|
||||
override_dh_auto_install:
|
||||
install -d debian/openpgp-card-tool/bin
|
||||
cargo install --locked --path=. --root=debian/openpgp-card-tool
|
||||
find debian -name ".crates*" -delete
|
|
@ -1 +0,0 @@
|
|||
3.0 (quilt)
|
|
@ -1,149 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
#
|
||||
# WARNING: This will wipe any information on a card. Do not use it unless
|
||||
# you're very sure you don't mind.
|
||||
#
|
||||
# Prepare an OpenPGP card for use within a hypothetical organization:
|
||||
#
|
||||
# - factory reset the card
|
||||
# - set card holder name, if desired
|
||||
# - generate elliptic curve 25519 keys
|
||||
# - write to stdout a JSON object with the card id, card holder, and
|
||||
# key fingerprints
|
||||
#
|
||||
# Usage: run with --help.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from subprocess import run
|
||||
|
||||
|
||||
tracing = False
|
||||
|
||||
|
||||
def trace(msg):
|
||||
if tracing:
|
||||
sys.stderr.write(f"DEBUG: {msg}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def opgpcard_raw(args):
|
||||
argv = ["opgpcard"] + args
|
||||
trace(f"running {argv}")
|
||||
p = run(argv, capture_output=True)
|
||||
if p.returncode != 0:
|
||||
raise Exception(f"opgpcard failed:\n{p.stderr}")
|
||||
o = p.stdout
|
||||
trace(f"opgpcard raw output: {o!r}")
|
||||
return o
|
||||
|
||||
|
||||
def opgpcard_json(args):
|
||||
o = json.loads(opgpcard_raw(["--output-format=json"] + args))
|
||||
trace(f"opgpcard JSON output: {o}")
|
||||
return o
|
||||
|
||||
|
||||
def list_cards():
|
||||
return opgpcard_json(["list"])["idents"]
|
||||
|
||||
|
||||
def pick_card(card):
|
||||
cards = list_cards()
|
||||
if card is None:
|
||||
if not cards:
|
||||
raise Exception("No cards found")
|
||||
if len(cards) > 1:
|
||||
raise Exception(f"Can't pick card automatically: found {len(cards)} cards")
|
||||
return cards[0]
|
||||
elif card in cards:
|
||||
return card
|
||||
else:
|
||||
raise Exception(f"Can't find specified card {card}")
|
||||
|
||||
|
||||
def factory_reset(card):
|
||||
opgpcard_raw(["factory-reset", "--card", card])
|
||||
|
||||
|
||||
def set_card_holder(card, admin_pin, name):
|
||||
trace(f"set card holder to {name!r}")
|
||||
opgpcard_raw(["admin", "--card", card, "--admin-pin", admin_pin, "name", name])
|
||||
|
||||
|
||||
def generate_key(card, admin_pin, user_pin):
|
||||
opgpcard_raw(
|
||||
[
|
||||
"admin",
|
||||
f"--card={card}",
|
||||
f"--admin-pin={admin_pin}",
|
||||
"generate",
|
||||
f"--user-pin={user_pin}",
|
||||
"--output=/dev/null",
|
||||
"cv25519",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def status(card):
|
||||
o = opgpcard_json(["status", f"--card={card}"])
|
||||
return {
|
||||
"card_ident": o["ident"],
|
||||
"cardholder_name": o["cardholder_name"],
|
||||
"signature_key": o["signature_key"]["fingerprint"],
|
||||
"decryption_key": o["signature_key"]["fingerprint"],
|
||||
"authentication_key": o["signature_key"]["fingerprint"],
|
||||
}
|
||||
|
||||
|
||||
def card_is_empty(card):
|
||||
o = status(card)
|
||||
del o["card_ident"]
|
||||
for key in o:
|
||||
if o[key]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--force", action="store_true", help="prepare a card that has data")
|
||||
p.add_argument(
|
||||
"--verbose", action="store_true", help="produce debugging output to stderr"
|
||||
)
|
||||
p.add_argument("--card", help="card identifier, default is to pick the only one")
|
||||
p.add_argument("--card-holder", help="name of card holder", required=True)
|
||||
p.add_argument(
|
||||
"--admin-pin", action="store", help="set file with admin PIN", required=True
|
||||
)
|
||||
p.add_argument(
|
||||
"--user-pin", action="store", help="set file with user PIN", required=True
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
global tracing
|
||||
tracing = True
|
||||
|
||||
trace(f"args: {args}")
|
||||
card = pick_card(args.card)
|
||||
if not args.force and not card_is_empty(card):
|
||||
raise Exception(f"card {card} has existing keys, not touching it")
|
||||
factory_reset(card)
|
||||
set_card_holder(card, args.admin_pin, args.card_holder)
|
||||
key = generate_key(card, args.admin_pin, args.user_pin)
|
||||
o = status(card)
|
||||
print(json.dumps(o, indent=4))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"ERROR: {e}\n")
|
||||
sys.exit(1)
|
|
@ -1,61 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# Scripting around opgpcard
|
||||
|
||||
The `opgpcard` tool can manipulate an OpenPGP smart card (also known
|
||||
as hardware token). There are various commercial as well as Free Software-implementations of the standard.
|
||||
Well known commercial products with OpenPGP card support include YubiKey and Nitrokey. This tool is meant to work with
|
||||
any card that implements the OpenPGP smart card interface.
|
||||
|
||||
`opgpcard` supports structured output as JSON and YAML. The default is
|
||||
human-readable text. The structured output it meant to be consumed by
|
||||
other programs, and is versioned.
|
||||
|
||||
For example, to list all the OpenPGP cards connected to a system:
|
||||
|
||||
~~~sh
|
||||
$ opgpcard --output-format=json list
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"idents": [
|
||||
"ABCD:01234567"
|
||||
]
|
||||
}
|
||||
$
|
||||
~~~
|
||||
|
||||
The structured output is versioned (text output is not), using the
|
||||
field name `schema_version`. The version numbering follows [semantic
|
||||
versioning](https://semver.org/):
|
||||
|
||||
* if a field is added, the minor level is incremented, and patch level
|
||||
is set to zero
|
||||
* if a field is removed, the major level is incremented, and minor and
|
||||
patch level are set to zero
|
||||
* if there are changed with no semantic impatc, the patch level is
|
||||
incremented
|
||||
|
||||
Each version of `opgpcard` supports only the latest minor version for
|
||||
each major version. Consumers of the output have to be OK with added
|
||||
fields.
|
||||
|
||||
Thus, for example, if the `opgpcard list` command would add a new
|
||||
field for when the command was run, the output might look like this:
|
||||
|
||||
~~~sh
|
||||
$ opgpcard --output-format=json list
|
||||
{
|
||||
"schema_version": "1.1.0",
|
||||
"date": "Tue, 18 Oct 2022 18:07:41 +0300",
|
||||
"idents": [
|
||||
"ABCD:01234567"
|
||||
]
|
||||
}
|
||||
$
|
||||
~~~
|
||||
|
||||
A new field means the minor level in the schema version is
|
||||
incremented.
|
111
tools/src/cli.rs
111
tools/src/cli.rs
|
@ -1,111 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use clap::{AppSettings, Parser};
|
||||
|
||||
use crate::commands;
|
||||
use crate::{OutputFormat, OutputVersion};
|
||||
|
||||
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
||||
pub const DEFAULT_OUTPUT_VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "opgpcard",
|
||||
author = "Heiko Schäfer <heiko@schaefer.name>",
|
||||
version,
|
||||
global_setting(AppSettings::DeriveDisplayOrder),
|
||||
about = "A tool for inspecting and configuring OpenPGP cards."
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// Produce output in the chosen format.
|
||||
#[clap(long, value_enum, default_value_t = OutputFormat::Text)]
|
||||
pub output_format: OutputFormat,
|
||||
|
||||
/// Pick output version to use, for non-textual formats.
|
||||
#[clap(long, default_value_t = DEFAULT_OUTPUT_VERSION)]
|
||||
pub output_version: OutputVersion,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Command,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum Command {
|
||||
/// Enumerate available OpenPGP cards
|
||||
List {},
|
||||
|
||||
/// Show information about the data on a card
|
||||
Status(commands::status::StatusCommand),
|
||||
|
||||
/// Show technical details about a card
|
||||
Info(commands::info::InfoCommand),
|
||||
|
||||
/// Show a card's authentication key as an SSH public key
|
||||
Ssh(commands::ssh::SshCommand),
|
||||
|
||||
/// Export the key data on a card as an OpenPGP public key
|
||||
Pubkey(commands::pubkey::PubkeyCommand),
|
||||
|
||||
/// Administer data on a card (including keys and metadata)
|
||||
Admin(commands::admin::AdminCommand),
|
||||
|
||||
/// PIN management (change PINs, reset blocked PINs)
|
||||
#[clap(
|
||||
long_about = indoc::indoc! { "
|
||||
PIN management (change PINs, reset blocked PINs)
|
||||
|
||||
OpenPGP cards use PINs (numerical passwords) to verify that a user is allowed to \
|
||||
perform an operation. There are two PINs for regular operation, User PIN and Admin \
|
||||
PIN, and one optional Resetting Code.
|
||||
|
||||
The User PIN is required to use cryptographic operations on a card (such as \
|
||||
decryption or signing).
|
||||
The Admin PIN is needed to configure a card (for example to import an OpenPGP key \
|
||||
into the card) or to unblock the User PIN.
|
||||
The Resetting Code only allows unblocking the User PIN. This is useful if the user \
|
||||
doesn't have access to the Admin PIN.
|
||||
|
||||
By default, on unconfigured (or factory reset) cards, the User PIN is typically set to
|
||||
123456, and the Admin PIN is set to 12345678."
|
||||
},
|
||||
)]
|
||||
Pin(commands::pin::PinCommand),
|
||||
|
||||
/// Decrypt data using a card
|
||||
Decrypt(commands::decrypt::DecryptCommand),
|
||||
|
||||
/// Sign data using a card
|
||||
///
|
||||
/// Currently, only detached signatures are supported.
|
||||
Sign(commands::sign::SignCommand),
|
||||
|
||||
/// Attestation management (Yubico only)
|
||||
///
|
||||
/// Yubico implements a proprietary extension to the OpenPGP card standard to
|
||||
/// cryptographically certify that a certain asymmetric key has been generated on device, and
|
||||
/// not imported.
|
||||
///
|
||||
/// This feature is available on YubiKey 5 devices with firmware version 5.2 or newer.
|
||||
Attestation(commands::attestation::AttestationCommand),
|
||||
|
||||
/// Completely reset a card (deletes all data including keys!)
|
||||
FactoryReset(commands::factory_reset::FactoryResetCommand),
|
||||
|
||||
/// Change identity (Nitrokey Start only)
|
||||
///
|
||||
/// A Nitrokey Start device contains three distinct virtual OpenPGP cards, select the identity
|
||||
/// of the virtual card to activate.
|
||||
SetIdentity(commands::set_identity::SetIdentityCommand),
|
||||
|
||||
/// Show supported output format versions
|
||||
#[clap(
|
||||
long_about = indoc::indoc! { "
|
||||
Show supported output format versions for JSON and YAML output.
|
||||
|
||||
Mark the currently chosen one with a star."
|
||||
}
|
||||
)]
|
||||
OutputVersions {},
|
||||
}
|
|
@ -1,568 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use openpgp_card_sequoia::state::{Admin, Open, Transaction};
|
||||
use openpgp_card_sequoia::types::AlgoSimple;
|
||||
use openpgp_card_sequoia::util::public_key_material_to_key;
|
||||
use openpgp_card_sequoia::{sq_util, PublicKey};
|
||||
use openpgp_card_sequoia::{types::KeyType, Card};
|
||||
use sequoia_openpgp::cert::prelude::ValidErasedKeyAmalgamation;
|
||||
use sequoia_openpgp::packet::key::{SecretParts, UnspecifiedRole};
|
||||
use sequoia_openpgp::packet::Key;
|
||||
use sequoia_openpgp::parse::Parse;
|
||||
use sequoia_openpgp::policy::Policy;
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::serialize::SerializeInto;
|
||||
use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
|
||||
use sequoia_openpgp::Cert;
|
||||
|
||||
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||
use crate::{output, util, ENTER_ADMIN_PIN, ENTER_USER_PIN};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AdminCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
pub ident: String,
|
||||
|
||||
#[clap(
|
||||
name = "Admin PIN file",
|
||||
short = 'P',
|
||||
long = "admin-pin",
|
||||
help = "Optionally, get Admin PIN from a file"
|
||||
)]
|
||||
pub admin_pin: Option<PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub cmd: AdminSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum AdminSubCommand {
|
||||
/// Set cardholder name
|
||||
Name {
|
||||
#[clap(help = "cardholder name to set on the card")]
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Set certificate URL
|
||||
Url {
|
||||
#[clap(help = "URL that provides the certificate for the key material on this card")]
|
||||
url: String,
|
||||
},
|
||||
|
||||
/// Import a Key onto the card.
|
||||
///
|
||||
/// Most keys can be imported without specifying subkey fingerprints. However, if the key
|
||||
/// contins more than one signing, decryption or authentication capable subkey, subkeys must be
|
||||
/// explicitly selected.
|
||||
///
|
||||
/// If any of the options is given, only the selected subkeys are imported into the selected
|
||||
/// slots.
|
||||
///
|
||||
/// Subkey capabilities must match the slot the key is imported into. The DEC slot can
|
||||
/// only be used for encryption capable subkeys. The SIG and AUT slots can be used for signing,
|
||||
/// certification and authentication capable subkeys.
|
||||
Import {
|
||||
#[clap(help = "File that contains the PGP private key")]
|
||||
keyfile: PathBuf,
|
||||
|
||||
/// Optionally, select the subkey to import in the SIG slot
|
||||
#[clap(name = "SIG subkey fingerprint", short = 's', long = "sig-fp")]
|
||||
sig_fp: Option<String>,
|
||||
|
||||
/// Optionally, select the subkey to import in the DEC slot
|
||||
#[clap(name = "DEC subkey fingerprint", short = 'd', long = "dec-fp")]
|
||||
dec_fp: Option<String>,
|
||||
|
||||
/// Optionally, select the subkey to import in the AUT slot
|
||||
#[clap(name = "AUT subkey fingerprint", short = 'a', long = "aut-fp")]
|
||||
aut_fp: Option<String>,
|
||||
},
|
||||
|
||||
/// Generate a Key on the card.
|
||||
///
|
||||
/// A signing key is always created, decryption and authentication keys
|
||||
/// are optional.
|
||||
Generate(AdminGenerateCommand),
|
||||
|
||||
/// Set the card's touch policy (if supported)
|
||||
///
|
||||
/// A touch policy defines if cryptographic operations on the card require user interaction
|
||||
/// with the card, for example by touching a button on the card.
|
||||
///
|
||||
/// Only some cards support this feature at all, not all cards support all policies.
|
||||
///
|
||||
/// Caution: Setting the ATT slot to Fixed or Cached-Fixed is permanent. Even a factory reset does
|
||||
/// not undo this setting.
|
||||
Touch {
|
||||
/// Key slot to set the touch policy for
|
||||
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
||||
key: BasePlusAttKeySlot,
|
||||
|
||||
/// Touch policy to set on this key slot
|
||||
#[clap(
|
||||
name = "Policy",
|
||||
short = 'p',
|
||||
long = "policy",
|
||||
value_enum,
|
||||
long_help = "Touch policy to set on this key slot
|
||||
|
||||
Off: No touch confirmation required.
|
||||
On: Touch confirmation required for each operation.
|
||||
Fixed: Like 'On', but the policy can only be changed by a reset.
|
||||
Cached: Like 'On', but touch confirmation is valid for 15 seconds.
|
||||
Cached-Fixed: Combines 'Cached' and 'Fixed'."
|
||||
)]
|
||||
policy: TouchPolicy,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AdminGenerateCommand {
|
||||
/// Output file
|
||||
#[clap(name = "output", long = "output", short = 'o')]
|
||||
output_file: PathBuf,
|
||||
|
||||
/// Do not create a key in the DEC slot
|
||||
#[clap(long = "no-dec", action = clap::ArgAction::SetFalse)]
|
||||
decrypt: bool,
|
||||
|
||||
/// Do not create a key in the AUT slot
|
||||
#[clap(long = "no-aut", action = clap::ArgAction::SetFalse)]
|
||||
auth: bool,
|
||||
|
||||
/// Choose the algorithm for the key material to generate on the card.
|
||||
///
|
||||
/// If the parameter is not given, use the algorithm currently set on the card.
|
||||
///
|
||||
/// Specific cards support a set of algorithms that can differ between models. On modern cards,
|
||||
/// use 'opgpcard info' to see the list of supported algorithms.
|
||||
#[clap(name = "algorithm", value_enum)]
|
||||
algo: Option<Algo>,
|
||||
|
||||
/// User ID to add to the exported certificate representation
|
||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
||||
user_ids: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
user_pin: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
#[clap(rename_all = "UPPER")]
|
||||
pub enum BasePlusAttKeySlot {
|
||||
Sig,
|
||||
Dec,
|
||||
Aut,
|
||||
Att,
|
||||
}
|
||||
|
||||
impl From<BasePlusAttKeySlot> for KeyType {
|
||||
fn from(ks: BasePlusAttKeySlot) -> Self {
|
||||
match ks {
|
||||
BasePlusAttKeySlot::Sig => KeyType::Signing,
|
||||
BasePlusAttKeySlot::Dec => KeyType::Decryption,
|
||||
BasePlusAttKeySlot::Aut => KeyType::Authentication,
|
||||
BasePlusAttKeySlot::Att => KeyType::Attestation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
pub enum TouchPolicy {
|
||||
#[clap(name = "Off")]
|
||||
Off,
|
||||
#[clap(name = "On")]
|
||||
On,
|
||||
#[clap(name = "Fixed")]
|
||||
Fixed,
|
||||
#[clap(name = "Cached")]
|
||||
Cached,
|
||||
#[clap(name = "Cached-Fixed")]
|
||||
CachedFixed,
|
||||
}
|
||||
|
||||
impl From<TouchPolicy> for openpgp_card_sequoia::types::TouchPolicy {
|
||||
fn from(tp: TouchPolicy) -> Self {
|
||||
use openpgp_card_sequoia::types::TouchPolicy as OCTouchPolicy;
|
||||
match tp {
|
||||
TouchPolicy::On => OCTouchPolicy::On,
|
||||
TouchPolicy::Off => OCTouchPolicy::Off,
|
||||
TouchPolicy::Fixed => OCTouchPolicy::Fixed,
|
||||
TouchPolicy::Cached => OCTouchPolicy::Cached,
|
||||
TouchPolicy::CachedFixed => OCTouchPolicy::CachedFixed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
#[clap(rename_all = "lower")]
|
||||
pub enum Algo {
|
||||
Rsa2048,
|
||||
Rsa3072,
|
||||
Rsa4096,
|
||||
Nistp256,
|
||||
Nistp384,
|
||||
Nistp521,
|
||||
Cv25519,
|
||||
}
|
||||
|
||||
impl From<Algo> for AlgoSimple {
|
||||
fn from(a: Algo) -> Self {
|
||||
match a {
|
||||
Algo::Rsa2048 => AlgoSimple::RSA2k,
|
||||
Algo::Rsa3072 => AlgoSimple::RSA3k,
|
||||
Algo::Rsa4096 => AlgoSimple::RSA4k,
|
||||
Algo::Nistp256 => AlgoSimple::NIST256,
|
||||
Algo::Nistp384 => AlgoSimple::NIST384,
|
||||
Algo::Nistp521 => AlgoSimple::NIST521,
|
||||
Algo::Cv25519 => AlgoSimple::Curve25519,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admin(
|
||||
output_format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
command: AdminCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
let admin_pin = util::get_pin(&mut card, command.admin_pin, ENTER_ADMIN_PIN)?;
|
||||
|
||||
match command.cmd {
|
||||
AdminSubCommand::Name { name } => {
|
||||
name_command(&name, card, admin_pin.as_deref())?;
|
||||
}
|
||||
AdminSubCommand::Url { url } => {
|
||||
url_command(&url, card, admin_pin.as_deref())?;
|
||||
}
|
||||
AdminSubCommand::Import {
|
||||
keyfile,
|
||||
sig_fp,
|
||||
dec_fp,
|
||||
aut_fp,
|
||||
} => {
|
||||
import_command(keyfile, sig_fp, dec_fp, aut_fp, card, admin_pin.as_deref())?;
|
||||
}
|
||||
AdminSubCommand::Generate(cmd) => {
|
||||
generate_command(
|
||||
output_format,
|
||||
output_version,
|
||||
card,
|
||||
admin_pin.as_deref(),
|
||||
cmd,
|
||||
)?;
|
||||
}
|
||||
AdminSubCommand::Touch { key, policy } => {
|
||||
touch_command(card, admin_pin.as_deref(), key, policy)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keys_pick_yolo<'a>(
|
||||
key: &'a Cert,
|
||||
policy: &'a dyn Policy,
|
||||
) -> Result<[Option<ValidErasedKeyAmalgamation<'a, SecretParts>>; 3]> {
|
||||
let key_by_type = |kt| sq_util::subkey_by_type(key, policy, kt);
|
||||
|
||||
Ok([
|
||||
key_by_type(KeyType::Signing)?,
|
||||
key_by_type(KeyType::Decryption)?,
|
||||
key_by_type(KeyType::Authentication)?,
|
||||
])
|
||||
}
|
||||
|
||||
fn keys_pick_explicit<'a>(
|
||||
key: &'a Cert,
|
||||
policy: &'a dyn Policy,
|
||||
sig_fp: Option<String>,
|
||||
dec_fp: Option<String>,
|
||||
aut_fp: Option<String>,
|
||||
) -> Result<[Option<ValidErasedKeyAmalgamation<'a, SecretParts>>; 3]> {
|
||||
let key_by_fp = |fp: Option<String>| match fp {
|
||||
Some(fp) => sq_util::private_subkey_by_fingerprint(key, policy, &fp),
|
||||
None => Ok(None),
|
||||
};
|
||||
|
||||
Ok([key_by_fp(sig_fp)?, key_by_fp(dec_fp)?, key_by_fp(aut_fp)?])
|
||||
}
|
||||
|
||||
fn gen_subkeys(
|
||||
admin: &mut Card<Admin>,
|
||||
decrypt: bool,
|
||||
auth: bool,
|
||||
algo: Option<AlgoSimple>,
|
||||
) -> Result<(PublicKey, Option<PublicKey>, Option<PublicKey>)> {
|
||||
// We begin by generating the signing subkey, which is mandatory.
|
||||
println!(" Generate subkey for Signing");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Signing, algo)?;
|
||||
let key_sig = public_key_material_to_key(&pkm, KeyType::Signing, &ts, None, None)?;
|
||||
|
||||
// make decryption subkey (unless disabled), with the same algorithm as
|
||||
// the sig key
|
||||
let key_dec = if decrypt {
|
||||
println!(" Generate subkey for Decryption");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Decryption, algo)?;
|
||||
Some(public_key_material_to_key(
|
||||
&pkm,
|
||||
KeyType::Decryption,
|
||||
&ts,
|
||||
Some(HashAlgorithm::SHA256), // FIXME
|
||||
Some(SymmetricAlgorithm::AES128), // FIXME
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// make authentication subkey (unless disabled), with the same
|
||||
// algorithm as the sig key
|
||||
let key_aut = if auth {
|
||||
println!(" Generate subkey for Authentication");
|
||||
let (pkm, ts) = admin.generate_key_simple(KeyType::Authentication, algo)?;
|
||||
|
||||
Some(public_key_material_to_key(
|
||||
&pkm,
|
||||
KeyType::Authentication,
|
||||
&ts,
|
||||
None,
|
||||
None,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((key_sig, key_dec, key_aut))
|
||||
}
|
||||
|
||||
fn name_command(
|
||||
name: &str,
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
admin.set_name(name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn url_command(
|
||||
url: &str,
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
admin.set_url(url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_command(
|
||||
keyfile: PathBuf,
|
||||
sig_fp: Option<String>,
|
||||
dec_fp: Option<String>,
|
||||
aut_fp: Option<String>,
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let key = Cert::from_file(keyfile)?;
|
||||
|
||||
let p = StandardPolicy::new();
|
||||
|
||||
// select the (sub)keys to upload
|
||||
let [sig, dec, auth] = match (&sig_fp, &dec_fp, &aut_fp) {
|
||||
// No fingerprint has been provided, try to autoselect keys
|
||||
// (this fails if there is more than one (sub)key for any keytype).
|
||||
(&None, &None, &None) => keys_pick_yolo(&key, &p)?,
|
||||
|
||||
_ => keys_pick_explicit(&key, &p, sig_fp, dec_fp, aut_fp)?,
|
||||
};
|
||||
|
||||
let mut pws: Vec<String> = vec![];
|
||||
|
||||
// helper: true, if `pw` decrypts `key`
|
||||
let pw_ok = |key: &Key<SecretParts, UnspecifiedRole>, pw: &str| {
|
||||
key.clone()
|
||||
.decrypt_secret(&sequoia_openpgp::crypto::Password::from(pw))
|
||||
.is_ok()
|
||||
};
|
||||
|
||||
// helper: if any password in `pws` decrypts `key`, return that password
|
||||
let find_pw = |key: &Key<SecretParts, UnspecifiedRole>, pws: &[String]| {
|
||||
pws.iter().find(|pw| pw_ok(key, pw)).cloned()
|
||||
};
|
||||
|
||||
// helper: check if we have the right password for `key` in `pws`,
|
||||
// if so return it. otherwise ask the user for the password,
|
||||
// add it to `pws` and return it.
|
||||
let mut get_pw_for_key = |key: &Option<ValidErasedKeyAmalgamation<SecretParts>>,
|
||||
key_type: &str|
|
||||
-> Result<Option<String>> {
|
||||
if let Some(k) = key {
|
||||
if !k.has_secret() {
|
||||
// key has no secret key material, it can't be imported
|
||||
return Err(anyhow!(
|
||||
"(Sub)Key {} contains no private key material",
|
||||
k.fingerprint()
|
||||
));
|
||||
}
|
||||
|
||||
if k.has_unencrypted_secret() {
|
||||
// key is unencrypted, we need no password
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// key is encrypted, we need the password
|
||||
|
||||
// do we already have the right password?
|
||||
if let Some(pw) = find_pw(k, &pws) {
|
||||
return Ok(Some(pw));
|
||||
}
|
||||
|
||||
// no, we need to get the password from user
|
||||
let pw = rpassword::prompt_password(format!(
|
||||
"Enter password for {} (sub)key {}:",
|
||||
key_type,
|
||||
k.fingerprint()
|
||||
))?;
|
||||
|
||||
if pw_ok(k, &pw) {
|
||||
// remember pw for next subkeys
|
||||
pws.push(pw.clone());
|
||||
|
||||
Ok(Some(pw))
|
||||
} else {
|
||||
// this password doesn't work, error out
|
||||
Err(anyhow!(
|
||||
"Password not valid for (Sub)Key {}",
|
||||
k.fingerprint()
|
||||
))
|
||||
}
|
||||
} else {
|
||||
// we have no key for this slot, so we don't need a password
|
||||
Ok(None)
|
||||
}
|
||||
};
|
||||
|
||||
// get passwords, if encrypted (try previous pw before asking for user input)
|
||||
let sig_p = get_pw_for_key(&sig, "signing")?;
|
||||
let dec_p = get_pw_for_key(&dec, "decryption")?;
|
||||
let auth_p = get_pw_for_key(&auth, "authentication")?;
|
||||
|
||||
// upload keys to card
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
if let Some(sig) = sig {
|
||||
println!("Uploading {} as signing key", sig.fingerprint());
|
||||
admin.upload_key(sig, KeyType::Signing, sig_p)?;
|
||||
}
|
||||
if let Some(dec) = dec {
|
||||
println!("Uploading {} as decryption key", dec.fingerprint());
|
||||
admin.upload_key(dec, KeyType::Decryption, dec_p)?;
|
||||
}
|
||||
if let Some(auth) = auth {
|
||||
println!("Uploading {} as authentication key", auth.fingerprint());
|
||||
admin.upload_key(auth, KeyType::Authentication, auth_p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_command(
|
||||
output_format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
mut card: Card<Transaction>,
|
||||
|
||||
admin_pin: Option<&[u8]>,
|
||||
|
||||
cmd: AdminGenerateCommand,
|
||||
) -> Result<()> {
|
||||
let user_pin = util::get_pin(&mut card, cmd.user_pin, ENTER_USER_PIN)?;
|
||||
|
||||
let mut output = output::AdminGenerate::default();
|
||||
output.ident(card.application_identifier()?.ident());
|
||||
|
||||
// 1) Interpret the user's choice of algorithm.
|
||||
//
|
||||
// Unset (None) means that the algorithm that is specified on the card
|
||||
// should remain unchanged.
|
||||
//
|
||||
// For RSA, different cards use different exact algorithm
|
||||
// specifications. In particular, the length of the value `e` differs
|
||||
// between cards. Some devices use 32 bit length for e, others use 17 bit.
|
||||
// In some cases, it's possible to get this information from the card,
|
||||
// but I believe this information is not obtainable in all cases.
|
||||
// Because of this, for generation of RSA keys, here we take the approach
|
||||
// of first trying one variant, and then if that fails, try the other.
|
||||
|
||||
let algo = cmd.algo.map(AlgoSimple::from);
|
||||
log::info!(" Key generation will be attempted with algo: {:?}", algo);
|
||||
output.algorithm(format!("{algo:?}"));
|
||||
|
||||
// 2) Then, generate keys on the card.
|
||||
// We need "admin" access to the card for this).
|
||||
let (key_sig, key_dec, key_aut) = {
|
||||
if let Ok(mut admin) = util::verify_to_admin(&mut card, admin_pin) {
|
||||
gen_subkeys(&mut admin, cmd.decrypt, cmd.auth, algo)?
|
||||
} else {
|
||||
return Err(anyhow!("Failed to open card in admin mode."));
|
||||
}
|
||||
};
|
||||
|
||||
// 3) Generate a Cert from the generated keys. For this, we
|
||||
// need "signing" access to the card (to make binding signatures within
|
||||
// the Cert).
|
||||
let cert = crate::get_cert(
|
||||
&mut card,
|
||||
key_sig,
|
||||
key_dec,
|
||||
key_aut,
|
||||
user_pin.as_deref(),
|
||||
&cmd.user_ids,
|
||||
&|| println!("Enter User PIN on card reader pinpad."),
|
||||
)?;
|
||||
|
||||
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
||||
output.public_key(armored);
|
||||
|
||||
// Write armored certificate to the output file
|
||||
let mut handle = util::open_or_stdout(Some(&cmd.output_file))?;
|
||||
handle.write_all(output.print(output_format, output_version)?.as_bytes())?;
|
||||
let _ = handle.write(b"\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn touch_command(
|
||||
mut card: Card<Transaction>,
|
||||
admin_pin: Option<&[u8]>,
|
||||
key: BasePlusAttKeySlot,
|
||||
policy: TouchPolicy,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let kt = KeyType::from(key);
|
||||
|
||||
let pol = openpgp_card_sequoia::types::TouchPolicy::from(policy);
|
||||
|
||||
let mut admin = util::verify_to_admin(&mut card, admin_pin)?;
|
||||
|
||||
admin.set_uif(kt, pol)?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use openpgp_card_sequoia::types::KeyType;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||
use crate::ENTER_USER_PIN;
|
||||
use crate::{output, pick_card_for_reading, util};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AttestationCommand {
|
||||
#[clap(subcommand)]
|
||||
pub cmd: AttSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum AttSubCommand {
|
||||
/// Print the card's attestation certificate
|
||||
///
|
||||
/// New YubiKeys are preloaded with an attestation certificate issued by the Yubico CA.
|
||||
Cert {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: Option<String>,
|
||||
},
|
||||
|
||||
/// Generate attestation statement for one of the key slots on the card
|
||||
///
|
||||
/// An attestation statement can only be generated for key slots that contain keys that were
|
||||
/// generated by the card. See 'opgpcard admin generate' and 'opgpcard status -v'.
|
||||
Generate {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: String,
|
||||
|
||||
/// Key slot to use
|
||||
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
||||
key: BaseKeySlot,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
user_pin: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Print the attestation statement for one of the key slots on the card
|
||||
///
|
||||
/// An attestation statement can only be printed after generating it. See 'opgpcard attestation
|
||||
/// generate'.
|
||||
Statement {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to reset"
|
||||
)]
|
||||
ident: Option<String>,
|
||||
|
||||
/// Key slot to use
|
||||
#[clap(name = "Key slot", short = 'k', long = "key", value_enum)]
|
||||
key: BaseKeySlot,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
#[clap(rename_all = "UPPER")]
|
||||
pub enum BaseKeySlot {
|
||||
Sig,
|
||||
Dec,
|
||||
Aut,
|
||||
}
|
||||
|
||||
impl From<BaseKeySlot> for KeyType {
|
||||
fn from(ks: BaseKeySlot) -> Self {
|
||||
match ks {
|
||||
BaseKeySlot::Sig => KeyType::Signing,
|
||||
BaseKeySlot::Dec => KeyType::Decryption,
|
||||
BaseKeySlot::Aut => KeyType::Authentication,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attestation(
|
||||
output_format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
command: AttestationCommand,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match command.cmd {
|
||||
AttSubCommand::Cert { ident } => cert(output_format, output_version, ident),
|
||||
AttSubCommand::Generate {
|
||||
ident,
|
||||
key,
|
||||
user_pin,
|
||||
} => generate(&ident, key, user_pin),
|
||||
AttSubCommand::Statement { ident, key } => statement(ident, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn cert(
|
||||
output_format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
ident: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut output = output::AttestationCert::default();
|
||||
|
||||
let backend = pick_card_for_reading(ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
output.ident(card.application_identifier()?.ident());
|
||||
|
||||
if let Ok(ac) = card.attestation_certificate() {
|
||||
let pem = util::pem_encode(ac);
|
||||
output.attestation_cert(pem);
|
||||
}
|
||||
|
||||
println!("{}", output.print(output_format, output_version)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate(
|
||||
ident: &str,
|
||||
key: BaseKeySlot,
|
||||
user_pin: Option<PathBuf>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
let user_pin = util::get_pin(&mut card, user_pin, ENTER_USER_PIN)?;
|
||||
|
||||
let mut sign = util::verify_to_sign(&mut card, user_pin.as_deref())?;
|
||||
|
||||
let kt = KeyType::from(key);
|
||||
sign.generate_attestation(kt, &|| {
|
||||
println!("Touch confirmation needed to generate an attestation")
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn statement(ident: Option<String>, key: BaseKeySlot) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = pick_card_for_reading(ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
// Get cardholder certificate from card.
|
||||
|
||||
let mut select_data_workaround = false;
|
||||
// Use "select data" workaround if the card reports a
|
||||
// yk firmware version number >= 5 and <= 5.4.3
|
||||
if let Ok(version) = card.firmware_version() {
|
||||
if version.len() == 3
|
||||
&& version[0] == 5
|
||||
&& (version[1] < 4 || (version[1] == 4 && version[2] <= 3))
|
||||
{
|
||||
select_data_workaround = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Select cardholder certificate
|
||||
match key {
|
||||
BaseKeySlot::Aut => card.select_data(0, &[0x7F, 0x21], select_data_workaround)?,
|
||||
BaseKeySlot::Dec => card.select_data(1, &[0x7F, 0x21], select_data_workaround)?,
|
||||
BaseKeySlot::Sig => card.select_data(2, &[0x7F, 0x21], select_data_workaround)?,
|
||||
};
|
||||
|
||||
// Get DO "cardholder certificate" (returns the slot that was previously selected)
|
||||
let cert = card.cardholder_certificate()?;
|
||||
|
||||
if !cert.is_empty() {
|
||||
let pem = util::pem_encode(cert);
|
||||
println!("{pem}");
|
||||
} else {
|
||||
println!("Cardholder certificate slot is empty");
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
use sequoia_openpgp::{
|
||||
parse::{stream::DecryptorBuilder, Parse},
|
||||
policy::StandardPolicy,
|
||||
};
|
||||
|
||||
use crate::util;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct DecryptCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: String,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
pin_file: Option<PathBuf>,
|
||||
|
||||
/// Input file (stdin if unset)
|
||||
#[clap(name = "input")]
|
||||
input: Option<PathBuf>,
|
||||
|
||||
/// Output file (stdout if unset)
|
||||
#[clap(name = "output", long = "output", short = 'o')]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn decrypt(command: DecryptCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let p = StandardPolicy::new();
|
||||
|
||||
let input = util::open_or_stdin(command.input.as_deref())?;
|
||||
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
if card.fingerprints()?.decryption().is_none() {
|
||||
return Err(anyhow!("Can't decrypt: this card has no key in the decryption slot.").into());
|
||||
}
|
||||
|
||||
let user_pin = util::get_pin(&mut card, command.pin_file, crate::ENTER_USER_PIN)?;
|
||||
|
||||
let mut user = util::verify_to_user(&mut card, user_pin.as_deref())?;
|
||||
let d = user.decryptor(&|| println!("Touch confirmation needed for decryption"))?;
|
||||
|
||||
let db = DecryptorBuilder::from_reader(input)?;
|
||||
let mut decryptor = db.with_policy(&p, None, d)?;
|
||||
|
||||
let mut sink = util::open_or_stdout(command.output.as_deref())?;
|
||||
std::io::copy(&mut decryptor, &mut sink)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
use crate::util;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct FactoryResetCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: String,
|
||||
}
|
||||
|
||||
pub fn factory_reset(command: FactoryResetCommand) -> Result<()> {
|
||||
println!("Resetting Card {}", command.ident);
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
|
||||
let mut card = open.transaction()?;
|
||||
card.factory_reset().map_err(|e| anyhow!(e))
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
use crate::output;
|
||||
use crate::pick_card_for_reading;
|
||||
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct InfoCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
pub ident: Option<String>,
|
||||
}
|
||||
|
||||
/// print metadata information about a card
|
||||
pub fn print_info(
|
||||
format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
command: InfoCommand,
|
||||
) -> Result<()> {
|
||||
let mut output = output::Info::default();
|
||||
|
||||
let backend = pick_card_for_reading(command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
let ai = card.application_identifier()?;
|
||||
|
||||
output.ident(ai.ident());
|
||||
|
||||
let version = ai.version().to_be_bytes();
|
||||
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||
|
||||
output.application_id(ai.to_string());
|
||||
output.manufacturer_id(format!("{:04X}", ai.manufacturer()));
|
||||
output.manufacturer_name(ai.manufacturer_name().to_string());
|
||||
|
||||
if let Some(cc) = card.historical_bytes()?.card_capabilities() {
|
||||
for line in cc.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||
output.card_capability(line.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(csd) = card.historical_bytes()?.card_service_data() {
|
||||
for line in csd.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||
output.card_service_data(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(eli) = card.extended_length_information()? {
|
||||
for line in eli.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||
output.extended_length_info(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let ec = card.extended_capabilities()?;
|
||||
for line in ec.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||
output.extended_capability(line.to_string());
|
||||
}
|
||||
|
||||
// Algorithm information (list of supported algorithms)
|
||||
//
|
||||
// FIXME: this should be output in a more structured shape
|
||||
// Algorithms should be grouped by key slot, and the format of the algorithm name should
|
||||
// probably have a human readable, and an alternate machine readable format.
|
||||
// Both formats should be output for machine readable formats.
|
||||
if let Ok(Some(ai)) = card.algorithm_information() {
|
||||
for line in ai.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or_else(|| line.trim());
|
||||
output.algorithm(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: print KDF info
|
||||
|
||||
// YubiKey specific (?) firmware version
|
||||
if let Ok(ver) = card.firmware_version() {
|
||||
let ver = ver.iter().map(u8::to_string).collect::<Vec<_>>().join(".");
|
||||
output.firmware_version(ver);
|
||||
}
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
pub mod admin;
|
||||
pub mod attestation;
|
||||
pub mod decrypt;
|
||||
pub mod factory_reset;
|
||||
pub mod info;
|
||||
pub mod pin;
|
||||
pub mod pubkey;
|
||||
pub mod set_identity;
|
||||
pub mod sign;
|
||||
pub mod ssh;
|
||||
pub mod status;
|
|
@ -1,364 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::{state::Open, state::Transaction, Card};
|
||||
|
||||
use crate::util;
|
||||
use crate::util::{load_pin, print_gnuk_note};
|
||||
use crate::{ENTER_ADMIN_PIN, ENTER_USER_PIN};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct PinCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
pub ident: String,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub cmd: PinSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum PinSubCommand {
|
||||
/// Set User PIN
|
||||
///
|
||||
/// Set a new User PIN by providing the current User PIN.
|
||||
SetUser {
|
||||
#[clap(
|
||||
name = "User PIN file old",
|
||||
short = 'p',
|
||||
long = "user-pin-old",
|
||||
help = "Optionally, get old User PIN from a file"
|
||||
)]
|
||||
user_pin_old: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file new",
|
||||
short = 'q',
|
||||
long = "user-pin-new",
|
||||
help = "Optionally, get new User PIN from a file"
|
||||
)]
|
||||
user_pin_new: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Set Admin PIN
|
||||
///
|
||||
/// Set a new Admin PIN by providing the current Admin PIN.
|
||||
SetAdmin {
|
||||
#[clap(
|
||||
name = "Admin PIN file old",
|
||||
short = 'P',
|
||||
long = "admin-pin-old",
|
||||
help = "Optionally, get old Admin PIN from a file"
|
||||
)]
|
||||
admin_pin_old: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "Admin PIN file new",
|
||||
short = 'Q',
|
||||
long = "admin-pin-new",
|
||||
help = "Optionally, get new Admin PIN from a file"
|
||||
)]
|
||||
admin_pin_new: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Reset User PIN with Admin PIN
|
||||
///
|
||||
/// Set a new User PIN by providing the Admin PIN. This can also be used if the User PIN has
|
||||
/// been blocked.
|
||||
ResetUser {
|
||||
#[clap(
|
||||
name = "Admin PIN file",
|
||||
short = 'P',
|
||||
long = "admin-pin",
|
||||
help = "Optionally, get Admin PIN from a file"
|
||||
)]
|
||||
admin_pin: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file new",
|
||||
short = 'p',
|
||||
long = "user-pin-new",
|
||||
help = "Optionally, get new User PIN from a file"
|
||||
)]
|
||||
user_pin_new: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Reset User PIN with Resetting Code
|
||||
///
|
||||
/// Set a new User PIN by providing the Resetting Code. This can also be used if the User PIN
|
||||
/// has been blocked.
|
||||
ResetUserRc {
|
||||
#[clap(
|
||||
name = "Resetting Code file",
|
||||
short = 'r',
|
||||
long = "reset-code",
|
||||
help = "Optionally, get the Resetting Code from a file"
|
||||
)]
|
||||
reset_code: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file new",
|
||||
short = 'p',
|
||||
long = "user-pin-new",
|
||||
help = "Optionally, get new User PIN from a file"
|
||||
)]
|
||||
user_pin_new: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Set Resetting Code
|
||||
///
|
||||
/// Set a Resetting Code by providing the Admin PIN.
|
||||
SetReset {
|
||||
#[clap(
|
||||
name = "Admin PIN file",
|
||||
short = 'P',
|
||||
long = "admin-pin",
|
||||
help = "Optionally, get Admin PIN from a file"
|
||||
)]
|
||||
admin_pin: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "Resetting Code file",
|
||||
short = 'r',
|
||||
long = "reset-code",
|
||||
help = "Optionally, get the Resetting Code from a file"
|
||||
)]
|
||||
reset_code: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn pin(ident: &str, cmd: PinSubCommand) -> Result<()> {
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let card = open.transaction()?;
|
||||
|
||||
match cmd {
|
||||
PinSubCommand::SetUser {
|
||||
user_pin_old,
|
||||
user_pin_new,
|
||||
} => set_user(user_pin_old, user_pin_new, card),
|
||||
|
||||
PinSubCommand::SetAdmin {
|
||||
admin_pin_old,
|
||||
admin_pin_new,
|
||||
} => set_admin(admin_pin_old, admin_pin_new, card),
|
||||
|
||||
PinSubCommand::ResetUser {
|
||||
admin_pin,
|
||||
user_pin_new,
|
||||
} => reset_user(admin_pin, user_pin_new, card),
|
||||
|
||||
PinSubCommand::SetReset {
|
||||
admin_pin,
|
||||
reset_code,
|
||||
} => set_reset(admin_pin, reset_code, card),
|
||||
|
||||
PinSubCommand::ResetUserRc {
|
||||
reset_code,
|
||||
user_pin_new,
|
||||
} => reset_user_rc(reset_code, user_pin_new, card),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_user(
|
||||
user_pin_old: Option<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> Result<()> {
|
||||
let pinpad_modify = card.feature_pinpad_modify();
|
||||
|
||||
let res = if !pinpad_modify {
|
||||
// get current user pin
|
||||
let user_pin1 = util::get_pin(&mut card, user_pin_old, ENTER_USER_PIN)?
|
||||
.expect("this should never be None");
|
||||
|
||||
// verify pin
|
||||
card.verify_user(&user_pin1)?;
|
||||
println!("PIN was accepted by the card.\n");
|
||||
|
||||
let pin_new = match user_pin_new {
|
||||
None => {
|
||||
// ask user for new user pin
|
||||
util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?
|
||||
}
|
||||
Some(path) => load_pin(&path)?,
|
||||
};
|
||||
|
||||
// set new user pin
|
||||
card.change_user_pin(&user_pin1, &pin_new)
|
||||
} else {
|
||||
// set new user pin via pinpad
|
||||
card.change_user_pin_pinpad(&|| {
|
||||
println!("Enter old User PIN on card reader pinpad, then new User PIN (twice).")
|
||||
})
|
||||
};
|
||||
|
||||
match res {
|
||||
Err(err) => {
|
||||
println!("\nFailed to change the User PIN!");
|
||||
println!("{err:?}");
|
||||
print_gnuk_note(err, &card)?;
|
||||
}
|
||||
Ok(_) => println!("\nUser PIN has been set."),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_admin(
|
||||
admin_pin_old: Option<PathBuf>,
|
||||
admin_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> Result<()> {
|
||||
let pinpad_modify = card.feature_pinpad_modify();
|
||||
|
||||
if !pinpad_modify {
|
||||
// get current admin pin
|
||||
let admin_pin1 = util::get_pin(&mut card, admin_pin_old, ENTER_ADMIN_PIN)?
|
||||
.expect("this should never be None");
|
||||
|
||||
// verify pin
|
||||
card.verify_admin(&admin_pin1)?;
|
||||
// (Verifying the PIN here fixes this class of problems:
|
||||
// https://developers.yubico.com/PGP/PGP_PIN_Change_Behavior.html
|
||||
// It is also just generally more user friendly than failing later)
|
||||
println!("PIN was accepted by the card.\n");
|
||||
|
||||
let pin_new = match admin_pin_new {
|
||||
None => {
|
||||
// ask user for new admin pin
|
||||
util::input_pin_twice("Enter new Admin PIN: ", "Repeat the new Admin PIN: ")?
|
||||
}
|
||||
Some(path) => load_pin(&path)?,
|
||||
};
|
||||
|
||||
// set new admin pin
|
||||
card.change_admin_pin(&admin_pin1, &pin_new)?;
|
||||
} else {
|
||||
// set new admin pin via pinpad
|
||||
card.change_admin_pin_pinpad(&|| {
|
||||
println!("Enter old Admin PIN on card reader pinpad, then new Admin PIN (twice).")
|
||||
})?;
|
||||
};
|
||||
|
||||
println!("\nAdmin PIN has been set.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_user(
|
||||
admin_pin: Option<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> Result<()> {
|
||||
// verify admin pin
|
||||
match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? {
|
||||
Some(admin_pin) => {
|
||||
// verify pin
|
||||
card.verify_admin(&admin_pin)?;
|
||||
}
|
||||
None => {
|
||||
card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
|
||||
}
|
||||
}
|
||||
println!("PIN was accepted by the card.\n");
|
||||
|
||||
// ask user for new user pin
|
||||
let pin = match user_pin_new {
|
||||
None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
|
||||
Some(path) => load_pin(&path)?,
|
||||
};
|
||||
|
||||
let res = if let Some(mut admin) = card.admin_card() {
|
||||
admin.reset_user_pin(&pin)
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Failed to use card in admin-mode."));
|
||||
};
|
||||
|
||||
match res {
|
||||
Err(err) => {
|
||||
println!("\nFailed to change the User PIN!");
|
||||
print_gnuk_note(err, &card)?;
|
||||
}
|
||||
Ok(_) => println!("\nUser PIN has been set."),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_reset(
|
||||
admin_pin: Option<PathBuf>,
|
||||
reset_code: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> Result<()> {
|
||||
// verify admin pin
|
||||
match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? {
|
||||
Some(admin_pin) => {
|
||||
// verify pin
|
||||
card.verify_admin(&admin_pin)?;
|
||||
}
|
||||
None => {
|
||||
card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
|
||||
}
|
||||
}
|
||||
println!("PIN was accepted by the card.\n");
|
||||
|
||||
// ask user for new resetting code
|
||||
let code = match reset_code {
|
||||
None => util::input_pin_twice(
|
||||
"Enter new resetting code: ",
|
||||
"Repeat the new resetting code: ",
|
||||
)?,
|
||||
Some(path) => load_pin(&path)?,
|
||||
};
|
||||
|
||||
if let Some(mut admin) = card.admin_card() {
|
||||
admin.set_resetting_code(&code)?;
|
||||
println!("\nResetting code has been set.");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to use card in admin-mode."))
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_user_rc(
|
||||
reset_code: Option<PathBuf>,
|
||||
user_pin_new: Option<PathBuf>,
|
||||
mut card: Card<Transaction>,
|
||||
) -> Result<()> {
|
||||
// reset by presenting resetting code
|
||||
|
||||
let rst = if let Some(path) = reset_code {
|
||||
// load resetting code from file
|
||||
load_pin(&path)?
|
||||
} else {
|
||||
// input resetting code
|
||||
rpassword::prompt_password("Enter resetting code: ")?
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
};
|
||||
|
||||
// ask user for new user pin
|
||||
let pin = match user_pin_new {
|
||||
None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
|
||||
Some(path) => load_pin(&path)?,
|
||||
};
|
||||
|
||||
// reset to new user pin
|
||||
match card.reset_user_pin(&rst, &pin) {
|
||||
Err(err) => {
|
||||
println!("\nFailed to change the User PIN!");
|
||||
print_gnuk_note(err, &card)
|
||||
}
|
||||
Ok(_) => {
|
||||
println!("\nUser PIN has been set.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::types::KeyType;
|
||||
use openpgp_card_sequoia::util::public_key_material_and_fp_to_key;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
use sequoia_openpgp::serialize::SerializeInto;
|
||||
|
||||
use crate::output;
|
||||
use crate::pick_card_for_reading;
|
||||
use crate::util;
|
||||
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct PubkeyCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: Option<String>,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
user_pin: Option<PathBuf>,
|
||||
|
||||
/// User ID to add to the exported certificate representation
|
||||
#[clap(name = "User ID", short = 'u', long = "userid")]
|
||||
user_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn print_pubkey(
|
||||
format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
command: PubkeyCommand,
|
||||
) -> Result<()> {
|
||||
let mut output = output::PublicKey::default();
|
||||
|
||||
let backend = pick_card_for_reading(command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
let ident = card.application_identifier()?.ident();
|
||||
output.ident(ident);
|
||||
|
||||
let user_pin = util::get_pin(&mut card, command.user_pin, crate::ENTER_USER_PIN)?;
|
||||
|
||||
let pkm = card.public_key_material(KeyType::Signing)?;
|
||||
let times = card.key_generation_times()?;
|
||||
let fps = card.fingerprints()?;
|
||||
|
||||
let key_sig = public_key_material_and_fp_to_key(
|
||||
&pkm,
|
||||
KeyType::Signing,
|
||||
times.signature().expect("Signature time is unset"),
|
||||
fps.signature().expect("Signature fingerprint is unset"),
|
||||
)?;
|
||||
|
||||
let mut key_dec = None;
|
||||
if let Ok(pkm) = card.public_key_material(KeyType::Decryption) {
|
||||
if let Some(ts) = times.decryption() {
|
||||
key_dec = Some(public_key_material_and_fp_to_key(
|
||||
&pkm,
|
||||
KeyType::Decryption,
|
||||
ts,
|
||||
fps.decryption().expect("Decryption fingerprint is unset"),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut key_aut = None;
|
||||
if let Ok(pkm) = card.public_key_material(KeyType::Authentication) {
|
||||
if let Some(ts) = times.authentication() {
|
||||
key_aut = Some(public_key_material_and_fp_to_key(
|
||||
&pkm,
|
||||
KeyType::Authentication,
|
||||
ts,
|
||||
fps.authentication()
|
||||
.expect("Authentication fingerprint is unset"),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
let cert = crate::get_cert(
|
||||
&mut card,
|
||||
key_sig,
|
||||
key_dec,
|
||||
key_aut,
|
||||
user_pin.as_deref(),
|
||||
&command.user_ids,
|
||||
&|| println!("Enter User PIN on card reader pinpad."),
|
||||
)?;
|
||||
|
||||
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
||||
output.public_key(armored);
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
Ok(())
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
use crate::util;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct SetIdentityCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
ident: String,
|
||||
|
||||
/// Identity of the virtual card to activate
|
||||
#[clap(name = "identity", value_enum)]
|
||||
id: SetIdentityId,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
pub enum SetIdentityId {
|
||||
#[clap(name = "0")]
|
||||
Zero,
|
||||
#[clap(name = "1")]
|
||||
One,
|
||||
#[clap(name = "2")]
|
||||
Two,
|
||||
}
|
||||
|
||||
impl From<SetIdentityId> for u8 {
|
||||
fn from(id: SetIdentityId) -> Self {
|
||||
match id {
|
||||
SetIdentityId::Zero => 0,
|
||||
SetIdentityId::One => 1,
|
||||
SetIdentityId::Two => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_identity(command: SetIdentityCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let backend = util::open_card(&command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
card.set_identity(u8::from(command.id))?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
use sequoia_openpgp::serialize::stream::{Armorer, Message, Signer};
|
||||
|
||||
use crate::util;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct SignCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
pub ident: String,
|
||||
|
||||
#[clap(
|
||||
name = "User PIN file",
|
||||
short = 'p',
|
||||
long = "user-pin",
|
||||
help = "Optionally, get User PIN from a file"
|
||||
)]
|
||||
pub user_pin: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
name = "detached",
|
||||
short = 'd',
|
||||
long = "detached",
|
||||
help = "Create a detached signature"
|
||||
)]
|
||||
pub detached: bool,
|
||||
|
||||
/// Input file (stdin if unset)
|
||||
#[clap(name = "input")]
|
||||
pub input: Option<PathBuf>,
|
||||
|
||||
/// Output file (stdout if unset)
|
||||
#[clap(name = "output", long = "output", short = 'o')]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn sign(command: SignCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if command.detached {
|
||||
sign_detached(
|
||||
&command.ident,
|
||||
command.user_pin,
|
||||
command.input.as_deref(),
|
||||
command.output.as_deref(),
|
||||
)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Only detached signatures are supported for now").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_detached(
|
||||
ident: &str,
|
||||
pin_file: Option<PathBuf>,
|
||||
input: Option<&Path>,
|
||||
output: Option<&Path>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut input = util::open_or_stdin(input)?;
|
||||
|
||||
let backend = util::open_card(ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
if card.fingerprints()?.signature().is_none() {
|
||||
return Err(anyhow!("Can't sign: this card has no key in the signing slot.").into());
|
||||
}
|
||||
|
||||
let user_pin = util::get_pin(&mut card, pin_file, crate::ENTER_USER_PIN)?;
|
||||
|
||||
let mut sign = util::verify_to_sign(&mut card, user_pin.as_deref())?;
|
||||
let s = sign.signer(&|| println!("Touch confirmation needed for signing"))?;
|
||||
|
||||
let sink = util::open_or_stdout(output)?;
|
||||
|
||||
let message = Armorer::new(Message::new(sink)).build()?;
|
||||
let mut signer = Signer::new(message, s).detached().build()?;
|
||||
|
||||
std::io::copy(&mut input, &mut signer)?;
|
||||
signer.finalize()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,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(())
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::types::KeyType;
|
||||
use openpgp_card_sequoia::{state::Open, Card};
|
||||
|
||||
use crate::output;
|
||||
use crate::pick_card_for_reading;
|
||||
use crate::versioned_output::{OutputBuilder, OutputFormat, OutputVersion};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct StatusCommand {
|
||||
#[clap(
|
||||
name = "card ident",
|
||||
short = 'c',
|
||||
long = "card",
|
||||
help = "Identifier of the card to use"
|
||||
)]
|
||||
pub ident: Option<String>,
|
||||
|
||||
#[clap(
|
||||
name = "verbose",
|
||||
short = 'v',
|
||||
long = "verbose",
|
||||
help = "Use verbose output"
|
||||
)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Print public key material for each key slot
|
||||
#[clap(name = "pkm", short = 'K', long = "public-key-material")]
|
||||
pub pkm: bool,
|
||||
}
|
||||
|
||||
pub fn print_status(
|
||||
format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
command: StatusCommand,
|
||||
) -> Result<()> {
|
||||
let mut output = output::Status::default();
|
||||
output.verbose(command.verbose);
|
||||
output.pkm(command.pkm);
|
||||
|
||||
let backend = pick_card_for_reading(command.ident)?;
|
||||
let mut open: Card<Open> = backend.into();
|
||||
let mut card = open.transaction()?;
|
||||
|
||||
output.ident(card.application_identifier()?.ident());
|
||||
|
||||
let ai = card.application_identifier()?;
|
||||
let version = ai.version().to_be_bytes();
|
||||
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||
|
||||
// Cardholder Name
|
||||
if let Some(name) = card.cardholder_name()? {
|
||||
output.cardholder_name(name);
|
||||
}
|
||||
|
||||
// We ignore the Cardholder "Sex" field, because it's silly and mostly unhelpful
|
||||
|
||||
// Certificate URL
|
||||
let url = card.url()?;
|
||||
if !url.is_empty() {
|
||||
output.certificate_url(url);
|
||||
}
|
||||
|
||||
// Language Preference
|
||||
if let Some(lang) = card.cardholder_related_data()?.lang() {
|
||||
for lang in lang {
|
||||
output.language_preference(format!("{lang}"));
|
||||
}
|
||||
}
|
||||
|
||||
// key information (imported vs. generated on card)
|
||||
let ki = card.key_information().ok().flatten();
|
||||
|
||||
let pws = card.pw_status_bytes()?;
|
||||
|
||||
// information about subkeys
|
||||
|
||||
let fps = card.fingerprints()?;
|
||||
let kgt = card.key_generation_times()?;
|
||||
|
||||
let mut signature_key = output::KeySlotInfo::default();
|
||||
if let Some(fp) = fps.signature() {
|
||||
signature_key.fingerprint(fp.to_spaced_hex());
|
||||
}
|
||||
signature_key.algorithm(format!("{}", card.algorithm_attributes(KeyType::Signing)?));
|
||||
if let Some(kgt) = kgt.signature() {
|
||||
signature_key.creation_time(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if let Some(uif) = card.uif_signing()? {
|
||||
signature_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||
signature_key.touch_features(format!("{}", uif.features()));
|
||||
}
|
||||
if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) {
|
||||
signature_key.status(format!("{ks}"));
|
||||
}
|
||||
|
||||
if let Ok(pkm) = card.public_key_material(KeyType::Signing) {
|
||||
signature_key.public_key_material(pkm.to_string());
|
||||
}
|
||||
|
||||
output.signature_key(signature_key);
|
||||
|
||||
let sst = card.security_support_template()?;
|
||||
output.signature_count(sst.signature_count());
|
||||
|
||||
let mut decryption_key = output::KeySlotInfo::default();
|
||||
if let Some(fp) = fps.decryption() {
|
||||
decryption_key.fingerprint(fp.to_spaced_hex());
|
||||
}
|
||||
decryption_key.algorithm(format!(
|
||||
"{}",
|
||||
card.algorithm_attributes(KeyType::Decryption)?
|
||||
));
|
||||
if let Some(kgt) = kgt.decryption() {
|
||||
decryption_key.creation_time(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if let Some(uif) = card.uif_decryption()? {
|
||||
decryption_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||
decryption_key.touch_features(format!("{}", uif.features()));
|
||||
}
|
||||
if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) {
|
||||
decryption_key.status(format!("{ks}"));
|
||||
}
|
||||
if let Ok(pkm) = card.public_key_material(KeyType::Decryption) {
|
||||
decryption_key.public_key_material(pkm.to_string());
|
||||
}
|
||||
output.decryption_key(decryption_key);
|
||||
|
||||
let mut authentication_key = output::KeySlotInfo::default();
|
||||
if let Some(fp) = fps.authentication() {
|
||||
authentication_key.fingerprint(fp.to_spaced_hex());
|
||||
}
|
||||
authentication_key.algorithm(format!(
|
||||
"{}",
|
||||
card.algorithm_attributes(KeyType::Authentication)?
|
||||
));
|
||||
if let Some(kgt) = kgt.authentication() {
|
||||
authentication_key.creation_time(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if let Some(uif) = card.uif_authentication()? {
|
||||
authentication_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||
authentication_key.touch_features(format!("{}", uif.features()));
|
||||
}
|
||||
if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) {
|
||||
authentication_key.status(format!("{ks}"));
|
||||
}
|
||||
if let Ok(pkm) = card.public_key_material(KeyType::Authentication) {
|
||||
authentication_key.public_key_material(pkm.to_string());
|
||||
}
|
||||
output.authentication_key(authentication_key);
|
||||
|
||||
let mut attestation_key = output::KeySlotInfo::default();
|
||||
if let Ok(Some(fp)) = card.attestation_key_fingerprint() {
|
||||
attestation_key.fingerprint(fp.to_spaced_hex());
|
||||
}
|
||||
if let Ok(Some(algo)) = card.attestation_key_algorithm_attributes() {
|
||||
attestation_key.algorithm(format!("{algo}"));
|
||||
}
|
||||
if let Ok(Some(kgt)) = card.attestation_key_generation_time() {
|
||||
attestation_key.creation_time(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if let Some(uif) = card.uif_attestation()? {
|
||||
attestation_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||
attestation_key.touch_features(format!("{}", uif.features()));
|
||||
}
|
||||
|
||||
// TODO: get public key data for the attestation key from the card
|
||||
// if let Ok(pkm) = card.public_key(KeyType::Attestation) {
|
||||
// attestation_key.public_key_material(pkm.to_string());
|
||||
// }
|
||||
|
||||
// "Key-Ref = 0x81 is reserved for the Attestation key of Yubico"
|
||||
// (see OpenPGP card spec 3.4.1 pg.43)
|
||||
if let Some(ki) = ki.as_ref() {
|
||||
if let Some(n) = (0..ki.num_additional()).find(|&n| ki.additional_ref(n) == 0x81) {
|
||||
let ks = ki.additional_status(n);
|
||||
attestation_key.status(format!("{ks}"));
|
||||
}
|
||||
};
|
||||
|
||||
output.attestation_key(attestation_key);
|
||||
|
||||
// technical details about the card's state
|
||||
output.user_pin_valid_for_only_one_signature(pws.pw1_cds_valid_once());
|
||||
|
||||
output.user_pin_remaining_attempts(pws.err_count_pw1());
|
||||
output.admin_pin_remaining_attempts(pws.err_count_pw3());
|
||||
output.reset_code_remaining_attempts(pws.err_count_rc());
|
||||
|
||||
if let Some(ki) = ki {
|
||||
let num = ki.num_additional();
|
||||
for i in 0..num {
|
||||
// 0x81 is the Yubico attestation key, it has already been used above -> skip here
|
||||
if ki.additional_ref(i) != 0x81 {
|
||||
output.additional_key_status(
|
||||
ki.additional_ref(i),
|
||||
ki.additional_status(i).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(fps) = card.ca_fingerprints() {
|
||||
for fp in fps.iter().flatten() {
|
||||
output.ca_fingerprint(fp.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: print "Login Data"
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use openpgp_card_sequoia::types::CardBackend;
|
||||
use openpgp_card_sequoia::util::make_cert;
|
||||
use openpgp_card_sequoia::PublicKey;
|
||||
use openpgp_card_sequoia::{state::Open, state::Transaction, Card};
|
||||
use sequoia_openpgp::Cert;
|
||||
|
||||
mod cli;
|
||||
mod commands;
|
||||
mod output;
|
||||
mod util;
|
||||
mod versioned_output;
|
||||
|
||||
use cli::OUTPUT_VERSIONS;
|
||||
use versioned_output::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
const ENTER_USER_PIN: &str = "Enter User PIN:";
|
||||
const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:";
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
match cli.cmd {
|
||||
cli::Command::OutputVersions {} => {
|
||||
output_versions(cli.output_version);
|
||||
}
|
||||
cli::Command::List {} => {
|
||||
list_cards(cli.output_format, cli.output_version)?;
|
||||
}
|
||||
cli::Command::Status(cmd) => {
|
||||
commands::status::print_status(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::Info(cmd) => {
|
||||
commands::info::print_info(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::Ssh(cmd) => {
|
||||
commands::ssh::print_ssh(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::Pubkey(cmd) => {
|
||||
commands::pubkey::print_pubkey(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::SetIdentity(cmd) => {
|
||||
commands::set_identity::set_identity(cmd)?;
|
||||
}
|
||||
cli::Command::Decrypt(cmd) => {
|
||||
commands::decrypt::decrypt(cmd)?;
|
||||
}
|
||||
cli::Command::Sign(cmd) => {
|
||||
commands::sign::sign(cmd)?;
|
||||
}
|
||||
cli::Command::Attestation(cmd) => {
|
||||
commands::attestation::attestation(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::FactoryReset(cmd) => {
|
||||
commands::factory_reset::factory_reset(cmd)?;
|
||||
}
|
||||
cli::Command::Admin(cmd) => {
|
||||
commands::admin::admin(cli.output_format, cli.output_version, cmd)?;
|
||||
}
|
||||
cli::Command::Pin(cmd) => {
|
||||
commands::pin::pin(&cmd.ident, cmd.cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn output_versions(chosen: OutputVersion) {
|
||||
for v in OUTPUT_VERSIONS.iter() {
|
||||
if v == &chosen {
|
||||
println!("* {v}");
|
||||
} else {
|
||||
println!(" {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_cards(format: OutputFormat, output_version: OutputVersion) -> Result<()> {
|
||||
let cards = util::cards()?;
|
||||
let mut output = output::List::default();
|
||||
if !cards.is_empty() {
|
||||
for backend in cards {
|
||||
let mut open: Card<Open> = backend.into();
|
||||
|
||||
output.push(open.transaction()?.application_identifier()?.ident());
|
||||
}
|
||||
}
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a card for a read operation. If `ident` is None, and exactly one card
|
||||
/// is plugged in, that card is returned. (We don't This
|
||||
fn pick_card_for_reading(ident: Option<String>) -> Result<Box<dyn CardBackend + Send + Sync>> {
|
||||
if let Some(ident) = ident {
|
||||
Ok(util::open_card(&ident)?)
|
||||
} else {
|
||||
let mut cards = util::cards()?;
|
||||
if cards.len() == 1 {
|
||||
Ok(cards.pop().unwrap())
|
||||
} else if cards.is_empty() {
|
||||
Err(anyhow::anyhow!("No cards found"))
|
||||
} else {
|
||||
// The output version for OutputFormat::Text doesn't matter (it's ignored).
|
||||
list_cards(OutputFormat::Text, OutputVersion::new(0, 0, 0))?;
|
||||
|
||||
println!("Specify which card to use with '--card <card ident>'\n");
|
||||
|
||||
Err(anyhow::anyhow!("Found more than one card"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cert(
|
||||
card: &mut Card<Transaction>,
|
||||
key_sig: PublicKey,
|
||||
key_dec: Option<PublicKey>,
|
||||
key_aut: Option<PublicKey>,
|
||||
user_pin: Option<&[u8]>,
|
||||
user_ids: &[String],
|
||||
prompt: &dyn Fn(),
|
||||
) -> Result<Cert> {
|
||||
if user_pin.is_none() && card.feature_pinpad_verify() {
|
||||
println!(
|
||||
"The public cert will now be generated.\n\n\
|
||||
You will need to enter your User PIN multiple times during this process.\n\n"
|
||||
);
|
||||
}
|
||||
|
||||
make_cert(
|
||||
card,
|
||||
key_sig,
|
||||
key_dec,
|
||||
key_aut,
|
||||
user_pin,
|
||||
prompt,
|
||||
&|| println!("Touch confirmation needed for signing"),
|
||||
user_ids,
|
||||
)
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct AttestationCert {
|
||||
ident: String,
|
||||
attestation_cert: String,
|
||||
}
|
||||
|
||||
impl AttestationCert {
|
||||
pub fn ident(&mut self, ident: String) {
|
||||
self.ident = ident;
|
||||
}
|
||||
|
||||
pub fn attestation_cert(&mut self, cert: String) {
|
||||
self.attestation_cert = cert;
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
Ok(format!(
|
||||
"OpenPGP card {}\n\n{}\n",
|
||||
self.ident, self.attestation_cert,
|
||||
))
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<AttestationCertV0, OpgpCardError> {
|
||||
Ok(AttestationCertV0 {
|
||||
schema_version: AttestationCertV0::VERSION,
|
||||
ident: self.ident.clone(),
|
||||
attestation_cert: self.attestation_cert.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for AttestationCert {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AttestationCertV0 {
|
||||
schema_version: OutputVersion,
|
||||
ident: String,
|
||||
attestation_cert: String,
|
||||
}
|
||||
|
||||
impl OutputVariant for AttestationCertV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct AdminGenerate {
|
||||
ident: String,
|
||||
algorithm: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
impl AdminGenerate {
|
||||
pub fn ident(&mut self, ident: String) {
|
||||
self.ident = ident;
|
||||
}
|
||||
|
||||
pub fn algorithm(&mut self, algorithm: String) {
|
||||
self.algorithm = algorithm;
|
||||
}
|
||||
|
||||
pub fn public_key(&mut self, key: String) {
|
||||
self.public_key = key;
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
// Do not print ident, as the file with the public_key must not contain anything else
|
||||
Ok(self.public_key.to_string())
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<AdminGenerateV0, OpgpCardError> {
|
||||
Ok(AdminGenerateV0 {
|
||||
schema_version: AdminGenerateV0::VERSION,
|
||||
ident: self.ident.clone(),
|
||||
algorithm: self.algorithm.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for AdminGenerate {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AdminGenerateV0 {
|
||||
schema_version: OutputVersion,
|
||||
ident: String,
|
||||
algorithm: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
impl OutputVariant for AdminGenerateV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Info {
|
||||
ident: String,
|
||||
card_version: String,
|
||||
application_id: String,
|
||||
manufacturer_id: String,
|
||||
manufacturer_name: String,
|
||||
card_capabilities: Vec<String>,
|
||||
card_service_data: Vec<String>,
|
||||
extended_length_info: Vec<String>,
|
||||
extended_capabilities: Vec<String>,
|
||||
algorithms: Option<Vec<String>>,
|
||||
firmware_version: Option<String>,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn ident(&mut self, ident: String) {
|
||||
self.ident = ident;
|
||||
}
|
||||
|
||||
pub fn card_version(&mut self, version: String) {
|
||||
self.card_version = version;
|
||||
}
|
||||
|
||||
pub fn application_id(&mut self, id: String) {
|
||||
self.application_id = id;
|
||||
}
|
||||
|
||||
pub fn manufacturer_id(&mut self, id: String) {
|
||||
self.manufacturer_id = id;
|
||||
}
|
||||
|
||||
pub fn manufacturer_name(&mut self, name: String) {
|
||||
self.manufacturer_name = name;
|
||||
}
|
||||
|
||||
pub fn card_capability(&mut self, capability: String) {
|
||||
self.card_capabilities.push(capability);
|
||||
}
|
||||
|
||||
pub fn card_service_data(&mut self, data: String) {
|
||||
self.card_service_data.push(data);
|
||||
}
|
||||
|
||||
pub fn extended_length_info(&mut self, info: String) {
|
||||
self.extended_length_info.push(info);
|
||||
}
|
||||
|
||||
pub fn extended_capability(&mut self, capability: String) {
|
||||
self.extended_capabilities.push(capability);
|
||||
}
|
||||
|
||||
pub fn algorithm(&mut self, algorithm: String) {
|
||||
if let Some(ref mut algos) = self.algorithms {
|
||||
algos.push(algorithm);
|
||||
} else {
|
||||
self.algorithms = Some(vec![algorithm]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn firmware_version(&mut self, version: String) {
|
||||
self.firmware_version = Some(version);
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
let mut s = format!("OpenPGP card {}\n\n", self.ident);
|
||||
|
||||
s.push_str(&format!(
|
||||
"Application Identifier: {}\n",
|
||||
self.application_id
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"Manufacturer [{}]: {}\n\n",
|
||||
self.manufacturer_id, self.manufacturer_name
|
||||
));
|
||||
|
||||
if !self.card_capabilities.is_empty() {
|
||||
s.push_str("Card Capabilities:\n");
|
||||
for c in self.card_capabilities.iter() {
|
||||
s.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
if !self.card_service_data.is_empty() {
|
||||
s.push_str("Card service data:\n");
|
||||
for c in self.card_service_data.iter() {
|
||||
s.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
if !self.extended_length_info.is_empty() {
|
||||
s.push_str("Extended Length Info:\n");
|
||||
for c in self.extended_length_info.iter() {
|
||||
s.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
s.push_str("Extended Capabilities:\n");
|
||||
for c in self.extended_capabilities.iter() {
|
||||
s.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
|
||||
if let Some(algos) = &self.algorithms {
|
||||
s.push_str("Supported algorithms:\n");
|
||||
for c in algos.iter() {
|
||||
s.push_str(&format!("- {c}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
if let Some(v) = &self.firmware_version {
|
||||
s.push_str(&format!("Firmware Version: {v}\n"));
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<InfoV0, OpgpCardError> {
|
||||
Ok(InfoV0 {
|
||||
schema_version: InfoV0::VERSION,
|
||||
ident: self.ident.clone(),
|
||||
card_version: self.card_version.clone(),
|
||||
application_id: self.application_id.clone(),
|
||||
manufacturer_id: self.manufacturer_id.clone(),
|
||||
manufacturer_name: self.manufacturer_name.clone(),
|
||||
card_capabilities: self.card_capabilities.clone(),
|
||||
card_service_data: self.card_service_data.clone(),
|
||||
extended_length_info: self.extended_length_info.clone(),
|
||||
extended_capabilities: self.extended_capabilities.clone(),
|
||||
algorithms: self.algorithms.clone(),
|
||||
firmware_version: self.firmware_version.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for Info {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if InfoV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if InfoV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct InfoV0 {
|
||||
schema_version: OutputVersion,
|
||||
ident: String,
|
||||
card_version: String,
|
||||
application_id: String,
|
||||
manufacturer_id: String,
|
||||
manufacturer_name: String,
|
||||
card_capabilities: Vec<String>,
|
||||
card_service_data: Vec<String>,
|
||||
extended_length_info: Vec<String>,
|
||||
extended_capabilities: Vec<String>,
|
||||
algorithms: Option<Vec<String>>,
|
||||
firmware_version: Option<String>,
|
||||
}
|
||||
|
||||
impl OutputVariant for InfoV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Default, Debug, Serialize)]
|
||||
pub struct List {
|
||||
idents: Vec<String>,
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn push(&mut self, idnet: String) {
|
||||
self.idents.push(idnet);
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
let s = if self.idents.is_empty() {
|
||||
"No OpenPGP cards found.".into()
|
||||
} else {
|
||||
let mut s = "Available OpenPGP cards:\n".to_string();
|
||||
for id in self.idents.iter() {
|
||||
s.push_str(&format!(" {id}\n"));
|
||||
}
|
||||
s
|
||||
};
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<ListV0, OpgpCardError> {
|
||||
Ok(ListV0 {
|
||||
schema_version: ListV0::VERSION,
|
||||
idents: self.idents.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for List {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if ListV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if ListV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ListV0 {
|
||||
schema_version: OutputVersion,
|
||||
idents: Vec<String>,
|
||||
}
|
||||
|
||||
impl OutputVariant for ListV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use crate::OutputVersion;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OpgpCardError {
|
||||
#[error("unknown output version {0}")]
|
||||
UnknownVersion(OutputVersion),
|
||||
|
||||
#[error("failed to serialize JSON output with serde_json")]
|
||||
SerdeJson(#[source] serde_json::Error),
|
||||
|
||||
#[error("failed to serialize YAML output with serde_yaml")]
|
||||
SerdeYaml(#[source] serde_yaml::Error),
|
||||
}
|
||||
|
||||
mod list;
|
||||
pub use list::List;
|
||||
|
||||
mod status;
|
||||
pub use status::{KeySlotInfo, Status};
|
||||
|
||||
mod info;
|
||||
pub use info::Info;
|
||||
|
||||
mod ssh;
|
||||
pub use ssh::Ssh;
|
||||
|
||||
mod pubkey;
|
||||
pub use pubkey::PublicKey;
|
||||
|
||||
mod generate;
|
||||
pub use generate::AdminGenerate;
|
||||
|
||||
mod attest;
|
||||
pub use attest::AttestationCert;
|
|
@ -1,75 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct PublicKey {
|
||||
ident: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn ident(&mut self, ident: String) {
|
||||
self.ident = ident;
|
||||
}
|
||||
|
||||
pub fn public_key(&mut self, key: String) {
|
||||
self.public_key = key;
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
Ok(format!(
|
||||
"OpenPGP card {}\n\n{}\n",
|
||||
self.ident, self.public_key
|
||||
))
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<PublicKeyV0, OpgpCardError> {
|
||||
Ok(PublicKeyV0 {
|
||||
schema_version: PublicKeyV0::VERSION,
|
||||
ident: self.ident.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for PublicKey {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct PublicKeyV0 {
|
||||
schema_version: OutputVersion,
|
||||
ident: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
impl OutputVariant for PublicKeyV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
|
@ -1,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);
|
||||
}
|
|
@ -1,340 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::output::OpgpCardError;
|
||||
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Status {
|
||||
verbose: bool, // show verbose text output?
|
||||
pkm: bool, // include public key material in text output?
|
||||
ident: String,
|
||||
card_version: String,
|
||||
cardholder_name: Option<String>,
|
||||
language_preferences: Vec<String>,
|
||||
certificate_url: Option<String>,
|
||||
signature_key: KeySlotInfo,
|
||||
signature_count: u32,
|
||||
user_pin_valid_for_only_one_signature: bool,
|
||||
decryption_key: KeySlotInfo,
|
||||
authentication_key: KeySlotInfo,
|
||||
attestation_key: Option<KeySlotInfo>,
|
||||
user_pin_remaining_attempts: u8,
|
||||
admin_pin_remaining_attempts: u8,
|
||||
reset_code_remaining_attempts: u8,
|
||||
additional_key_statuses: Vec<(u8, String)>,
|
||||
ca_fingerprints: Vec<String>,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn verbose(&mut self, verbose: bool) {
|
||||
self.verbose = verbose;
|
||||
}
|
||||
|
||||
pub fn pkm(&mut self, pkm: bool) {
|
||||
self.pkm = pkm;
|
||||
}
|
||||
|
||||
pub fn ident(&mut self, ident: String) {
|
||||
self.ident = ident;
|
||||
}
|
||||
|
||||
pub fn card_version(&mut self, card_version: String) {
|
||||
self.card_version = card_version;
|
||||
}
|
||||
|
||||
pub fn cardholder_name(&mut self, card_holder: String) {
|
||||
self.cardholder_name = Some(card_holder);
|
||||
}
|
||||
|
||||
pub fn language_preference(&mut self, pref: String) {
|
||||
self.language_preferences.push(pref);
|
||||
}
|
||||
|
||||
pub fn certificate_url(&mut self, url: String) {
|
||||
self.certificate_url = Some(url);
|
||||
}
|
||||
|
||||
pub fn signature_key(&mut self, key: KeySlotInfo) {
|
||||
self.signature_key = key;
|
||||
}
|
||||
|
||||
pub fn signature_count(&mut self, count: u32) {
|
||||
self.signature_count = count;
|
||||
}
|
||||
|
||||
pub fn user_pin_valid_for_only_one_signature(&mut self, sign_pin_valid_once: bool) {
|
||||
self.user_pin_valid_for_only_one_signature = sign_pin_valid_once;
|
||||
}
|
||||
|
||||
pub fn decryption_key(&mut self, key: KeySlotInfo) {
|
||||
self.decryption_key = key;
|
||||
}
|
||||
|
||||
pub fn authentication_key(&mut self, key: KeySlotInfo) {
|
||||
self.authentication_key = key;
|
||||
}
|
||||
|
||||
pub fn attestation_key(&mut self, key: KeySlotInfo) {
|
||||
self.attestation_key = Some(key);
|
||||
}
|
||||
|
||||
pub fn user_pin_remaining_attempts(&mut self, count: u8) {
|
||||
self.user_pin_remaining_attempts = count;
|
||||
}
|
||||
|
||||
pub fn admin_pin_remaining_attempts(&mut self, count: u8) {
|
||||
self.admin_pin_remaining_attempts = count;
|
||||
}
|
||||
|
||||
pub fn reset_code_remaining_attempts(&mut self, count: u8) {
|
||||
self.reset_code_remaining_attempts = count;
|
||||
}
|
||||
|
||||
pub fn additional_key_status(&mut self, keyref: u8, status: String) {
|
||||
self.additional_key_statuses.push((keyref, status));
|
||||
}
|
||||
|
||||
pub fn ca_fingerprint(&mut self, fingerprint: String) {
|
||||
self.ca_fingerprints.push(fingerprint);
|
||||
}
|
||||
|
||||
fn text(&self) -> Result<String, OpgpCardError> {
|
||||
let mut s = String::new();
|
||||
|
||||
s.push_str(&format!(
|
||||
"OpenPGP card {} (card version {})\n\n",
|
||||
self.ident, self.card_version
|
||||
));
|
||||
|
||||
let mut nl = false;
|
||||
if let Some(name) = &self.cardholder_name {
|
||||
if !name.is_empty() {
|
||||
s.push_str(&format!("Cardholder: {name}\n"));
|
||||
nl = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(url) = &self.certificate_url {
|
||||
if !url.is_empty() {
|
||||
s.push_str(&format!("Certificate URL: {url}\n"));
|
||||
nl = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.language_preferences.is_empty() {
|
||||
let prefs = self.language_preferences.to_vec().join(", ");
|
||||
if !prefs.is_empty() {
|
||||
s.push_str(&format!("Language preferences: '{prefs}'\n"));
|
||||
nl = true;
|
||||
}
|
||||
}
|
||||
|
||||
if nl {
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
s.push_str("Signature key:\n");
|
||||
for line in self.signature_key.format(self.verbose, self.pkm) {
|
||||
s.push_str(&format!(" {line}\n"));
|
||||
}
|
||||
if self.verbose {
|
||||
if self.user_pin_valid_for_only_one_signature {
|
||||
s.push_str(" User PIN presentation is valid for only one signature\n");
|
||||
} else {
|
||||
s.push_str(" User PIN presentation is valid for unlimited signatures\n");
|
||||
}
|
||||
}
|
||||
s.push_str(&format!(" Signatures made: {}\n", self.signature_count));
|
||||
s.push('\n');
|
||||
|
||||
s.push_str("Decryption key:\n");
|
||||
for line in self.decryption_key.format(self.verbose, self.pkm) {
|
||||
s.push_str(&format!(" {line}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
|
||||
s.push_str("Authentication key:\n");
|
||||
for line in self.authentication_key.format(self.verbose, self.pkm) {
|
||||
s.push_str(&format!(" {line}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
|
||||
if self.verbose {
|
||||
if let Some(attestation_key) = &self.attestation_key {
|
||||
if attestation_key.touch_policy.is_some() || attestation_key.algorithm.is_some() {
|
||||
s.push_str("Attestation key:\n");
|
||||
for line in attestation_key.format(self.verbose, self.pkm) {
|
||||
s.push_str(&format!(" {line}\n"));
|
||||
}
|
||||
s.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.push_str(&format!(
|
||||
"Remaining PIN attempts: User: {}, Admin: {}, Reset Code: {}\n",
|
||||
self.user_pin_remaining_attempts,
|
||||
self.admin_pin_remaining_attempts,
|
||||
self.reset_code_remaining_attempts
|
||||
));
|
||||
|
||||
if self.verbose {
|
||||
for (keyref, status) in self.additional_key_statuses.iter() {
|
||||
s.push_str(&format!("Additional key status (#{keyref}): {status}\n"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn v1(&self) -> Result<StatusV0, OpgpCardError> {
|
||||
Ok(StatusV0 {
|
||||
schema_version: StatusV0::VERSION,
|
||||
ident: self.ident.clone(),
|
||||
card_version: self.card_version.clone(),
|
||||
cardholder_name: self.cardholder_name.clone(),
|
||||
language_preferences: self.language_preferences.clone(),
|
||||
certificate_url: self.certificate_url.clone(),
|
||||
signature_key: self.signature_key.clone(),
|
||||
signature_count: self.signature_count,
|
||||
decryption_key: self.decryption_key.clone(),
|
||||
authentication_key: self.authentication_key.clone(),
|
||||
attestation_key: self.attestation_key.clone(),
|
||||
user_pin_valid_for_only_one_signature: self.user_pin_valid_for_only_one_signature,
|
||||
user_pin_remaining_attempts: self.user_pin_remaining_attempts,
|
||||
admin_pin_remaining_attempts: self.admin_pin_remaining_attempts,
|
||||
reset_code_remaining_attempts: self.reset_code_remaining_attempts,
|
||||
additional_key_statuses: self.additional_key_statuses.clone(),
|
||||
// ca_fingerprints: self.ca_fingerprints.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputBuilder for Status {
|
||||
type Err = OpgpCardError;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let result = if StatusV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.json()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeJson)
|
||||
}
|
||||
OutputFormat::Yaml => {
|
||||
let result = if StatusV0::VERSION.is_acceptable_for(&version) {
|
||||
self.v1()?.yaml()
|
||||
} else {
|
||||
return Err(Self::Err::UnknownVersion(version));
|
||||
};
|
||||
result.map_err(Self::Err::SerdeYaml)
|
||||
}
|
||||
OutputFormat::Text => Ok(self.text()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct StatusV0 {
|
||||
schema_version: OutputVersion,
|
||||
ident: String,
|
||||
card_version: String,
|
||||
cardholder_name: Option<String>,
|
||||
language_preferences: Vec<String>,
|
||||
certificate_url: Option<String>,
|
||||
signature_key: KeySlotInfo,
|
||||
signature_count: u32,
|
||||
user_pin_valid_for_only_one_signature: bool,
|
||||
decryption_key: KeySlotInfo,
|
||||
authentication_key: KeySlotInfo,
|
||||
attestation_key: Option<KeySlotInfo>,
|
||||
user_pin_remaining_attempts: u8,
|
||||
admin_pin_remaining_attempts: u8,
|
||||
reset_code_remaining_attempts: u8,
|
||||
additional_key_statuses: Vec<(u8, String)>,
|
||||
// ca_fingerprints: Vec<String>, // TODO: add to JSON output after clarifying the content
|
||||
}
|
||||
|
||||
impl OutputVariant for StatusV0 {
|
||||
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
pub struct KeySlotInfo {
|
||||
fingerprint: Option<String>,
|
||||
creation_time: Option<String>,
|
||||
algorithm: Option<String>,
|
||||
touch_policy: Option<String>,
|
||||
touch_features: Option<String>,
|
||||
status: Option<String>,
|
||||
public_key_material: Option<String>,
|
||||
}
|
||||
|
||||
impl KeySlotInfo {
|
||||
pub fn fingerprint(&mut self, fingerprint: String) {
|
||||
self.fingerprint = Some(fingerprint);
|
||||
}
|
||||
|
||||
pub fn algorithm(&mut self, algorithm: String) {
|
||||
self.algorithm = Some(algorithm);
|
||||
}
|
||||
|
||||
pub fn creation_time(&mut self, created: String) {
|
||||
self.creation_time = Some(created);
|
||||
}
|
||||
|
||||
pub fn touch_policy(&mut self, policy: String) {
|
||||
self.touch_policy = Some(policy);
|
||||
}
|
||||
|
||||
pub fn touch_features(&mut self, features: String) {
|
||||
self.touch_features = Some(features);
|
||||
}
|
||||
|
||||
pub fn status(&mut self, status: String) {
|
||||
self.status = Some(status);
|
||||
}
|
||||
|
||||
pub fn public_key_material(&mut self, material: String) {
|
||||
self.public_key_material = Some(material);
|
||||
}
|
||||
|
||||
fn format(&self, verbose: bool, pkm: bool) -> Vec<String> {
|
||||
let mut lines = vec![];
|
||||
|
||||
if let Some(fp) = &self.fingerprint {
|
||||
lines.push(format!("Fingerprint: {fp}"));
|
||||
} else {
|
||||
lines.push("Fingerprint: [unset]".to_string());
|
||||
}
|
||||
if let Some(ts) = &self.creation_time {
|
||||
lines.push(format!("Creation Time: {ts}"));
|
||||
}
|
||||
if let Some(a) = &self.algorithm {
|
||||
lines.push(format!("Algorithm: {a}"));
|
||||
}
|
||||
|
||||
if verbose {
|
||||
if let Some(policy) = &self.touch_policy {
|
||||
if let Some(features) = &self.touch_features {
|
||||
lines.push(format!("Touch policy: {policy} (features: {features})"));
|
||||
}
|
||||
}
|
||||
if let Some(status) = &self.status {
|
||||
lines.push(format!("Key Status: {status}"));
|
||||
}
|
||||
}
|
||||
if pkm {
|
||||
if let Some(material) = &self.public_key_material {
|
||||
lines.push(format!("Public key material: {material}"));
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021-2022 Heiko Schaefer <heiko@schaefer.name>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use openpgp_card_pcsc::PcscBackend;
|
||||
use openpgp_card_sequoia::state::{Admin, Sign, Transaction, User};
|
||||
use openpgp_card_sequoia::types::{
|
||||
Algo, CardBackend, Curve, EccType, Error, PublicKeyMaterial, StatusBytes,
|
||||
};
|
||||
use openpgp_card_sequoia::Card;
|
||||
|
||||
pub(crate) fn cards() -> Result<Vec<Box<dyn CardBackend + Send + Sync>>, Error> {
|
||||
PcscBackend::cards(None).map(|cards| cards.into_iter().map(|c| c.into()).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn open_card(ident: &str) -> Result<Box<dyn CardBackend + Send + Sync>, Error> {
|
||||
Ok(PcscBackend::open_by_ident(ident, None)?.into())
|
||||
}
|
||||
|
||||
/// Get pin from file. Or via user input, if no file and no pinpad is available.
|
||||
///
|
||||
/// If a pinpad is available, return Null (the pinpad will be used to get access to the card).
|
||||
///
|
||||
/// `msg` is the message to show when asking the user to enter a PIN.
|
||||
pub(crate) fn get_pin(
|
||||
card: &mut Card<Transaction<'_>>,
|
||||
pin_file: Option<PathBuf>,
|
||||
msg: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
if let Some(path) = pin_file {
|
||||
// we have a pin file
|
||||
Ok(Some(load_pin(&path).context(format!(
|
||||
"Failed to read PIN file {}",
|
||||
path.display()
|
||||
))?))
|
||||
} else if !card.feature_pinpad_verify() {
|
||||
// we have no pin file and no pinpad
|
||||
let pin = rpassword::prompt_password(msg).context("Failed to read PIN")?;
|
||||
Ok(Some(pin.into_bytes()))
|
||||
} else {
|
||||
// we have a pinpad
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Let the user input a PIN twice, return PIN if both entries match, error otherwise
|
||||
pub(crate) fn input_pin_twice(msg1: &str, msg2: &str) -> Result<Vec<u8>> {
|
||||
// get new user pin
|
||||
let newpin1 = rpassword::prompt_password(msg1)?;
|
||||
let newpin2 = rpassword::prompt_password(msg2)?;
|
||||
|
||||
if newpin1 != newpin2 {
|
||||
Err(anyhow::anyhow!("PINs do not match."))
|
||||
} else {
|
||||
Ok(newpin1.as_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify_to_user<'app, 'open>(
|
||||
card: &'open mut Card<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<User<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
if let Some(pin) = pin {
|
||||
card.verify_user(pin)?;
|
||||
} else {
|
||||
if !card.feature_pinpad_verify() {
|
||||
return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
|
||||
};
|
||||
|
||||
card.verify_user_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?;
|
||||
}
|
||||
|
||||
card.user_card()
|
||||
.ok_or_else(|| anyhow!("Couldn't get user access").into())
|
||||
}
|
||||
|
||||
pub(crate) fn verify_to_sign<'app, 'open>(
|
||||
card: &'open mut Card<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<Sign<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
if let Some(pin) = pin {
|
||||
card.verify_user_for_signing(pin)?;
|
||||
} else {
|
||||
if !card.feature_pinpad_verify() {
|
||||
return Err(anyhow!("No user PIN file provided, and no pinpad found").into());
|
||||
}
|
||||
card.verify_user_for_signing_pinpad(&|| println!("Enter user PIN on card reader pinpad."))?;
|
||||
}
|
||||
card.signing_card()
|
||||
.ok_or_else(|| anyhow!("Couldn't get sign access").into())
|
||||
}
|
||||
|
||||
pub(crate) fn verify_to_admin<'app, 'open>(
|
||||
card: &'open mut Card<Transaction<'app>>,
|
||||
pin: Option<&[u8]>,
|
||||
) -> Result<Card<Admin<'app, 'open>>, Box<dyn std::error::Error>> {
|
||||
if let Some(pin) = pin {
|
||||
card.verify_admin(pin)?;
|
||||
} else {
|
||||
if !card.feature_pinpad_verify() {
|
||||
return Err(anyhow!("No admin PIN file provided, and no pinpad found").into());
|
||||
}
|
||||
|
||||
card.verify_admin_pinpad(&|| println!("Enter admin PIN on card reader pinpad."))?;
|
||||
}
|
||||
card.admin_card()
|
||||
.ok_or_else(|| anyhow!("Couldn't get admin access").into())
|
||||
}
|
||||
|
||||
pub(crate) fn load_pin(pin_file: &Path) -> Result<Vec<u8>> {
|
||||
let pin = std::fs::read_to_string(pin_file)?;
|
||||
Ok(pin.trim().as_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub(crate) fn open_or_stdin(f: Option<&Path>) -> Result<Box<dyn std::io::Read + Send + Sync>> {
|
||||
match f {
|
||||
Some(f) => Ok(Box::new(
|
||||
std::fs::File::open(f).context("Failed to open input file")?,
|
||||
)),
|
||||
None => Ok(Box::new(std::io::stdin())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn open_or_stdout(f: Option<&Path>) -> Result<Box<dyn std::io::Write + Send + Sync>> {
|
||||
match f {
|
||||
Some(f) => Ok(Box::new(
|
||||
std::fs::File::create(f).context("Failed to open input file")?,
|
||||
)),
|
||||
None => Ok(Box::new(std::io::stdout())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ssh_pubkey(pkm: &PublicKeyMaterial, ident: String) -> Result<sshkeys::PublicKey> {
|
||||
let cardname = format!("opgpcard:{ident}");
|
||||
|
||||
let (key_type, kind) = match pkm {
|
||||
PublicKeyMaterial::R(rsa) => {
|
||||
let key_type = sshkeys::KeyType::from_name("ssh-rsa")?;
|
||||
|
||||
let kind = sshkeys::PublicKeyKind::Rsa(sshkeys::RsaPublicKey {
|
||||
e: rsa.v().to_vec(),
|
||||
n: rsa.n().to_vec(),
|
||||
});
|
||||
|
||||
Ok((key_type, kind))
|
||||
}
|
||||
PublicKeyMaterial::E(ecc) => {
|
||||
if let Algo::Ecc(ecc_attrs) = ecc.algo() {
|
||||
match ecc_attrs.ecc_type() {
|
||||
EccType::EdDSA => {
|
||||
let key_type = sshkeys::KeyType::from_name("ssh-ed25519")?;
|
||||
|
||||
let kind = sshkeys::PublicKeyKind::Ed25519(sshkeys::Ed25519PublicKey {
|
||||
key: ecc.data().to_vec(),
|
||||
sk_application: None,
|
||||
});
|
||||
|
||||
Ok((key_type, kind))
|
||||
}
|
||||
EccType::ECDSA => {
|
||||
let (curve, name) = match ecc_attrs.curve() {
|
||||
Curve::NistP256r1 => Ok((
|
||||
sshkeys::Curve::from_identifier("nistp256")?,
|
||||
"ecdsa-sha2-nistp256",
|
||||
)),
|
||||
Curve::NistP384r1 => Ok((
|
||||
sshkeys::Curve::from_identifier("nistp384")?,
|
||||
"ecdsa-sha2-nistp384",
|
||||
)),
|
||||
Curve::NistP521r1 => Ok((
|
||||
sshkeys::Curve::from_identifier("nistp521")?,
|
||||
"ecdsa-sha2-nistp521",
|
||||
)),
|
||||
_ => Err(anyhow!("Unexpected ECDSA curve {:?}", ecc_attrs.curve())),
|
||||
}?;
|
||||
|
||||
let key_type = sshkeys::KeyType::from_name(name)?;
|
||||
|
||||
let kind = sshkeys::PublicKeyKind::Ecdsa(sshkeys::EcdsaPublicKey {
|
||||
curve,
|
||||
key: ecc.data().to_vec(),
|
||||
sk_application: None,
|
||||
});
|
||||
|
||||
Ok((key_type, kind))
|
||||
}
|
||||
_ => Err(anyhow!("Unexpected EccType {:?}", ecc_attrs.ecc_type())),
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("Unexpected Algo in EccPub {:?}", ecc))
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow!("Unexpected PublicKeyMaterial type {:?}", pkm)),
|
||||
}?;
|
||||
|
||||
let pk = sshkeys::PublicKey {
|
||||
key_type,
|
||||
comment: Some(cardname),
|
||||
kind,
|
||||
};
|
||||
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Return a String representation of an ssh public key, in a form like:
|
||||
/// "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAuTuxILMTvzTIRvaRqqUM3aRDoEBgz/JAoWKsD1ECxy opgpcard:FFFE:43194240"
|
||||
pub(crate) fn get_ssh_pubkey_string(pkm: &PublicKeyMaterial, ident: String) -> Result<String> {
|
||||
let pk = get_ssh_pubkey(pkm, ident)?;
|
||||
|
||||
let mut v = vec![];
|
||||
pk.write(&mut v)?;
|
||||
|
||||
let s = String::from_utf8_lossy(&v).to_string();
|
||||
|
||||
Ok(s.trim().into())
|
||||
}
|
||||
|
||||
/// Gnuk doesn't allow the User password (pw1) to be changed while no
|
||||
/// private key material exists on the card.
|
||||
///
|
||||
/// This fn checks for Gnuk's Status code and the case that no keys exist
|
||||
/// on the card, and prints a note to the user, pointing out that the
|
||||
/// absence of keys on the card might be the reason for the error they get.
|
||||
pub(crate) fn print_gnuk_note(err: Error, card: &Card<Transaction>) -> Result<()> {
|
||||
if matches!(
|
||||
err,
|
||||
Error::CardStatus(StatusBytes::ConditionOfUseNotSatisfied)
|
||||
) {
|
||||
// check if no keys exist on the card
|
||||
let fps = card.fingerprints()?;
|
||||
if fps.signature().is_none() && fps.decryption().is_none() && fps.authentication().is_none()
|
||||
{
|
||||
println!(
|
||||
"\nNOTE: Some cards (e.g. Gnuk) don't allow \
|
||||
User PIN change while no keys exist on the card."
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn pem_encode(data: Vec<u8>) -> String {
|
||||
const PEM_TAG: &str = "CERTIFICATE";
|
||||
|
||||
let pem = pem::Pem {
|
||||
tag: String::from(PEM_TAG),
|
||||
contents: data,
|
||||
};
|
||||
|
||||
pem::encode(&pem)
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use semver::Version;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
||||
pub enum OutputFormat {
|
||||
Json,
|
||||
Text,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OutputVersion {
|
||||
version: Version,
|
||||
}
|
||||
|
||||
impl OutputVersion {
|
||||
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
|
||||
Self {
|
||||
version: Version::new(major, minor, patch),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does this version fulfill the needs of the version that is requested?
|
||||
pub fn is_acceptable_for(&self, wanted: &Self) -> bool {
|
||||
self.version.major == wanted.version.major
|
||||
&& (self.version.minor > wanted.version.minor
|
||||
|| (self.version.minor == wanted.version.minor
|
||||
&& self.version.patch >= wanted.version.patch))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for OutputVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OutputVersion {
|
||||
type Err = semver::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let v = Version::parse(s)?;
|
||||
Ok(Self::new(v.major, v.minor, v.patch))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for &OutputVersion {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.version == other.version
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for OutputVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OutputBuilder {
|
||||
type Err;
|
||||
|
||||
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err>;
|
||||
}
|
||||
|
||||
pub trait OutputVariant: Serialize {
|
||||
const VERSION: OutputVersion;
|
||||
fn json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string_pretty(self)
|
||||
}
|
||||
fn yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
}
|
|
@ -1,238 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
-->
|
||||
|
||||
# Introduction
|
||||
|
||||
This document describes the requirements and acceptance criteria for
|
||||
the `opgpcard` tool, and also how to verify that they are met. This
|
||||
document is meant to be read and understood by all stakeholders, and
|
||||
processed by the [Subplot](https://subplot.tech/) tool, which also
|
||||
generates code to automatically perform the verification.
|
||||
|
||||
## Note about running the tests described here
|
||||
|
||||
The verification scenarios in this document assume the availability of
|
||||
a virtual smart card. Specifically one described in
|
||||
<https://gitlab.com/openpgp-card/virtual-cards>. The
|
||||
`openpgp-card/tools` crate is set up to generate tests only if the
|
||||
environment variable `CARD_BASED_TESTS` is set (to any value),
|
||||
and the `openpgp-card` repository `.gitlab-ci.yml` file is set up to
|
||||
set that environment variable when the repository is tested in GitLab CI.
|
||||
|
||||
This means that if you run `cargo test`, no test code is normally
|
||||
generated from this document. To run the tests locally, outside of
|
||||
GitLab CI, use the script `tools/cargo-test-in-docker`.
|
||||
|
||||
# Acceptance criteria
|
||||
|
||||
These scenarios mainly test the JSON output format of the tool. That
|
||||
format is meant for consumption by other tools, and it is thus more
|
||||
important that it stays stable. The text output that is meant for
|
||||
human consumption may change at will, so it's not worth testing.
|
||||
|
||||
## Smoke test
|
||||
|
||||
_Requirement: The tool can report its version._
|
||||
|
||||
Justification: This is useful mainly to make sure the tool can be run
|
||||
at all. As such, it acts as a simple [smoke
|
||||
test](https://en.wikipedia.org/wiki/Smoke_testing_(software)).
|
||||
However, if this fails, then nothing else has a chance to work.
|
||||
|
||||
Note that this is not in JSON format, as it is output by the `clap`
|
||||
library, and `opgpcard` doesn't affect what it looks like.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --version
|
||||
then stdout matches regex ^opgpcard \d+\.\d+\.\d+$
|
||||
~~~
|
||||
|
||||
## List cards: `opgpcard list`
|
||||
|
||||
_Requirement: The tool lists available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json list
|
||||
then stdout, as JSON, matches embedded file list.json
|
||||
~~~
|
||||
|
||||
~~~{#list.json .file .json}
|
||||
{
|
||||
"idents": ["AFAF:00001234"]
|
||||
}
|
||||
~~~
|
||||
|
||||
## Card status: `opgpcard status`
|
||||
|
||||
_Requirement: The tool shows status of available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json status
|
||||
then stdout, as JSON, matches embedded file status.json
|
||||
~~~
|
||||
|
||||
~~~{#status.json .file .json}
|
||||
{
|
||||
"card_version": "2.0",
|
||||
"ident": "AFAF:00001234"
|
||||
}
|
||||
~~~
|
||||
|
||||
## Card information: `opgpcard info`
|
||||
|
||||
_Requirement: The tool shows information about available cards._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
when I run opgpcard --output-format=json info
|
||||
then stdout, as JSON, matches embedded file info.json
|
||||
~~~
|
||||
|
||||
~~~{#info.json .file .json}
|
||||
{
|
||||
"card_version": "2.0",
|
||||
"application_id": "D276000124 01 0200 AFAF 00001234 0000",
|
||||
"manufacturer_id": "AFAF",
|
||||
"manufacturer_name": "Unknown",
|
||||
"card_service_data": [],
|
||||
"ident": "AFAF:00001234"
|
||||
}
|
||||
~~~
|
||||
|
||||
## Key generation: `opgpcard generate` and `opgpcard decrypt`
|
||||
|
||||
_Requirement: The tool is able to generate keys and use them for decryption._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
given file admin.pin
|
||||
given file user.pin
|
||||
when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin generate --user-pin user.pin --output certfile
|
||||
then file certfile contains "-----BEGIN PGP PUBLIC KEY BLOCK-----"
|
||||
then file certfile contains "-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
given file message
|
||||
when I run sq encrypt message --recipient-cert certfile --output message.enc
|
||||
and I run opgpcard decrypt --card AFAF:00001234 --user-pin user.pin message.enc --output message.dec
|
||||
then files message and message.dec match
|
||||
~~~
|
||||
|
||||
~~~{#admin.pin .file}
|
||||
12345678
|
||||
~~~
|
||||
|
||||
~~~{#user.pin .file}
|
||||
123456
|
||||
~~~
|
||||
|
||||
~~~{#message .file}
|
||||
Hello World!
|
||||
~~~
|
||||
|
||||
## Key generation: `opgpcard generate` and `opgpcard sign`
|
||||
|
||||
_Requirement: The tool is able to generate keys and use them for signing._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
given file admin.pin
|
||||
given file user.pin
|
||||
when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin generate --user-pin user.pin --output certfile
|
||||
then file certfile contains "-----BEGIN PGP PUBLIC KEY BLOCK-----"
|
||||
then file certfile contains "-----END PGP PUBLIC KEY BLOCK-----"
|
||||
|
||||
given file message
|
||||
when I run opgpcard sign message --card AFAF:00001234 --user-pin user.pin --detached --output message.sig
|
||||
when I run sq verify message --detached message.sig --signer-cert certfile
|
||||
then stderr contains "1 good signature."
|
||||
~~~
|
||||
|
||||
## Key import: `opgpcard import` and `opgpcard decrypt`
|
||||
|
||||
_Requirement: The tool is able to import keys and use them for decryption._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
given file admin.pin
|
||||
given file user.pin
|
||||
given file nist256key
|
||||
when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin import nist256key
|
||||
then stdout contains "CCCFFFAAC77C9F9D3BB2D2CA3C93515DA813C03F"
|
||||
then stdout contains "360EC3C59A7D8E51DCE9FA1171858B15EE7F4BCA"
|
||||
then stdout contains "6D186AC7C6761FC22BE07557D2BE4918C44C74D9"
|
||||
|
||||
given file message
|
||||
when I run sq encrypt message --recipient-cert nist256key --output message.enc
|
||||
and I run opgpcard decrypt --card AFAF:00001234 --user-pin user.pin message.enc --output message.dec
|
||||
then files message and message.dec match
|
||||
~~~
|
||||
|
||||
## Key import: `opgpcard import` and `opgpcard sign`
|
||||
|
||||
_Requirement: The tool is able to import keys and use them for signing._
|
||||
|
||||
This is not at all a thorough test, but it exercises the simple happy
|
||||
paths of the subcommand.
|
||||
|
||||
~~~scenario
|
||||
given an installed opgpcard
|
||||
given file admin.pin
|
||||
given file nist256key
|
||||
when I run opgpcard admin --card AFAF:00001234 --admin-pin admin.pin import nist256key
|
||||
then stdout contains "CCCFFFAAC77C9F9D3BB2D2CA3C93515DA813C03F"
|
||||
then stdout contains "360EC3C59A7D8E51DCE9FA1171858B15EE7F4BCA"
|
||||
then stdout contains "6D186AC7C6761FC22BE07557D2BE4918C44C74D9"
|
||||
|
||||
given file user.pin
|
||||
given file message
|
||||
when I run opgpcard sign message --card AFAF:00001234 --user-pin user.pin --detached --output message.sig
|
||||
when I run sq verify message --detached message.sig --signer-cert nist256key
|
||||
then stderr contains "1 good signature."
|
||||
~~~
|
||||
|
||||
~~~{#nist256key .file}
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lHcEYPP/JxMIKoZIzj0DAQcCAwT5a9JIM6BX1zxvFkNr2LMGLyTw72+iXsUZlA8X
|
||||
w3Bn91jVRpSSIITjibHKliS2e2kZlaoHOZvlXmZ3nqOANjV+AAEAzCPG24MzHigZ
|
||||
qyoaNr+7o6u/D8DndXHhsrERqm9cCgcOybQfTklTVCBQMjU2IDxuaXN0MjU2QGV4
|
||||
YW1wbGUub3JnPoiQBBMTCAA4FiEEzM//qsd8n507stLKPJNRXagTwD8FAmDz/ycC
|
||||
GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQPJNRXagTwD+bZAD/fu4NjabH
|
||||
GKHB1dIpqX+opDt8E3RFes58P+p4wh8W+xEBAMcPs6HLYvcLLkqtpV06wKYngPY+
|
||||
Ln/wcpQOagwO+EgfnHsEYPP/JxIIKoZIzj0DAQcCAwTtyP4rOGNlU+Tzpa7UYv5h
|
||||
jR/T9DzMVUntaFhb3Cm0ung7IEGNAOcbgpCx/fdm7BPL+9MJB+qwpsz8bQa4DfnE
|
||||
AwEIBwABALvh9XLpqe1MqwPodYlWKgw4me/tR2FNKmLXPC1gl3g7EAeIeAQYEwgA
|
||||
IBYhBMzP/6rHfJ+dO7LSyjyTUV2oE8A/BQJg8/8nAhsMAAoJEDyTUV2oE8A/SMMA
|
||||
/3DuQU8hb+U9U2nX93bHwpTBQfAONsEn/vUeZ6u4NdX4AP9ABH//08SFfFttiWHm
|
||||
TTAR9e57Rw0DhI/wb6qqWABIyZx3BGDz/zkTCCqGSM49AwEHAgMEJz+bbG6RHQag
|
||||
BoULLuklPRUtQauVTxM9WZZG3PEAnIZuu4LKkHn/JPAN04iSV+K3lBWN+HALVZSV
|
||||
kFweNSOX6gAA/RD5JKvdwS3CofhQY+czewkb8feXGLQIaPS9rIWP7QX4En2IeAQY
|
||||
EwgAIBYhBMzP/6rHfJ+dO7LSyjyTUV2oE8A/BQJg8/85AhsgAAoJEDyTUV2oE8A/
|
||||
CSkA/2WnUoIwtv4ZBhuCpJY/GIFqRJEPgQ7DW1bXTrYsoTehAQD1wDkG0vD6Jnfu
|
||||
QIPHexNllmYakW7WNqu1gobPuNEQyw==
|
||||
=E2Hb
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
~~~
|
|
@ -1,99 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use subplotlib::file::SubplotDataFile;
|
||||
use subplotlib::steplibrary::runcmd::Runcmd;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SubplotContext {}
|
||||
|
||||
impl ContextElement for SubplotContext {}
|
||||
|
||||
#[step]
|
||||
#[context(SubplotContext)]
|
||||
#[context(Runcmd)]
|
||||
fn install_opgpcard(context: &ScenarioContext) {
|
||||
let target_exe = env!("CARGO_BIN_EXE_opgpcard");
|
||||
let target_path = Path::new(target_exe);
|
||||
let target_path = target_path.parent().ok_or("No parent?")?;
|
||||
context.with_mut(
|
||||
|context: &mut Runcmd| {
|
||||
context.prepend_to_path(target_path);
|
||||
Ok(())
|
||||
},
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
|
||||
#[step]
|
||||
#[context(Runcmd)]
|
||||
fn stdout_matches_json_file(context: &ScenarioContext, file: SubplotDataFile) {
|
||||
let expected: Value = serde_json::from_slice(file.data())?;
|
||||
println!("expecting JSON: {:#?}", expected);
|
||||
|
||||
let stdout = context.with(|runcmd: &Runcmd| Ok(runcmd.stdout_as_string()), false)?;
|
||||
let actual: Value = serde_json::from_str(&stdout)?;
|
||||
println!("stdout JSON: {:#?}", actual);
|
||||
|
||||
println!("fuzzy checking JSON values");
|
||||
assert!(json_eq(&actual, &expected));
|
||||
}
|
||||
|
||||
// Fuzzy match JSON values. For objects, anything in expected must be
|
||||
// in actual, but it's OK for there to be extra things.
|
||||
fn json_eq(actual: &Value, expected: &Value) -> bool {
|
||||
match actual {
|
||||
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||
println!("simple value");
|
||||
println!("actual ={:?}", actual);
|
||||
println!("expected={:?}", expected);
|
||||
let eq = actual == expected;
|
||||
println!("simple value eq={}", eq);
|
||||
return eq;
|
||||
}
|
||||
Value::Array(a_values) => {
|
||||
if let Value::Array(e_values) = expected {
|
||||
println!("both actual and equal are arrays");
|
||||
for (a_value, e_value) in a_values.iter().zip(e_values.iter()) {
|
||||
println!("comparing corresponding array elements");
|
||||
if !json_eq(a_value, e_value) {
|
||||
println!("array elements differ");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("arrays match");
|
||||
return true;
|
||||
} else {
|
||||
println!("actual is array, expected is not");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Value::Object(a_obj) => {
|
||||
if let Value::Object(e_obj) = expected {
|
||||
println!("both actual and equal are objects");
|
||||
for key in e_obj.keys() {
|
||||
println!("checking key {}", key);
|
||||
if !a_obj.contains_key(key) {
|
||||
println!("key {} is missing from actual", key);
|
||||
return false;
|
||||
}
|
||||
let a_value = a_obj.get(key).unwrap();
|
||||
let e_value = e_obj.get(key).unwrap();
|
||||
let eq = json_eq(a_value, e_value);
|
||||
println!("values for {} eq={}", key, eq);
|
||||
if !eq {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("objects match");
|
||||
return true;
|
||||
} else {
|
||||
println!("actual is object, expected is not");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
title: "opgpcard acceptance tests"
|
||||
markdowns:
|
||||
- opgpcard.md
|
||||
bindings:
|
||||
- opgpcard.yaml
|
||||
- lib/files.yaml
|
||||
- lib/runcmd.yaml
|
||||
impls:
|
||||
rust:
|
||||
- opgpcard.rs
|
||||
classes:
|
||||
- json
|
|
@ -1,12 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
- given: an installed opgpcard
|
||||
impl:
|
||||
rust:
|
||||
function: install_opgpcard
|
||||
|
||||
- then: "stdout, as JSON, matches embedded file {file:file}"
|
||||
impl:
|
||||
rust:
|
||||
function: stdout_matches_json_file
|
|
@ -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'
|
|
@ -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"));
|
Loading…
Reference in a new issue