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
|
||||
script:
|
||||
- rustup component add clippy
|
||||
- rm tools/tests/opgpcard.rs # otherwise build fails
|
||||
- cargo clippy --verbose --tests -- -D warnings
|
||||
cache:
|
||||
# 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
|
||||
- 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:
|
||||
- rm tools/tests/opgpcard.rs # otherwise build fails
|
||||
- cargo udeps --workspace --all-features --all-targets
|
||||
cache: [ ]
|
||||
|
||||
cargo-test:
|
||||
stage: test
|
||||
image: rust:latest
|
||||
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
|
||||
- apt clean
|
||||
- /etc/init.d/pcscd start
|
||||
- su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim
|
||||
script:
|
||||
- touch tools/virtual-card-available
|
||||
- cargo test
|
||||
cache:
|
||||
# inherit all general cache settings
|
||||
|
@ -97,13 +102,16 @@ cargo-test:
|
|||
|
||||
cargo-test-debian-bookworm:
|
||||
stage: test
|
||||
image: debian:bookworm-slim
|
||||
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 rustc cargo clang make pkg-config nettle-dev libssl-dev capnproto ca-certificates libpcsclite-dev
|
||||
- apt clean
|
||||
- /etc/init.d/pcscd start
|
||||
- su - -c "sh /home/jcardsim/run-card.sh >/dev/null" jcardsim
|
||||
script:
|
||||
- touch tools/virtual-card-available
|
||||
- cargo test
|
||||
cache:
|
||||
# inherit all general cache settings
|
||||
|
|
|
@ -16,6 +16,7 @@ ignore = [
|
|||
unlicensed = "deny"
|
||||
allow = [
|
||||
"MIT",
|
||||
"MIT-0",
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
"BSD-2-Clause",
|
||||
|
|
|
@ -23,3 +23,18 @@ anyhow = "1"
|
|||
clap = { version = "3", features = ["derive"] }
|
||||
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"
|
||||
|
||||
[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 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)]
|
||||
#[clap(
|
||||
name = "opgpcard",
|
||||
|
@ -13,12 +18,24 @@ use std::path::PathBuf;
|
|||
about = "A tool for inspecting and configuring OpenPGP cards."
|
||||
)]
|
||||
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)]
|
||||
pub cmd: Command,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub enum Command {
|
||||
/// Show all output versions that are supported. Mark the
|
||||
/// currently chosen one with a star.
|
||||
OutputVersions {},
|
||||
|
||||
/// Enumerate available OpenPGP cards
|
||||
List {},
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use sequoia_openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
|
|||
use sequoia_openpgp::Cert;
|
||||
|
||||
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_sequoia::card::{Admin, Card, Open};
|
||||
use openpgp_card_sequoia::util::{
|
||||
|
@ -28,7 +28,12 @@ use crate::util::{load_pin, print_gnuk_note};
|
|||
use std::io::Write;
|
||||
|
||||
mod cli;
|
||||
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:";
|
||||
|
@ -39,29 +44,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let cli = cli::Cli::parse();
|
||||
|
||||
match cli.cmd {
|
||||
cli::Command::OutputVersions {} => {
|
||||
output_versions(cli.output_version);
|
||||
}
|
||||
cli::Command::List {} => {
|
||||
println!("Available OpenPGP cards:");
|
||||
list_cards()?;
|
||||
list_cards(cli.output_format, cli.output_version)?;
|
||||
}
|
||||
cli::Command::Status {
|
||||
ident,
|
||||
verbose,
|
||||
pkm,
|
||||
} => {
|
||||
print_status(ident, verbose, pkm)?;
|
||||
print_status(cli.output_format, cli.output_version, ident, verbose, pkm)?;
|
||||
}
|
||||
cli::Command::Info { ident } => {
|
||||
print_info(ident)?;
|
||||
print_info(cli.output_format, cli.output_version, ident)?;
|
||||
}
|
||||
cli::Command::Ssh { ident } => {
|
||||
print_ssh(ident)?;
|
||||
print_ssh(cli.output_format, cli.output_version, ident)?;
|
||||
}
|
||||
cli::Command::Pubkey {
|
||||
ident,
|
||||
user_pin,
|
||||
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 } => {
|
||||
set_identity(&ident, id)?;
|
||||
|
@ -89,15 +102,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
cli::Command::Attestation { cmd } => match cmd {
|
||||
cli::AttCommand::Cert { ident } => {
|
||||
let mut output = output::AttestationCert::default();
|
||||
|
||||
let card = pick_card_for_reading(ident)?;
|
||||
|
||||
let mut pgp = OpenPgp::new(card);
|
||||
let mut open = Open::new(pgp.transaction()?)?;
|
||||
output.ident(open.application_identifier()?.ident());
|
||||
|
||||
if let Ok(ac) = open.attestation_certificate() {
|
||||
let pem = util::pem_encode(ac);
|
||||
println!("{}", pem);
|
||||
output.attestation_cert(pem);
|
||||
}
|
||||
|
||||
println!("{}", output.print(cli.output_format, cli.output_version)?);
|
||||
}
|
||||
cli::AttCommand::Generate {
|
||||
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);
|
||||
|
||||
generate_keys(
|
||||
cli.output_format,
|
||||
cli.output_version,
|
||||
open,
|
||||
admin_pin.as_deref(),
|
||||
user_pin.as_deref(),
|
||||
|
@ -558,17 +578,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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 mut output = output::List::default();
|
||||
if !cards.is_empty() {
|
||||
for card in cards {
|
||||
let mut pgp = OpenPgp::new(card);
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -595,7 +625,7 @@ fn pick_card_for_reading(ident: Option<String>) -> Result<Box<dyn CardBackend +
|
|||
Err(anyhow::anyhow!("No cards found"))
|
||||
} else {
|
||||
println!("Found {} cards:", cards.len());
|
||||
list_cards()?;
|
||||
list_cards(OutputFormat::Text, OutputVersion::new(1, 0, 0))?;
|
||||
|
||||
println!();
|
||||
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 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 mut open = Open::new(pgpt)?;
|
||||
|
||||
print!("OpenPGP card {}", open.application_identifier()?.ident());
|
||||
output.ident(open.application_identifier()?.ident());
|
||||
|
||||
let ai = open.application_identifier()?;
|
||||
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
|
||||
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() {
|
||||
// FIXME: decoding as utf8 is wrong (the spec defines this field as latin1 encoded)
|
||||
let name = String::from_utf8_lossy(name).to_string();
|
||||
|
||||
print!("Cardholder: ");
|
||||
|
||||
// This field is silly, maybe ignore it?!
|
||||
if let Some(sex) = crd.sex() {
|
||||
if sex == Sex::Male {
|
||||
print!("Mr. ");
|
||||
} else if sex == Sex::Female {
|
||||
print!("Mrs. ");
|
||||
}
|
||||
}
|
||||
// // This field is silly, maybe ignore it?!
|
||||
// if let Some(sex) = crd.sex() {
|
||||
// if sex == Sex::Male {
|
||||
// print!("Mr. ");
|
||||
// } else if sex == Sex::Female {
|
||||
// print!("Mrs. ");
|
||||
// }
|
||||
// }
|
||||
|
||||
// re-format name ("last<<first")
|
||||
let name: Vec<_> = name.split("<<").collect();
|
||||
let name = name.iter().cloned().rev().collect::<Vec<_>>().join(" ");
|
||||
|
||||
println!("{}", name);
|
||||
|
||||
card_holder_output = true;
|
||||
output.card_holder(name);
|
||||
}
|
||||
|
||||
let url = open.url()?;
|
||||
if !url.is_empty() {
|
||||
println!("URL: {}", url);
|
||||
card_holder_output = true;
|
||||
output.url(url);
|
||||
}
|
||||
|
||||
if let Some(lang) = crd.lang() {
|
||||
let l = lang
|
||||
.iter()
|
||||
.map(|l| format!("{}", l))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
println!("Language preferences: '{}'", l);
|
||||
card_holder_output = true;
|
||||
for lang in lang {
|
||||
output.language_preference(format!("{}", lang));
|
||||
}
|
||||
|
||||
if card_holder_output {
|
||||
println!();
|
||||
}
|
||||
|
||||
// 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 kgt = open.key_generation_times()?;
|
||||
|
||||
println!("Signature key");
|
||||
let mut signature_key = output::KeySlotInfo::default();
|
||||
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() {
|
||||
println! {" Created: {}", kgt.to_datetime()};
|
||||
signature_key.created(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if verbose {
|
||||
if let Some(uif) = ard.uif_pso_cds()? {
|
||||
println!(
|
||||
" Touch policy: {} [Features: {}]",
|
||||
uif.touch_policy(),
|
||||
uif.features()
|
||||
);
|
||||
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()) {
|
||||
println!(" Key Status: {}", ks);
|
||||
}
|
||||
signature_key.status(format!("{}", ks));
|
||||
}
|
||||
|
||||
if verbose {
|
||||
if pws.pw1_cds_valid_once() {
|
||||
println!(" User PIN presentation valid for one signature");
|
||||
} else {
|
||||
println!(" User PIN presentation valid for unlimited signatures");
|
||||
signature_key.pin_valid_once();
|
||||
}
|
||||
}
|
||||
|
||||
let sst = open.security_support_template()?;
|
||||
println!(" Signatures made: {}", sst.signature_count());
|
||||
|
||||
if pkm {
|
||||
if let Ok(pkm) = open.public_key(KeyType::Signing) {
|
||||
println! {" Public key material: {}", pkm};
|
||||
signature_key.public_key_material(pkm.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Decryption key");
|
||||
output.signature_key(signature_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() {
|
||||
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() {
|
||||
println! {" Created: {}", kgt.to_datetime()};
|
||||
decryption_key.created(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if verbose {
|
||||
if let Some(uif) = ard.uif_pso_dec()? {
|
||||
println!(
|
||||
" Touch policy: {} [Features: {}]",
|
||||
uif.touch_policy(),
|
||||
uif.features()
|
||||
);
|
||||
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()) {
|
||||
println!(" Key Status: {}", ks);
|
||||
}
|
||||
decryption_key.status(format!("{}", ks));
|
||||
}
|
||||
if pkm {
|
||||
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!();
|
||||
println!("Authentication key");
|
||||
let mut authentication_key = output::KeySlotInfo::default();
|
||||
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() {
|
||||
println! {" Created: {}", kgt.to_datetime()};
|
||||
authentication_key.created(format!("{}", kgt.to_datetime()));
|
||||
}
|
||||
if verbose {
|
||||
if let Some(uif) = ard.uif_pso_aut()? {
|
||||
println!(
|
||||
" Touch policy: {} [Features: {}]",
|
||||
uif.touch_policy(),
|
||||
uif.features()
|
||||
);
|
||||
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()) {
|
||||
println!(" Key Status: {}", ks);
|
||||
}
|
||||
authentication_key.status(format!("{}", ks));
|
||||
}
|
||||
if pkm {
|
||||
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
|
||||
|
||||
println!();
|
||||
|
||||
println!(
|
||||
"Remaining PIN attempts: User: {}, Admin: {}, Reset Code: {}",
|
||||
pws.err_count_pw1(),
|
||||
pws.err_count_pw3(),
|
||||
pws.err_count_rc(),
|
||||
);
|
||||
|
||||
if verbose {
|
||||
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());
|
||||
|
||||
// FIXME: Handle attestation key information as a separate
|
||||
// KeySlotInfo! Attestation touch information should go into its
|
||||
// own `Option<KeySlotInfo>`, and (if any information about the
|
||||
// attestation key exists at all, which is not the case for most
|
||||
// cards) it should be printed as a fourth KeySlot block.
|
||||
if let Some(uif) = ard.uif_attestation()? {
|
||||
println!(
|
||||
"Touch policy attestation: {} [Features: {}]",
|
||||
uif.touch_policy(),
|
||||
uif.features()
|
||||
);
|
||||
println!();
|
||||
output.card_touch_policy(uif.touch_policy().to_string());
|
||||
output.card_touch_features(uif.features().to_string());
|
||||
}
|
||||
|
||||
if let Some(ki) = ki {
|
||||
let num = ki.num_additional();
|
||||
for i in 0..num {
|
||||
println!(
|
||||
"Key Status (#{}): {}",
|
||||
ki.additional_ref(i),
|
||||
ki.additional_status(i)
|
||||
);
|
||||
}
|
||||
|
||||
if num > 0 {
|
||||
println!();
|
||||
output.key_status(ki.additional_ref(i), ki.additional_status(i).to_string());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
for fp in fps.iter().flatten() {
|
||||
output.ca_fingerprint(fp.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: print "Login Data"
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 mut pgp = OpenPgp::new(card);
|
||||
|
@ -835,36 +836,44 @@ fn print_info(ident: Option<String>) -> Result<()> {
|
|||
|
||||
let ai = open.application_identifier()?;
|
||||
|
||||
print!("OpenPGP card {}", ai.ident());
|
||||
output.ident(ai.ident());
|
||||
|
||||
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);
|
||||
println!(
|
||||
"Manufacturer [{:04X}]: {}\n",
|
||||
ai.manufacturer(),
|
||||
ai.manufacturer_name()
|
||||
);
|
||||
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) = 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() {
|
||||
println!("Card service data:\n{}", csd);
|
||||
output.card_service_data(csd.to_string());
|
||||
}
|
||||
|
||||
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()?;
|
||||
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)
|
||||
if let Ok(Some(ai)) = open.algorithm_information() {
|
||||
println!("Supported algorithms:");
|
||||
println!("{}", ai);
|
||||
for line in ai.to_string().lines() {
|
||||
let line = line.strip_prefix("- ").unwrap_or(line);
|
||||
output.algorithm(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: print KDF info
|
||||
|
@ -872,55 +881,63 @@ fn print_info(ident: Option<String>) -> Result<()> {
|
|||
// YubiKey specific (?) firmware version
|
||||
if let Ok(ver) = open.firmware_version() {
|
||||
let ver = ver.iter().map(u8::to_string).collect::<Vec<_>>().join(".");
|
||||
|
||||
println!("Firmware Version: {}\n", ver);
|
||||
output.firmware_version(ver);
|
||||
}
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
|
||||
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 mut pgp = OpenPgp::new(card);
|
||||
let mut open = Open::new(pgp.transaction()?)?;
|
||||
|
||||
let ident = open.application_identifier()?.ident();
|
||||
|
||||
println!("OpenPGP card {}", ident);
|
||||
output.ident(ident.clone());
|
||||
|
||||
// Print fingerprint of authentication subkey
|
||||
let fps = open.fingerprints()?;
|
||||
|
||||
println!();
|
||||
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
|
||||
if let Ok(pkm) = open.public_key(KeyType::Authentication) {
|
||||
if let Ok(ssh) = util::get_ssh_pubkey_string(&pkm, ident) {
|
||||
println!();
|
||||
println!("SSH public key:\n{}", ssh);
|
||||
output.ssh_public_key(ssh);
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_pubkey(
|
||||
format: OutputFormat,
|
||||
output_version: OutputVersion,
|
||||
ident: Option<String>,
|
||||
user_pin: Option<PathBuf>,
|
||||
user_ids: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let mut output = output::PublicKey::default();
|
||||
|
||||
let card = pick_card_for_reading(ident)?;
|
||||
|
||||
let mut pgp = OpenPgp::new(card);
|
||||
let mut open = Open::new(pgp.transaction()?)?;
|
||||
|
||||
let ident = open.application_identifier()?.ident();
|
||||
|
||||
println!("OpenPGP card {}", ident);
|
||||
output.ident(ident);
|
||||
|
||||
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()?)?;
|
||||
println!("{}", armored);
|
||||
output.public_key(armored);
|
||||
|
||||
println!("{}", output.print(format, output_version)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1096,15 +1114,20 @@ fn get_cert(
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_keys(
|
||||
format: OutputFormat,
|
||||
version: OutputVersion,
|
||||
mut open: Open,
|
||||
admin_pin: Option<&[u8]>,
|
||||
user_pin: Option<&[u8]>,
|
||||
output: Option<PathBuf>,
|
||||
output_file: Option<PathBuf>,
|
||||
decrypt: bool,
|
||||
auth: bool,
|
||||
algo: Option<String>,
|
||||
user_ids: Vec<String>,
|
||||
) -> Result<()> {
|
||||
let mut output = output::AdminGenerate::default();
|
||||
output.ident(open.application_identifier()?.ident());
|
||||
|
||||
// 1) Interpret the user's choice of algorithm.
|
||||
//
|
||||
// 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);
|
||||
output.algorithm(format!("{:?}", a));
|
||||
|
||||
// 2) Then, generate keys on the card.
|
||||
// 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()?)?;
|
||||
output.public_key(armored);
|
||||
|
||||
// Write armored certificate to the output file (or stdout)
|
||||
let mut output = util::open_or_stdout(output.as_deref())?;
|
||||
output.write_all(armored.as_bytes())?;
|
||||
let mut handle = util::open_or_stdout(output_file.as_deref())?;
|
||||
handle.write_all(output.print(format, version)?.as_bytes())?;
|
||||
let _ = handle.write(b"\n")?;
|
||||
|
||||
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