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