Merge branch 'liw/json' into 'main'
Add JSON and YAML output to opgpcard Closes #36 See merge request openpgp-card/openpgp-card!9
This commit is contained in:
commit
44c73a154b
23 changed files with 1657 additions and 175 deletions
|
@ -58,6 +58,7 @@ cargo-clippy:
|
||||||
- apt clean
|
- apt clean
|
||||||
script:
|
script:
|
||||||
- rustup component add clippy
|
- rustup component add clippy
|
||||||
|
- rm tools/tests/opgpcard.rs # otherwise build fails
|
||||||
- cargo clippy --verbose --tests -- -D warnings
|
- cargo clippy --verbose --tests -- -D warnings
|
||||||
cache:
|
cache:
|
||||||
# inherit all general cache settings
|
# inherit all general cache settings
|
||||||
|
@ -76,18 +77,22 @@ udeps:
|
||||||
- curl --location --output /tmp/cargo-udeps.tar.gz https://github.com/est31/cargo-udeps/releases/download/v0.1.26/cargo-udeps-v0.1.26-x86_64-unknown-linux-gnu.tar.gz
|
- curl --location --output /tmp/cargo-udeps.tar.gz https://github.com/est31/cargo-udeps/releases/download/v0.1.26/cargo-udeps-v0.1.26-x86_64-unknown-linux-gnu.tar.gz
|
||||||
- tar --extract --verbose --gzip --file /tmp/cargo-udeps.tar.gz --directory /usr/local/bin/ --strip-components=2 ./cargo-udeps-v0.1.26-x86_64-unknown-linux-gnu/cargo-udeps
|
- tar --extract --verbose --gzip --file /tmp/cargo-udeps.tar.gz --directory /usr/local/bin/ --strip-components=2 ./cargo-udeps-v0.1.26-x86_64-unknown-linux-gnu/cargo-udeps
|
||||||
script:
|
script:
|
||||||
|
- rm tools/tests/opgpcard.rs # otherwise build fails
|
||||||
- cargo udeps --workspace --all-features --all-targets
|
- cargo udeps --workspace --all-features --all-targets
|
||||||
cache: [ ]
|
cache: [ ]
|
||||||
|
|
||||||
cargo-test:
|
cargo-test:
|
||||||
stage: test
|
stage: test
|
||||||
image: rust:latest
|
image: registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p /run/user/$UID
|
- mkdir -p /run/user/$UID
|
||||||
- apt update -y -qq
|
- apt update -y -qq
|
||||||
- apt install -y -qq --no-install-recommends git clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev
|
- apt install -y -qq --no-install-recommends git clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev
|
||||||
- apt clean
|
- apt clean
|
||||||
|
- /etc/init.d/pcscd start
|
||||||
|
- su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim
|
||||||
script:
|
script:
|
||||||
|
- touch tools/virtual-card-available
|
||||||
- cargo test
|
- cargo test
|
||||||
cache:
|
cache:
|
||||||
# inherit all general cache settings
|
# inherit all general cache settings
|
||||||
|
@ -97,13 +102,16 @@ cargo-test:
|
||||||
|
|
||||||
cargo-test-debian-bookworm:
|
cargo-test-debian-bookworm:
|
||||||
stage: test
|
stage: test
|
||||||
image: debian:bookworm-slim
|
image: registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir -p /run/user/$UID
|
- mkdir -p /run/user/$UID
|
||||||
- apt update -y -qq
|
- apt update -y -qq
|
||||||
- apt install -y -qq --no-install-recommends git rustc cargo clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev
|
- apt install -y -qq --no-install-recommends git rustc cargo clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev
|
||||||
- apt clean
|
- apt clean
|
||||||
|
- /etc/init.d/pcscd start
|
||||||
|
- su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim
|
||||||
script:
|
script:
|
||||||
|
- touch tools/virtual-card-available
|
||||||
- cargo test
|
- cargo test
|
||||||
cache:
|
cache:
|
||||||
# inherit all general cache settings
|
# inherit all general cache settings
|
||||||
|
|
|
@ -16,6 +16,7 @@ ignore = [
|
||||||
unlicensed = "deny"
|
unlicensed = "deny"
|
||||||
allow = [
|
allow = [
|
||||||
"MIT",
|
"MIT",
|
||||||
|
"MIT-0",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
"BSD-3-Clause",
|
"BSD-3-Clause",
|
||||||
"BSD-2-Clause",
|
"BSD-2-Clause",
|
||||||
|
|
|
@ -23,3 +23,18 @@ anyhow = "1"
|
||||||
clap = { version = "3", features = ["derive"] }
|
clap = { version = "3", features = ["derive"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
log = "0.4"
|
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"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
subplot-build = "0.5.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
fehler = "1.0.0"
|
||||||
|
subplotlib = "0.5.0"
|
||||||
|
|
||||||
|
[package.metadata.cargo-udeps.ignore]
|
||||||
|
development = ["fehler", "subplotlib"]
|
||||||
|
|
27
tools/build.rs
Normal file
27
tools/build.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fir>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Only generate test code from the subplot, if a virtual smart
|
||||||
|
// card is available. This is a kludge until Subplot can do
|
||||||
|
// conditional scenarios
|
||||||
|
// (https://gitlab.com/subplot/subplot/-/issues/20).
|
||||||
|
let flagfile = Path::new("virtual-card-available");
|
||||||
|
if flagfile.exists() {
|
||||||
|
subplot_build::codegen("subplot/opgpcard.subplot")
|
||||||
|
.expect("failed to generate code with Subplot");
|
||||||
|
} else {
|
||||||
|
// 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.
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
tools/cargo-test-in-docker
Executable file
23
tools/cargo-test-in-docker
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Run this to run opgpcard (tools directory) test suite inside a
|
||||||
|
# Docker container with a virtual smartcard running. The test suite
|
||||||
|
# contains tests that run the opgpcard binary and rely on a virtual
|
||||||
|
# smart card to be available.
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
docker run --rm -it \
|
||||||
|
-v root:/root \
|
||||||
|
-v cargo:/cargo \
|
||||||
|
-v $(pwd):/src \
|
||||||
|
registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps sh -c '
|
||||||
|
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 &&
|
||||||
|
if ! [ -e virtual-card-available ]; then rm -f tests/opgpcard.rs; fi &&
|
||||||
|
CARGO_TARGET_DIR=/cargo/ cargo test'
|
62
tools/scripting.md
Normal file
62
tools/scripting.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Scripting around opgpcard
|
||||||
|
|
||||||
|
The `opgpcard` tool can manipulate an OpenPGP smart card (also known
|
||||||
|
as hardware token). There are various manufacturers of these, but
|
||||||
|
Yubikey by Yubico is well known. The 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": [
|
||||||
|
"0006:11339805"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
$
|
||||||
|
~~~
|
||||||
|
|
||||||
|
The structured output is versioned (text output is not), using the
|
||||||
|
field name `schema_version`. The version numbering follows [semantic
|
||||||
|
versionin](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": [
|
||||||
|
"0006:11339805"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
$
|
||||||
|
~~~
|
||||||
|
|
||||||
|
A new field means the minor level in the schema version is
|
||||||
|
incremented.
|
||||||
|
|
||||||
|
|
||||||
|
# Legalese
|
||||||
|
|
||||||
|
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
SPDX-License-Identifier: MIT OR Apache-2.0
|
|
@ -4,6 +4,11 @@
|
||||||
use clap::{AppSettings, Parser};
|
use clap::{AppSettings, Parser};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{OutputFormat, OutputVersion};
|
||||||
|
|
||||||
|
pub const DEFAULT_OUTPUT_VERSION: &str = "0.9.0";
|
||||||
|
pub const OUTPUT_VERSIONS: &[OutputVersion] = &[OutputVersion::new(0, 9, 0)];
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[clap(
|
#[clap(
|
||||||
name = "opgpcard",
|
name = "opgpcard",
|
||||||
|
@ -13,12 +18,24 @@ use std::path::PathBuf;
|
||||||
about = "A tool for inspecting and configuring OpenPGP cards."
|
about = "A tool for inspecting and configuring OpenPGP cards."
|
||||||
)]
|
)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
|
/// Produce output in the chosen format.
|
||||||
|
#[clap(long, value_enum, default_value = "text")]
|
||||||
|
pub output_format: OutputFormat,
|
||||||
|
|
||||||
|
/// Pick output version to use, for non-textual formats.
|
||||||
|
#[clap(long, default_value = DEFAULT_OUTPUT_VERSION)]
|
||||||
|
pub output_version: OutputVersion,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
pub cmd: Command,
|
pub cmd: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
|
/// Show all output versions that are supported. Mark the
|
||||||
|
/// currently chosen one with a star.
|
||||||
|
OutputVersions {},
|
||||||
|
|
||||||
/// Enumerate available OpenPGP cards
|
/// Enumerate available OpenPGP cards
|
||||||
List {},
|
List {},
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
|
||||||
use sequoia_openpgp::Cert;
|
use sequoia_openpgp::Cert;
|
||||||
|
|
||||||
use openpgp_card::algorithm::AlgoSimple;
|
use openpgp_card::algorithm::AlgoSimple;
|
||||||
use openpgp_card::card_do::{Sex, TouchPolicy};
|
use openpgp_card::card_do::TouchPolicy;
|
||||||
use openpgp_card::{CardBackend, KeyType, OpenPgp};
|
use openpgp_card::{CardBackend, KeyType, OpenPgp};
|
||||||
use openpgp_card_sequoia::card::{Admin, Card, Open};
|
use openpgp_card_sequoia::card::{Admin, Card, Open};
|
||||||
use openpgp_card_sequoia::util::{
|
use openpgp_card_sequoia::util::{
|
||||||
|
@ -28,7 +28,12 @@ use crate::util::{load_pin, print_gnuk_note};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod output;
|
||||||
mod util;
|
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_USER_PIN: &str = "Enter User PIN:";
|
||||||
const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:";
|
const ENTER_ADMIN_PIN: &str = "Enter Admin PIN:";
|
||||||
|
@ -39,29 +44,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
|
|
||||||
match cli.cmd {
|
match cli.cmd {
|
||||||
|
cli::Command::OutputVersions {} => {
|
||||||
|
output_versions(cli.output_version);
|
||||||
|
}
|
||||||
cli::Command::List {} => {
|
cli::Command::List {} => {
|
||||||
println!("Available OpenPGP cards:");
|
list_cards(cli.output_format, cli.output_version)?;
|
||||||
list_cards()?;
|
|
||||||
}
|
}
|
||||||
cli::Command::Status {
|
cli::Command::Status {
|
||||||
ident,
|
ident,
|
||||||
verbose,
|
verbose,
|
||||||
pkm,
|
pkm,
|
||||||
} => {
|
} => {
|
||||||
print_status(ident, verbose, pkm)?;
|
print_status(cli.output_format, cli.output_version, ident, verbose, pkm)?;
|
||||||
}
|
}
|
||||||
cli::Command::Info { ident } => {
|
cli::Command::Info { ident } => {
|
||||||
print_info(ident)?;
|
print_info(cli.output_format, cli.output_version, ident)?;
|
||||||
}
|
}
|
||||||
cli::Command::Ssh { ident } => {
|
cli::Command::Ssh { ident } => {
|
||||||
print_ssh(ident)?;
|
print_ssh(cli.output_format, cli.output_version, ident)?;
|
||||||
}
|
}
|
||||||
cli::Command::Pubkey {
|
cli::Command::Pubkey {
|
||||||
ident,
|
ident,
|
||||||
user_pin,
|
user_pin,
|
||||||
user_id,
|
user_id,
|
||||||
} => {
|
} => {
|
||||||
print_pubkey(ident, user_pin, user_id)?;
|
print_pubkey(
|
||||||
|
cli.output_format,
|
||||||
|
cli.output_version,
|
||||||
|
ident,
|
||||||
|
user_pin,
|
||||||
|
user_id,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
cli::Command::SetIdentity { ident, id } => {
|
cli::Command::SetIdentity { ident, id } => {
|
||||||
set_identity(&ident, id)?;
|
set_identity(&ident, id)?;
|
||||||
|
@ -89,15 +102,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
cli::Command::Attestation { cmd } => match cmd {
|
cli::Command::Attestation { cmd } => match cmd {
|
||||||
cli::AttCommand::Cert { ident } => {
|
cli::AttCommand::Cert { ident } => {
|
||||||
|
let mut output = output::AttestationCert::default();
|
||||||
|
|
||||||
let card = pick_card_for_reading(ident)?;
|
let card = pick_card_for_reading(ident)?;
|
||||||
|
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let mut open = Open::new(pgp.transaction()?)?;
|
let mut open = Open::new(pgp.transaction()?)?;
|
||||||
|
output.ident(open.application_identifier()?.ident());
|
||||||
|
|
||||||
if let Ok(ac) = open.attestation_certificate() {
|
if let Ok(ac) = open.attestation_certificate() {
|
||||||
let pem = util::pem_encode(ac);
|
let pem = util::pem_encode(ac);
|
||||||
println!("{}", pem);
|
output.attestation_cert(pem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(cli.output_format, cli.output_version)?);
|
||||||
}
|
}
|
||||||
cli::AttCommand::Generate {
|
cli::AttCommand::Generate {
|
||||||
ident,
|
ident,
|
||||||
|
@ -309,6 +327,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
|
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
|
||||||
|
|
||||||
generate_keys(
|
generate_keys(
|
||||||
|
cli.output_format,
|
||||||
|
cli.output_version,
|
||||||
open,
|
open,
|
||||||
admin_pin.as_deref(),
|
admin_pin.as_deref(),
|
||||||
user_pin.as_deref(),
|
user_pin.as_deref(),
|
||||||
|
@ -558,17 +578,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_cards() -> Result<()> {
|
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 cards = util::cards()?;
|
||||||
|
let mut output = output::List::default();
|
||||||
if !cards.is_empty() {
|
if !cards.is_empty() {
|
||||||
for card in cards {
|
for card in cards {
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let open = Open::new(pgp.transaction()?)?;
|
let open = Open::new(pgp.transaction()?)?;
|
||||||
println!(" {}", open.application_identifier()?.ident());
|
output.push(open.application_identifier()?.ident());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
println!("No OpenPGP cards found.");
|
|
||||||
}
|
}
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -595,7 +625,7 @@ fn pick_card_for_reading(ident: Option<String>) -> Result<Box<dyn CardBackend +
|
||||||
Err(anyhow::anyhow!("No cards found"))
|
Err(anyhow::anyhow!("No cards found"))
|
||||||
} else {
|
} else {
|
||||||
println!("Found {} cards:", cards.len());
|
println!("Found {} cards:", cards.len());
|
||||||
list_cards()?;
|
list_cards(OutputFormat::Text, OutputVersion::new(1, 0, 0))?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("Specify which card to use with '--card <card ident>'");
|
println!("Specify which card to use with '--card <card ident>'");
|
||||||
|
@ -606,7 +636,16 @@ fn pick_card_for_reading(ident: Option<String>) -> Result<Box<dyn CardBackend +
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_status(ident: Option<String>, verbose: bool, pkm: bool) -> Result<()> {
|
fn print_status(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
ident: Option<String>,
|
||||||
|
verbose: bool,
|
||||||
|
pkm: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Status::default();
|
||||||
|
output.verbose(verbose);
|
||||||
|
|
||||||
let card = pick_card_for_reading(ident)?;
|
let card = pick_card_for_reading(ident)?;
|
||||||
|
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
|
@ -615,61 +654,44 @@ fn print_status(ident: Option<String>, verbose: bool, pkm: bool) -> Result<()> {
|
||||||
let ard = pgpt.application_related_data()?;
|
let ard = pgpt.application_related_data()?;
|
||||||
|
|
||||||
let mut open = Open::new(pgpt)?;
|
let mut open = Open::new(pgpt)?;
|
||||||
|
output.ident(open.application_identifier()?.ident());
|
||||||
print!("OpenPGP card {}", open.application_identifier()?.ident());
|
|
||||||
|
|
||||||
let ai = open.application_identifier()?;
|
let ai = open.application_identifier()?;
|
||||||
let version = ai.version().to_be_bytes();
|
let version = ai.version().to_be_bytes();
|
||||||
println!(" (card version {}.{})\n", version[0], version[1]);
|
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||||
|
|
||||||
// card / cardholder metadata
|
// card / cardholder metadata
|
||||||
let crd = open.cardholder_related_data()?;
|
let crd = open.cardholder_related_data()?;
|
||||||
|
|
||||||
// Remember if any cardholder information is printed (if so, we print a newline later)
|
|
||||||
let mut card_holder_output = false;
|
|
||||||
|
|
||||||
if let Some(name) = crd.name() {
|
if let Some(name) = crd.name() {
|
||||||
// FIXME: decoding as utf8 is wrong (the spec defines this field as latin1 encoded)
|
// FIXME: decoding as utf8 is wrong (the spec defines this field as latin1 encoded)
|
||||||
let name = String::from_utf8_lossy(name).to_string();
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
|
||||||
print!("Cardholder: ");
|
// // This field is silly, maybe ignore it?!
|
||||||
|
// if let Some(sex) = crd.sex() {
|
||||||
// This field is silly, maybe ignore it?!
|
// if sex == Sex::Male {
|
||||||
if let Some(sex) = crd.sex() {
|
// print!("Mr. ");
|
||||||
if sex == Sex::Male {
|
// } else if sex == Sex::Female {
|
||||||
print!("Mr. ");
|
// print!("Mrs. ");
|
||||||
} else if sex == Sex::Female {
|
// }
|
||||||
print!("Mrs. ");
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-format name ("last<<first")
|
// re-format name ("last<<first")
|
||||||
let name: Vec<_> = name.split("<<").collect();
|
let name: Vec<_> = name.split("<<").collect();
|
||||||
let name = name.iter().cloned().rev().collect::<Vec<_>>().join(" ");
|
let name = name.iter().cloned().rev().collect::<Vec<_>>().join(" ");
|
||||||
|
|
||||||
println!("{}", name);
|
output.card_holder(name);
|
||||||
|
|
||||||
card_holder_output = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = open.url()?;
|
let url = open.url()?;
|
||||||
if !url.is_empty() {
|
if !url.is_empty() {
|
||||||
println!("URL: {}", url);
|
output.url(url);
|
||||||
card_holder_output = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(lang) = crd.lang() {
|
if let Some(lang) = crd.lang() {
|
||||||
let l = lang
|
for lang in lang {
|
||||||
.iter()
|
output.language_preference(format!("{}", lang));
|
||||||
.map(|l| format!("{}", l))
|
}
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
println!("Language preferences: '{}'", l);
|
|
||||||
card_holder_output = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if card_holder_output {
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// key information (imported vs. generated on card)
|
// key information (imported vs. generated on card)
|
||||||
|
@ -682,152 +704,131 @@ fn print_status(ident: Option<String>, verbose: bool, pkm: bool) -> Result<()> {
|
||||||
let fps = open.fingerprints()?;
|
let fps = open.fingerprints()?;
|
||||||
let kgt = open.key_generation_times()?;
|
let kgt = open.key_generation_times()?;
|
||||||
|
|
||||||
println!("Signature key");
|
let mut signature_key = output::KeySlotInfo::default();
|
||||||
if let Some(fp) = fps.signature() {
|
if let Some(fp) = fps.signature() {
|
||||||
println!(" Fingerprint: {}", fp.to_spaced_hex());
|
signature_key.fingerprint(fp.to_spaced_hex());
|
||||||
}
|
}
|
||||||
println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Signing)?};
|
signature_key.algorithm(format!("{}", open.algorithm_attributes(KeyType::Signing)?));
|
||||||
if let Some(kgt) = kgt.signature() {
|
if let Some(kgt) = kgt.signature() {
|
||||||
println! {" Created: {}", kgt.to_datetime()};
|
signature_key.created(format!("{}", kgt.to_datetime()));
|
||||||
}
|
}
|
||||||
if verbose {
|
if let Some(uif) = ard.uif_pso_cds()? {
|
||||||
if let Some(uif) = ard.uif_pso_cds()? {
|
signature_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
println!(
|
signature_key.touch_features(format!("{}", uif.features()));
|
||||||
" Touch policy: {} [Features: {}]",
|
}
|
||||||
uif.touch_policy(),
|
if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) {
|
||||||
uif.features()
|
signature_key.status(format!("{}", ks));
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(ks) = ki.as_ref().map(|ki| ki.sig_status()) {
|
|
||||||
println!(" Key Status: {}", ks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbose {
|
if pws.pw1_cds_valid_once() {
|
||||||
if pws.pw1_cds_valid_once() {
|
signature_key.pin_valid_once();
|
||||||
println!(" User PIN presentation valid for one signature");
|
|
||||||
} else {
|
|
||||||
println!(" User PIN presentation valid for unlimited signatures");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sst = open.security_support_template()?;
|
|
||||||
println!(" Signatures made: {}", sst.signature_count());
|
|
||||||
|
|
||||||
if pkm {
|
if pkm {
|
||||||
if let Ok(pkm) = open.public_key(KeyType::Signing) {
|
if let Ok(pkm) = open.public_key(KeyType::Signing) {
|
||||||
println! {" Public key material: {}", pkm};
|
signature_key.public_key_material(pkm.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
output.signature_key(signature_key);
|
||||||
println!("Decryption key");
|
|
||||||
|
let sst = open.security_support_template()?;
|
||||||
|
output.signature_count(sst.signature_count());
|
||||||
|
|
||||||
|
let mut decryption_key = output::KeySlotInfo::default();
|
||||||
if let Some(fp) = fps.decryption() {
|
if let Some(fp) = fps.decryption() {
|
||||||
println!(" Fingerprint: {}", fp.to_spaced_hex());
|
decryption_key.fingerprint(fp.to_spaced_hex());
|
||||||
}
|
}
|
||||||
println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Decryption)?};
|
decryption_key.algorithm(format!(
|
||||||
|
"{}",
|
||||||
|
open.algorithm_attributes(KeyType::Decryption)?
|
||||||
|
));
|
||||||
if let Some(kgt) = kgt.decryption() {
|
if let Some(kgt) = kgt.decryption() {
|
||||||
println! {" Created: {}", kgt.to_datetime()};
|
decryption_key.created(format!("{}", kgt.to_datetime()));
|
||||||
}
|
}
|
||||||
if verbose {
|
if let Some(uif) = ard.uif_pso_dec()? {
|
||||||
if let Some(uif) = ard.uif_pso_dec()? {
|
decryption_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
println!(
|
decryption_key.touch_features(format!("{}", uif.features()));
|
||||||
" Touch policy: {} [Features: {}]",
|
}
|
||||||
uif.touch_policy(),
|
if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) {
|
||||||
uif.features()
|
decryption_key.status(format!("{}", ks));
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(ks) = ki.as_ref().map(|ki| ki.dec_status()) {
|
|
||||||
println!(" Key Status: {}", ks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if pkm {
|
if pkm {
|
||||||
if let Ok(pkm) = open.public_key(KeyType::Decryption) {
|
if let Ok(pkm) = open.public_key(KeyType::Decryption) {
|
||||||
println! {" Public key material: {}", pkm};
|
decryption_key.public_key_material(pkm.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
output.decryption_key(decryption_key);
|
||||||
|
|
||||||
println!();
|
let mut authentication_key = output::KeySlotInfo::default();
|
||||||
println!("Authentication key");
|
|
||||||
if let Some(fp) = fps.authentication() {
|
if let Some(fp) = fps.authentication() {
|
||||||
println!(" Fingerprint: {}", fp.to_spaced_hex());
|
authentication_key.fingerprint(fp.to_spaced_hex());
|
||||||
}
|
}
|
||||||
println! {" Algorithm: {}", open.algorithm_attributes(KeyType::Authentication)?};
|
authentication_key.algorithm(format!(
|
||||||
|
"{}",
|
||||||
|
open.algorithm_attributes(KeyType::Authentication)?
|
||||||
|
));
|
||||||
if let Some(kgt) = kgt.authentication() {
|
if let Some(kgt) = kgt.authentication() {
|
||||||
println! {" Created: {}", kgt.to_datetime()};
|
authentication_key.created(format!("{}", kgt.to_datetime()));
|
||||||
}
|
}
|
||||||
if verbose {
|
if let Some(uif) = ard.uif_pso_aut()? {
|
||||||
if let Some(uif) = ard.uif_pso_aut()? {
|
authentication_key.touch_policy(format!("{}", uif.touch_policy()));
|
||||||
println!(
|
authentication_key.touch_features(format!("{}", uif.features()));
|
||||||
" Touch policy: {} [Features: {}]",
|
}
|
||||||
uif.touch_policy(),
|
if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) {
|
||||||
uif.features()
|
authentication_key.status(format!("{}", ks));
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(ks) = ki.as_ref().map(|ki| ki.aut_status()) {
|
|
||||||
println!(" Key Status: {}", ks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if pkm {
|
if pkm {
|
||||||
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||||
println! {" public key material: {}", pkm};
|
authentication_key.public_key_material(pkm.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
output.authentication_key(authentication_key);
|
||||||
|
|
||||||
// technical details about the card's state
|
// technical details about the card's state
|
||||||
|
|
||||||
println!();
|
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());
|
||||||
|
|
||||||
println!(
|
// FIXME: Handle attestation key information as a separate
|
||||||
"Remaining PIN attempts: User: {}, Admin: {}, Reset Code: {}",
|
// KeySlotInfo! Attestation touch information should go into its
|
||||||
pws.err_count_pw1(),
|
// own `Option<KeySlotInfo>`, and (if any information about the
|
||||||
pws.err_count_pw3(),
|
// attestation key exists at all, which is not the case for most
|
||||||
pws.err_count_rc(),
|
// cards) it should be printed as a fourth KeySlot block.
|
||||||
);
|
if let Some(uif) = ard.uif_attestation()? {
|
||||||
|
output.card_touch_policy(uif.touch_policy().to_string());
|
||||||
|
output.card_touch_features(uif.features().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if verbose {
|
if let Some(ki) = ki {
|
||||||
println!();
|
let num = ki.num_additional();
|
||||||
|
for i in 0..num {
|
||||||
if let Some(uif) = ard.uif_attestation()? {
|
output.key_status(ki.additional_ref(i), ki.additional_status(i).to_string());
|
||||||
println!(
|
|
||||||
"Touch policy attestation: {} [Features: {}]",
|
|
||||||
uif.touch_policy(),
|
|
||||||
uif.features()
|
|
||||||
);
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ki) = ki {
|
if let Ok(fps) = ard.ca_fingerprints() {
|
||||||
let num = ki.num_additional();
|
for fp in fps.iter().flatten() {
|
||||||
for i in 0..num {
|
output.ca_fingerprint(fp.to_string());
|
||||||
println!(
|
|
||||||
"Key Status (#{}): {}",
|
|
||||||
ki.additional_ref(i),
|
|
||||||
ki.additional_status(i)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if num > 0 {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(fps) = ard.ca_fingerprints() {
|
|
||||||
for (num, fp) in fps.iter().enumerate() {
|
|
||||||
if let Some(fp) = fp {
|
|
||||||
println!("CA fingerprint {}: {:x?}", num + 1, fp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: print "Login Data"
|
// FIXME: print "Login Data"
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// print metadata information about a card
|
/// print metadata information about a card
|
||||||
fn print_info(ident: Option<String>) -> Result<()> {
|
fn print_info(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
ident: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Info::default();
|
||||||
|
|
||||||
let card = pick_card_for_reading(ident)?;
|
let card = pick_card_for_reading(ident)?;
|
||||||
|
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
|
@ -835,36 +836,44 @@ fn print_info(ident: Option<String>) -> Result<()> {
|
||||||
|
|
||||||
let ai = open.application_identifier()?;
|
let ai = open.application_identifier()?;
|
||||||
|
|
||||||
print!("OpenPGP card {}", ai.ident());
|
output.ident(ai.ident());
|
||||||
|
|
||||||
let version = ai.version().to_be_bytes();
|
let version = ai.version().to_be_bytes();
|
||||||
println!(" (card version {}.{})\n", version[0], version[1]);
|
output.card_version(format!("{}.{}", version[0], version[1]));
|
||||||
|
|
||||||
println!("Application Identifier: {}", ai);
|
output.application_id(ai.to_string());
|
||||||
println!(
|
output.manufacturer_id(format!("{:04X}", ai.manufacturer()));
|
||||||
"Manufacturer [{:04X}]: {}\n",
|
output.manufacturer_name(ai.manufacturer_name().to_string());
|
||||||
ai.manufacturer(),
|
|
||||||
ai.manufacturer_name()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(cc) = open.historical_bytes()?.card_capabilities() {
|
if let Some(cc) = open.historical_bytes()?.card_capabilities() {
|
||||||
println!("Card Capabilities:\n{}", cc);
|
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) = open.historical_bytes()?.card_service_data() {
|
if let Some(csd) = open.historical_bytes()?.card_service_data() {
|
||||||
println!("Card service data:\n{}", csd);
|
output.card_service_data(csd.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(eli) = open.extended_length_information()? {
|
if let Some(eli) = open.extended_length_information()? {
|
||||||
println!("Extended Length Info:\n{}", eli);
|
for line in eli.to_string().lines() {
|
||||||
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.extended_length_info(line.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ec = open.extended_capabilities()?;
|
let ec = open.extended_capabilities()?;
|
||||||
println!("Extended Capabilities:\n{}", ec);
|
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)
|
// Algorithm information (list of supported algorithms)
|
||||||
if let Ok(Some(ai)) = open.algorithm_information() {
|
if let Ok(Some(ai)) = open.algorithm_information() {
|
||||||
println!("Supported algorithms:");
|
for line in ai.to_string().lines() {
|
||||||
println!("{}", ai);
|
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||||
|
output.algorithm(line.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: print KDF info
|
// FIXME: print KDF info
|
||||||
|
@ -872,55 +881,63 @@ fn print_info(ident: Option<String>) -> Result<()> {
|
||||||
// YubiKey specific (?) firmware version
|
// YubiKey specific (?) firmware version
|
||||||
if let Ok(ver) = open.firmware_version() {
|
if let Ok(ver) = open.firmware_version() {
|
||||||
let ver = ver.iter().map(u8::to_string).collect::<Vec<_>>().join(".");
|
let ver = ver.iter().map(u8::to_string).collect::<Vec<_>>().join(".");
|
||||||
|
output.firmware_version(ver);
|
||||||
println!("Firmware Version: {}\n", ver);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_ssh(ident: Option<String>) -> Result<()> {
|
fn print_ssh(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
|
ident: Option<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut output = output::Ssh::default();
|
||||||
|
|
||||||
let card = pick_card_for_reading(ident)?;
|
let card = pick_card_for_reading(ident)?;
|
||||||
|
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let mut open = Open::new(pgp.transaction()?)?;
|
let mut open = Open::new(pgp.transaction()?)?;
|
||||||
|
|
||||||
let ident = open.application_identifier()?.ident();
|
let ident = open.application_identifier()?.ident();
|
||||||
|
output.ident(ident.clone());
|
||||||
println!("OpenPGP card {}", ident);
|
|
||||||
|
|
||||||
// Print fingerprint of authentication subkey
|
// Print fingerprint of authentication subkey
|
||||||
let fps = open.fingerprints()?;
|
let fps = open.fingerprints()?;
|
||||||
|
|
||||||
println!();
|
|
||||||
if let Some(fp) = fps.authentication() {
|
if let Some(fp) = fps.authentication() {
|
||||||
println!("Authentication key fingerprint:\n{}", fp);
|
output.authentication_key_fingerprint(fp.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show authentication subkey as openssh public key string
|
// Show authentication subkey as openssh public key string
|
||||||
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||||
if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) {
|
if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) {
|
||||||
println!();
|
output.ssh_public_key(ssh);
|
||||||
println!("SSH public key:\n{}", ssh);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_pubkey(
|
fn print_pubkey(
|
||||||
|
format: OutputFormat,
|
||||||
|
output_version: OutputVersion,
|
||||||
ident: Option<String>,
|
ident: Option<String>,
|
||||||
user_pin: Option<PathBuf>,
|
user_pin: Option<PathBuf>,
|
||||||
user_ids: Vec<String>,
|
user_ids: Vec<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let mut output = output::PublicKey::default();
|
||||||
|
|
||||||
let card = pick_card_for_reading(ident)?;
|
let card = pick_card_for_reading(ident)?;
|
||||||
|
|
||||||
let mut pgp = OpenPgp::new(card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let mut open = Open::new(pgp.transaction()?)?;
|
let mut open = Open::new(pgp.transaction()?)?;
|
||||||
|
|
||||||
let ident = open.application_identifier()?.ident();
|
let ident = open.application_identifier()?.ident();
|
||||||
|
output.ident(ident);
|
||||||
println!("OpenPGP card {}", ident);
|
|
||||||
|
|
||||||
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
|
let user_pin = util::get_pin(&mut open, user_pin, ENTER_USER_PIN);
|
||||||
|
|
||||||
|
@ -971,8 +988,9 @@ fn print_pubkey(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
||||||
println!("{}", armored);
|
output.public_key(armored);
|
||||||
|
|
||||||
|
println!("{}", output.print(format, output_version)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1096,15 +1114,20 @@ fn get_cert(
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn generate_keys(
|
fn generate_keys(
|
||||||
|
format: OutputFormat,
|
||||||
|
version: OutputVersion,
|
||||||
mut open: Open,
|
mut open: Open,
|
||||||
admin_pin: Option<&[u8]>,
|
admin_pin: Option<&[u8]>,
|
||||||
user_pin: Option<&[u8]>,
|
user_pin: Option<&[u8]>,
|
||||||
output: Option<PathBuf>,
|
output_file: Option<PathBuf>,
|
||||||
decrypt: bool,
|
decrypt: bool,
|
||||||
auth: bool,
|
auth: bool,
|
||||||
algo: Option<String>,
|
algo: Option<String>,
|
||||||
user_ids: Vec<String>,
|
user_ids: Vec<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let mut output = output::AdminGenerate::default();
|
||||||
|
output.ident(open.application_identifier()?.ident());
|
||||||
|
|
||||||
// 1) Interpret the user's choice of algorithm.
|
// 1) Interpret the user's choice of algorithm.
|
||||||
//
|
//
|
||||||
// Unset (None) means that the algorithm that is specified on the card
|
// Unset (None) means that the algorithm that is specified on the card
|
||||||
|
@ -1131,6 +1154,7 @@ fn generate_keys(
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!(" Key generation will be attempted with algo: {:?}", a);
|
log::info!(" Key generation will be attempted with algo: {:?}", a);
|
||||||
|
output.algorithm(format!("{:?}", a));
|
||||||
|
|
||||||
// 2) Then, generate keys on the card.
|
// 2) Then, generate keys on the card.
|
||||||
// We need "admin" access to the card for this).
|
// We need "admin" access to the card for this).
|
||||||
|
@ -1156,10 +1180,12 @@ fn generate_keys(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
let armored = String::from_utf8(cert.armored().to_vec()?)?;
|
||||||
|
output.public_key(armored);
|
||||||
|
|
||||||
// Write armored certificate to the output file (or stdout)
|
// Write armored certificate to the output file (or stdout)
|
||||||
let mut output = util::open_or_stdout(output.as_deref())?;
|
let mut handle = util::open_or_stdout(output_file.as_deref())?;
|
||||||
output.write_all(armored.as_bytes())?;
|
handle.write_all(output.print(format, version)?.as_bytes())?;
|
||||||
|
let _ = handle.write(b"\n")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
75
tools/src/bin/opgpcard/output/attest.rs
Normal file
75
tools/src/bin/opgpcard/output/attest.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct AttestationCert {
|
||||||
|
ident: String,
|
||||||
|
attestation_cert: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttestationCert {
|
||||||
|
pub fn ident(&mut self, ident: String) {
|
||||||
|
self.ident = ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attestation_cert(&mut self, cert: String) {
|
||||||
|
self.attestation_cert = cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
Ok(format!(
|
||||||
|
"OpenPGP card {}\n\n{}\n",
|
||||||
|
self.ident, self.attestation_cert,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<AttestationCertV0, OpgpCardError> {
|
||||||
|
Ok(AttestationCertV0 {
|
||||||
|
schema_version: AttestationCertV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
attestation_cert: self.attestation_cert.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for AttestationCert {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if AttestationCertV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AttestationCertV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
attestation_cert: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for AttestationCertV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
82
tools/src/bin/opgpcard/output/generate.rs
Normal file
82
tools/src/bin/opgpcard/output/generate.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct AdminGenerate {
|
||||||
|
ident: String,
|
||||||
|
algorithm: String,
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminGenerate {
|
||||||
|
pub fn ident(&mut self, ident: String) {
|
||||||
|
self.ident = ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn algorithm(&mut self, algorithm: String) {
|
||||||
|
self.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&mut self, key: String) {
|
||||||
|
self.public_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
Ok(format!(
|
||||||
|
"OpenPGP card {}\n\n{}\n",
|
||||||
|
self.ident, self.public_key,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<AdminGenerateV0, OpgpCardError> {
|
||||||
|
Ok(AdminGenerateV0 {
|
||||||
|
schema_version: AdminGenerateV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
algorithm: self.algorithm.clone(),
|
||||||
|
public_key: self.public_key.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for AdminGenerate {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if AdminGenerateV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AdminGenerateV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
algorithm: String,
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for AdminGenerateV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
189
tools/src/bin/opgpcard/output/info.rs
Normal file
189
tools/src/bin/opgpcard/output/info.rs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Info {
|
||||||
|
ident: String,
|
||||||
|
card_version: String,
|
||||||
|
application_id: String,
|
||||||
|
manufacturer_id: String,
|
||||||
|
manufacturer_name: String,
|
||||||
|
card_capabilities: Vec<String>,
|
||||||
|
card_service_data: String,
|
||||||
|
extended_length_info: Vec<String>,
|
||||||
|
extended_capabilities: Vec<String>,
|
||||||
|
algorithms: Option<Vec<String>>,
|
||||||
|
firmware_version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Info {
|
||||||
|
pub fn ident(&mut self, ident: String) {
|
||||||
|
self.ident = ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn card_version(&mut self, version: String) {
|
||||||
|
self.card_version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn application_id(&mut self, id: String) {
|
||||||
|
self.application_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manufacturer_id(&mut self, id: String) {
|
||||||
|
self.manufacturer_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manufacturer_name(&mut self, name: String) {
|
||||||
|
self.manufacturer_name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn card_capability(&mut self, capability: String) {
|
||||||
|
self.card_capabilities.push(capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn card_service_data(&mut self, data: String) {
|
||||||
|
self.card_service_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extended_length_info(&mut self, info: String) {
|
||||||
|
self.extended_length_info.push(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extended_capability(&mut self, capability: String) {
|
||||||
|
self.extended_capabilities.push(capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn algorithm(&mut self, algorithm: String) {
|
||||||
|
if let Some(ref mut algos) = self.algorithms {
|
||||||
|
algos.push(algorithm);
|
||||||
|
} else {
|
||||||
|
self.algorithms = Some(vec![algorithm]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn firmware_version(&mut self, version: String) {
|
||||||
|
self.firmware_version = Some(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
let mut s = format!("OpenPGP card {}\n\n", self.ident);
|
||||||
|
|
||||||
|
s.push_str(&format!(
|
||||||
|
"Application Identifier: {}\n",
|
||||||
|
self.application_id
|
||||||
|
));
|
||||||
|
s.push_str(&format!(
|
||||||
|
"Manufacturer [{}]: {}\n\n",
|
||||||
|
self.manufacturer_id, self.manufacturer_name
|
||||||
|
));
|
||||||
|
|
||||||
|
if !self.card_capabilities.is_empty() {
|
||||||
|
s.push_str("Card Capabilities:\n");
|
||||||
|
for c in self.card_capabilities.iter() {
|
||||||
|
s.push_str(&format!("- {}\n", c));
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.card_service_data.is_empty() {
|
||||||
|
s.push_str(&format!("Card service data: {}\n", self.card_service_data));
|
||||||
|
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!("- {}\n", c));
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("Extended Capabilities:\n");
|
||||||
|
for c in self.extended_capabilities.iter() {
|
||||||
|
s.push_str(&format!("- {}\n", c));
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
|
||||||
|
if let Some(algos) = &self.algorithms {
|
||||||
|
s.push_str("Supported algorithms:\n");
|
||||||
|
for c in algos.iter() {
|
||||||
|
s.push_str(&format!("- {}\n", c));
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = &self.firmware_version {
|
||||||
|
s.push_str(&format!("Firmware Version: {}\n", v));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<InfoV0, OpgpCardError> {
|
||||||
|
Ok(InfoV0 {
|
||||||
|
schema_version: InfoV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
card_version: self.card_version.clone(),
|
||||||
|
application_id: self.application_id.clone(),
|
||||||
|
manufacturer_id: self.manufacturer_id.clone(),
|
||||||
|
manufacturer_name: self.manufacturer_name.clone(),
|
||||||
|
card_capabilities: self.card_capabilities.clone(),
|
||||||
|
card_service_data: self.card_service_data.clone(),
|
||||||
|
extended_length_info: self.extended_length_info.clone(),
|
||||||
|
extended_capabilities: self.extended_capabilities.clone(),
|
||||||
|
algorithms: self.algorithms.clone(),
|
||||||
|
firmware_version: self.firmware_version.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for Info {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if InfoV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if InfoV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct InfoV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
card_version: String,
|
||||||
|
application_id: String,
|
||||||
|
manufacturer_id: String,
|
||||||
|
manufacturer_name: String,
|
||||||
|
card_capabilities: Vec<String>,
|
||||||
|
card_service_data: String,
|
||||||
|
extended_length_info: Vec<String>,
|
||||||
|
extended_capabilities: Vec<String>,
|
||||||
|
algorithms: Option<Vec<String>>,
|
||||||
|
firmware_version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for InfoV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
74
tools/src/bin/opgpcard/output/list.rs
Normal file
74
tools/src/bin/opgpcard/output/list.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize)]
|
||||||
|
pub struct List {
|
||||||
|
idents: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
pub fn push(&mut self, idnet: String) {
|
||||||
|
self.idents.push(idnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
let s = if self.idents.is_empty() {
|
||||||
|
"No OpenPGP cards found.".into()
|
||||||
|
} else {
|
||||||
|
let mut s = "Available OpenPGP cards:\n".to_string();
|
||||||
|
for id in self.idents.iter() {
|
||||||
|
s.push_str(&format!(" {}\n", id));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<ListV0, OpgpCardError> {
|
||||||
|
Ok(ListV0 {
|
||||||
|
schema_version: ListV0::VERSION,
|
||||||
|
idents: self.idents.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for List {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if ListV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if ListV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ListV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
idents: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for ListV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
37
tools/src/bin/opgpcard/output/mod.rs
Normal file
37
tools/src/bin/opgpcard/output/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use crate::OutputVersion;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum OpgpCardError {
|
||||||
|
#[error("unknown output version {0}")]
|
||||||
|
UnknownVersion(OutputVersion),
|
||||||
|
|
||||||
|
#[error("failed to serialize JSON output with serde_json")]
|
||||||
|
SerdeJson(#[source] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("failed to serialize YAML output with serde_yaml")]
|
||||||
|
SerdeYaml(#[source] serde_yaml::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
mod list;
|
||||||
|
pub use list::List;
|
||||||
|
|
||||||
|
mod status;
|
||||||
|
pub use status::{KeySlotInfo, Status};
|
||||||
|
|
||||||
|
mod info;
|
||||||
|
pub use info::Info;
|
||||||
|
|
||||||
|
mod ssh;
|
||||||
|
pub use ssh::Ssh;
|
||||||
|
|
||||||
|
mod pubkey;
|
||||||
|
pub use pubkey::PublicKey;
|
||||||
|
|
||||||
|
mod generate;
|
||||||
|
pub use generate::AdminGenerate;
|
||||||
|
|
||||||
|
mod attest;
|
||||||
|
pub use attest::AttestationCert;
|
75
tools/src/bin/opgpcard/output/pubkey.rs
Normal file
75
tools/src/bin/opgpcard/output/pubkey.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct PublicKey {
|
||||||
|
ident: String,
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PublicKey {
|
||||||
|
pub fn ident(&mut self, ident: String) {
|
||||||
|
self.ident = ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&mut self, key: String) {
|
||||||
|
self.public_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
Ok(format!(
|
||||||
|
"OpenPGP card {}\n\n{}\n",
|
||||||
|
self.ident, self.public_key
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<PublicKeyV0, OpgpCardError> {
|
||||||
|
Ok(PublicKeyV0 {
|
||||||
|
schema_version: PublicKeyV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
public_key: self.public_key.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for PublicKey {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if PublicKeyV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PublicKeyV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for PublicKeyV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
88
tools/src/bin/opgpcard/output/ssh.rs
Normal file
88
tools/src/bin/opgpcard/output/ssh.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Ssh {
|
||||||
|
ident: String,
|
||||||
|
authentication_key_fingerprint: Option<String>,
|
||||||
|
ssh_public_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ssh {
|
||||||
|
pub fn ident(&mut self, ident: String) {
|
||||||
|
self.ident = ident;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authentication_key_fingerprint(&mut self, fp: String) {
|
||||||
|
self.authentication_key_fingerprint = Some(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ssh_public_key(&mut self, key: String) {
|
||||||
|
self.ssh_public_key = Some(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
let mut s = format!("OpenPGP card {}\n", self.ident);
|
||||||
|
|
||||||
|
if let Some(fp) = &self.authentication_key_fingerprint {
|
||||||
|
s.push_str(&format!("Authentication key fingerprint:\n{}\n", fp));
|
||||||
|
}
|
||||||
|
if let Some(key) = &self.ssh_public_key {
|
||||||
|
s.push_str(&format!("SSH public key:\n{}\n", key));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<SshV0, OpgpCardError> {
|
||||||
|
Ok(SshV0 {
|
||||||
|
schema_version: SshV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
authentication_key_fingerprint: self.authentication_key_fingerprint.clone(),
|
||||||
|
ssh_public_key: self.ssh_public_key.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for Ssh {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if SshV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if SshV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct SshV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
authentication_key_fingerprint: Option<String>,
|
||||||
|
ssh_public_key: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for SshV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
327
tools/src/bin/opgpcard/output/status.rs
Normal file
327
tools/src/bin/opgpcard/output/status.rs
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::output::OpgpCardError;
|
||||||
|
use crate::{OutputBuilder, OutputFormat, OutputVariant, OutputVersion};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Status {
|
||||||
|
verbose: bool,
|
||||||
|
ident: String,
|
||||||
|
card_version: String,
|
||||||
|
card_holder: Option<String>,
|
||||||
|
url: Option<String>,
|
||||||
|
language_preferences: Vec<String>,
|
||||||
|
signature_key: KeySlotInfo,
|
||||||
|
signature_count: u32,
|
||||||
|
decryption_key: KeySlotInfo,
|
||||||
|
authentication_key: KeySlotInfo,
|
||||||
|
user_pin_remaining_attempts: u8,
|
||||||
|
admin_pin_remaining_attempts: u8,
|
||||||
|
reset_code_remaining_attempts: u8,
|
||||||
|
card_touch_policy: String,
|
||||||
|
card_touch_features: String,
|
||||||
|
key_statuses: Vec<(u8, String)>,
|
||||||
|
ca_fingerprints: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn verbose(&mut self, verbose: bool) {
|
||||||
|
self.verbose = verbose;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 card_holder(&mut self, card_holder: String) {
|
||||||
|
self.card_holder = Some(card_holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&mut self, url: String) {
|
||||||
|
self.url = Some(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn language_preference(&mut self, pref: String) {
|
||||||
|
self.language_preferences.push(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 decryption_key(&mut self, key: KeySlotInfo) {
|
||||||
|
self.decryption_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authentication_key(&mut self, key: KeySlotInfo) {
|
||||||
|
self.authentication_key = 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 card_touch_policy(&mut self, policy: String) {
|
||||||
|
self.card_touch_policy = policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn card_touch_features(&mut self, features: String) {
|
||||||
|
self.card_touch_features = features;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_status(&mut self, keyref: u8, status: String) {
|
||||||
|
self.key_statuses.push((keyref, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ca_fingerprint(&mut self, fingerprint: String) {
|
||||||
|
self.ca_fingerprints.push(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&self) -> Result<String, OpgpCardError> {
|
||||||
|
let mut s = String::new();
|
||||||
|
|
||||||
|
s.push_str(&format!(
|
||||||
|
"OpenPGP card {} (card version {})\n\n",
|
||||||
|
self.ident, self.card_version
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut nl = false;
|
||||||
|
if let Some(name) = &self.card_holder {
|
||||||
|
if !name.is_empty() {
|
||||||
|
s.push_str(&format!("Cardholder: {}\n", name));
|
||||||
|
nl = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = &self.url {
|
||||||
|
if !url.is_empty() {
|
||||||
|
s.push_str(&format!("URL: {}\n", url));
|
||||||
|
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: '{}'\n", prefs));
|
||||||
|
nl = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nl {
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("Signature key:\n");
|
||||||
|
for line in self.signature_key.format(self.verbose) {
|
||||||
|
s.push_str(&format!(" {}\n", line));
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
s.push_str(&format!(" {}\n", line));
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
|
||||||
|
s.push_str("Authentication key:\n");
|
||||||
|
for line in self.authentication_key.format(self.verbose) {
|
||||||
|
s.push_str(&format!(" {}\n", line));
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"Touch policy attestation: {}\n",
|
||||||
|
self.card_touch_policy
|
||||||
|
));
|
||||||
|
|
||||||
|
for (keyref, status) in self.key_statuses.iter() {
|
||||||
|
s.push_str(&format!("Key status (#{}): {}\n", keyref, status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1(&self) -> Result<StatusV0, OpgpCardError> {
|
||||||
|
Ok(StatusV0 {
|
||||||
|
schema_version: StatusV0::VERSION,
|
||||||
|
ident: self.ident.clone(),
|
||||||
|
card_version: self.card_version.clone(),
|
||||||
|
card_holder: self.card_holder.clone(),
|
||||||
|
url: self.url.clone(),
|
||||||
|
language_preferences: self.language_preferences.clone(),
|
||||||
|
signature_key: self.signature_key.clone(),
|
||||||
|
signature_count: self.signature_count,
|
||||||
|
decryption_key: self.decryption_key.clone(),
|
||||||
|
authentication_key: self.authentication_key.clone(),
|
||||||
|
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,
|
||||||
|
card_touch_policy: self.card_touch_policy.clone(),
|
||||||
|
card_touch_features: self.card_touch_features.clone(),
|
||||||
|
key_statuses: self.key_statuses.clone(),
|
||||||
|
ca_fingerprints: self.ca_fingerprints.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputBuilder for Status {
|
||||||
|
type Err = OpgpCardError;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let result = if StatusV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.json()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeJson)
|
||||||
|
}
|
||||||
|
OutputFormat::Yaml => {
|
||||||
|
let result = if StatusV0::VERSION.is_acceptable_for(&version) {
|
||||||
|
self.v1()?.yaml()
|
||||||
|
} else {
|
||||||
|
return Err(Self::Err::UnknownVersion(version));
|
||||||
|
};
|
||||||
|
result.map_err(Self::Err::SerdeYaml)
|
||||||
|
}
|
||||||
|
OutputFormat::Text => Ok(self.text()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct StatusV0 {
|
||||||
|
schema_version: OutputVersion,
|
||||||
|
ident: String,
|
||||||
|
card_version: String,
|
||||||
|
card_holder: Option<String>,
|
||||||
|
url: Option<String>,
|
||||||
|
language_preferences: Vec<String>,
|
||||||
|
signature_key: KeySlotInfo,
|
||||||
|
signature_count: u32,
|
||||||
|
decryption_key: KeySlotInfo,
|
||||||
|
authentication_key: KeySlotInfo,
|
||||||
|
user_pin_remaining_attempts: u8,
|
||||||
|
admin_pin_remaining_attempts: u8,
|
||||||
|
reset_code_remaining_attempts: u8,
|
||||||
|
card_touch_policy: String,
|
||||||
|
card_touch_features: String,
|
||||||
|
key_statuses: Vec<(u8, String)>,
|
||||||
|
ca_fingerprints: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVariant for StatusV0 {
|
||||||
|
const VERSION: OutputVersion = OutputVersion::new(0, 9, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Serialize)]
|
||||||
|
pub struct KeySlotInfo {
|
||||||
|
fingerprint: Option<String>,
|
||||||
|
algorithm: Option<String>,
|
||||||
|
created: Option<String>,
|
||||||
|
touch_policy: Option<String>,
|
||||||
|
touch_features: Option<String>,
|
||||||
|
status: Option<String>,
|
||||||
|
pin_valid_once: bool,
|
||||||
|
public_key_material: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeySlotInfo {
|
||||||
|
pub fn fingerprint(&mut self, fingerprint: String) {
|
||||||
|
self.fingerprint = Some(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn algorithm(&mut self, algorithm: String) {
|
||||||
|
self.algorithm = Some(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created(&mut self, created: String) {
|
||||||
|
self.created = 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 pin_valid_once(&mut self) {
|
||||||
|
self.pin_valid_once = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key_material(&mut self, material: String) {
|
||||||
|
self.public_key_material = Some(material);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(&self, verbose: bool) -> Vec<String> {
|
||||||
|
let mut lines = vec![];
|
||||||
|
|
||||||
|
if let Some(fp) = &self.fingerprint {
|
||||||
|
lines.push(format!("Fingerprint: {}", fp));
|
||||||
|
}
|
||||||
|
if let Some(a) = &self.algorithm {
|
||||||
|
lines.push(format!("Algorithm: {}", a));
|
||||||
|
}
|
||||||
|
if let Some(ts) = &self.created {
|
||||||
|
lines.push(format!("Created: {}", ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
if let Some(policy) = &self.touch_policy {
|
||||||
|
if let Some(features) = &self.touch_features {
|
||||||
|
lines.push(format!("Touch policy: {} (features: {})", policy, features));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(status) = &self.status {
|
||||||
|
lines.push(format!("Key Status: {}", status));
|
||||||
|
}
|
||||||
|
if self.pin_valid_once {
|
||||||
|
lines.push("User PIN presentation valid for one signature".into());
|
||||||
|
} else {
|
||||||
|
lines.push("User PIN presentation valid for unlimited signatures".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(material) = &self.public_key_material {
|
||||||
|
lines.push(format!("Public key material: {}", material));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
}
|
81
tools/src/bin/opgpcard/versioned_output.rs
Normal file
81
tools/src/bin/opgpcard/versioned_output.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use semver::Version;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
Json,
|
||||||
|
Text,
|
||||||
|
Yaml,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OutputVersion {
|
||||||
|
version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVersion {
|
||||||
|
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
version: Version::new(major, minor, patch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does this version fulfill the needs of the version that is requested?
|
||||||
|
pub fn is_acceptable_for(&self, wanted: &Self) -> bool {
|
||||||
|
self.version.major == wanted.version.major
|
||||||
|
&& (self.version.minor > wanted.version.minor
|
||||||
|
|| (self.version.minor == wanted.version.minor
|
||||||
|
&& self.version.patch >= wanted.version.patch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for OutputVersion {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for OutputVersion {
|
||||||
|
type Err = semver::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let v = Version::parse(s)?;
|
||||||
|
Ok(Self::new(v.major, v.minor, v.patch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for &OutputVersion {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.version == other.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for OutputVersion {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OutputBuilder {
|
||||||
|
type Err;
|
||||||
|
|
||||||
|
fn print(&self, format: OutputFormat, version: OutputVersion) -> Result<String, Self::Err>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OutputVariant: Serialize {
|
||||||
|
const VERSION: OutputVersion;
|
||||||
|
fn json(&self) -> Result<String, serde_json::Error> {
|
||||||
|
serde_json::to_string_pretty(self)
|
||||||
|
}
|
||||||
|
fn yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||||
|
serde_yaml::to_string(self)
|
||||||
|
}
|
||||||
|
}
|
114
tools/subplot/opgpcard.md
Normal file
114
tools/subplot/opgpcard.md
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
This document describes the requirements and acceptance criteria for
|
||||||
|
the `opgpcard` tool, and also how to verify that they are met. This
|
||||||
|
document is meant to be read and understood by all stakeholders, and
|
||||||
|
processed by the [Subplot](https://subplot.tech/) tool, which also
|
||||||
|
generates code to automatically perform the verification.
|
||||||
|
|
||||||
|
## Note about running the tests described here
|
||||||
|
|
||||||
|
The verification scenarios in this document assume the availability of
|
||||||
|
a virtual smart card. Specifically one described in
|
||||||
|
<https://gitlab.com/openpgp-card/virtual-cards>. The
|
||||||
|
`openpgp-card/tools` crate is set up to generate tests only if the
|
||||||
|
file `tools/virtual-card-available` exists, and the `openpgp-card`
|
||||||
|
repository `.gitlab-ci.yml` file is set up to create that file 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 01 0200 AFAF 00001234 0000",
|
||||||
|
"manufacturer_id": "AFAF",
|
||||||
|
"manufacturer_name": "Unknown",
|
||||||
|
"card_service_data": "",
|
||||||
|
"ident": "AFAF:00001234"
|
||||||
|
}
|
||||||
|
~~~
|
99
tools/subplot/opgpcard.rs
Normal file
99
tools/subplot/opgpcard.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
use subplotlib::file::SubplotDataFile;
|
||||||
|
use subplotlib::steplibrary::runcmd::Runcmd;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct SubplotContext {}
|
||||||
|
|
||||||
|
impl ContextElement for SubplotContext {}
|
||||||
|
|
||||||
|
#[step]
|
||||||
|
#[context(SubplotContext)]
|
||||||
|
#[context(Runcmd)]
|
||||||
|
fn install_opgpcard(context: &ScenarioContext) {
|
||||||
|
let target_exe = env!("CARGO_BIN_EXE_opgpcard");
|
||||||
|
let target_path = Path::new(target_exe);
|
||||||
|
let target_path = target_path.parent().ok_or("No parent?")?;
|
||||||
|
context.with_mut(
|
||||||
|
|context: &mut Runcmd| {
|
||||||
|
context.prepend_to_path(target_path);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[step]
|
||||||
|
#[context(Runcmd)]
|
||||||
|
fn stdout_matches_json_file(context: &ScenarioContext, file: SubplotDataFile) {
|
||||||
|
let expected: Value = serde_json::from_slice(file.data())?;
|
||||||
|
println!("expecting JSON: {:#?}", expected);
|
||||||
|
|
||||||
|
let stdout = context.with(|runcmd: &Runcmd| Ok(runcmd.stdout_as_string()), false)?;
|
||||||
|
let actual: Value = serde_json::from_str(&stdout)?;
|
||||||
|
println!("stdout JSON: {:#?}", actual);
|
||||||
|
|
||||||
|
println!("fuzzy checking JSON values");
|
||||||
|
assert!(json_eq(&actual, &expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzzy match JSON values. For objects, anything in expected must be
|
||||||
|
// in actual, but it's OK for there to be extra things.
|
||||||
|
fn json_eq(actual: &Value, expected: &Value) -> bool {
|
||||||
|
match actual {
|
||||||
|
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||||
|
println!("simple value");
|
||||||
|
println!("actual ={:?}", actual);
|
||||||
|
println!("expected={:?}", expected);
|
||||||
|
let eq = actual == expected;
|
||||||
|
println!("simple value eq={}", eq);
|
||||||
|
return eq;
|
||||||
|
}
|
||||||
|
Value::Array(a_values) => {
|
||||||
|
if let Value::Array(e_values) = expected {
|
||||||
|
println!("both actual and equal are arrays");
|
||||||
|
for (a_value, e_value) in a_values.iter().zip(e_values.iter()) {
|
||||||
|
println!("comparing corresponding array elements");
|
||||||
|
if !json_eq(a_value, e_value) {
|
||||||
|
println!("array elements differ");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("arrays match");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
println!("actual is array, expected is not");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(a_obj) => {
|
||||||
|
if let Value::Object(e_obj) = expected {
|
||||||
|
println!("both actual and equal are objects");
|
||||||
|
for key in e_obj.keys() {
|
||||||
|
println!("checking key {}", key);
|
||||||
|
if !a_obj.contains_key(key) {
|
||||||
|
println!("key {} is missing from actual", key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let a_value = a_obj.get(key).unwrap();
|
||||||
|
let e_value = e_obj.get(key).unwrap();
|
||||||
|
let eq = json_eq(a_value, e_value);
|
||||||
|
println!("values for {} eq={}", key, eq);
|
||||||
|
if !eq {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("objects match");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
println!("actual is object, expected is not");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
tools/subplot/opgpcard.subplot
Normal file
15
tools/subplot/opgpcard.subplot
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
title: "opgpcard acceptance tests"
|
||||||
|
markdowns:
|
||||||
|
- opgpcard.md
|
||||||
|
bindings:
|
||||||
|
- opgpcard.yaml
|
||||||
|
- lib/files.yaml
|
||||||
|
- lib/runcmd.yaml
|
||||||
|
impls:
|
||||||
|
rust:
|
||||||
|
- opgpcard.rs
|
||||||
|
classes:
|
||||||
|
- json
|
12
tools/subplot/opgpcard.yaml
Normal file
12
tools/subplot/opgpcard.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
- given: an installed opgpcard
|
||||||
|
impl:
|
||||||
|
rust:
|
||||||
|
function: install_opgpcard
|
||||||
|
|
||||||
|
- then: "stdout, as JSON, matches embedded file {file:file}"
|
||||||
|
impl:
|
||||||
|
rust:
|
||||||
|
function: stdout_matches_json_file
|
29
tools/subplot/test-in-docker.sh
Executable file
29
tools/subplot/test-in-docker.sh
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Run "cargo test" inside a Docker container with virtual cards
|
||||||
|
# installed and running. This will allow at least rudimentary testing
|
||||||
|
# of actual card functionality of opgpcard.
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
image="registry.gitlab.com/openpgp-card/virtual-cards/smartpgp-builddeps"
|
||||||
|
|
||||||
|
src="$(cd .. && pwd)"
|
||||||
|
|
||||||
|
if ! [ -e Cargo.toml ] || ! grep '^name.*openpgp-card-tools' Cargo.toml; then
|
||||||
|
echo "MUST run this in the root of the openpgp-card-tool crate" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build
|
||||||
|
docker run --rm -t \
|
||||||
|
--volume "cargo:/cargo" \
|
||||||
|
--volume "dotcargo:/root/.cargo" \
|
||||||
|
--volume "$src:/opt" "$image" \
|
||||||
|
bash -c '
|
||||||
|
/etc/init.d/pcscd start && \
|
||||||
|
su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim && \
|
||||||
|
cd /opt/tools && env CARGO_TARGET_DIR=/cargo cargo test'
|
6
tools/tests/opgpcard.rs
Normal file
6
tools/tests/opgpcard.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/opgpcard.rs"));
|
Loading…
Reference in a new issue