Add the crate card-functionality, which implements a test suite to test the openpgp-card crate against a set of OpenPGP cards

This commit is contained in:
Heiko Schaefer 2021-07-16 17:26:10 +02:00
parent 4b7b4a2ab6
commit 77e32e02c2
5 changed files with 481 additions and 0 deletions

View file

@ -7,4 +7,5 @@ members = [
"openpgp-card",
"openpgp-card-sequoia",
"scdc",
"card-functionality",
]

View file

@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
# SPDX-License-Identifier: MIT OR Apache-2.0
[package]
name = "card-functionality"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
openpgp-card = { path = "../openpgp-card" }
openpgp-card-sequoia = { path = "../openpgp-card-sequoia" }
sequoia-openpgp = "1.3"
anyhow = "1"
env_logger = "0.8"

View file

@ -0,0 +1,83 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
7aYYBzgV/of68E1HMFmY7A==
=JojA
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1,309 @@
// SPDX-FileCopyrightText: 2021 Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! These tests rely mainly on the card-app abstraction layer in
//! openpgp-card. However, for crypto-operations, higher level APIs and
//! Sequoia PGP are used.
//!
//! The main purpose of this test suite is to be able to test the behavior
//! of different OpenPGP card implementation.
//!
//! These tests assert (and fail) in cases where a certain behavior is
//! expected from all cards, and a card doesn't conform.
//! However, in some aspects, card behavior is expected to diverge, and
//! it's not ok for us to just fail and reject the card's output.
//! Even when it contradicts the OpenPGP card spec.
//!
//! For such cases, these tests return a TestOutput, which is a
//! Vec<TestResult>, to document the return values of the card in question.
//!
//! e.g.: the Yubikey 5 fails to handle the VERIFY command with empty data
//! (see OpenPGP card spec, 7.2.2: "If the command is called
//! without data, the actual access status of the addressed password is
//! returned or the access status is set to 'not verified'").
//!
//! The Yubikey 5 erroneously returns Status 0x6a80 ("Incorrect parameters in
//! the command data field").
use anyhow::Result;
use std::collections::HashMap;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::Cert;
use openpgp_card::apdu::PcscClient;
use openpgp_card::card_app::CardApp;
use openpgp_card::{CardClientBox, KeyType, Sex};
mod util;
#[derive(Debug)]
enum TestResult {
Status([u8; 2]),
Text(String),
}
type TestOutput = Vec<TestResult>;
/// Map: Card ident -> TestOutput
type TestsOutput = HashMap<String, TestOutput>;
/// Run after each "upload keys", if key *was* uploaded (?)
fn test_decrypt() {
// FIXME
unimplemented!()
}
/// Run after each "upload keys", if key *was* uploaded (?)
fn test_sign() {
// FIXME
unimplemented!()
}
fn check_key_upload_metadata(
ca: &mut CardApp,
meta: &[(String, u32)],
) -> Result<()> {
let ard = ca.get_app_data()?;
// check fingerprints
let card_fp = CardApp::get_fingerprints(&ard)?;
let sig = card_fp.signature().expect("signature fingerprint");
assert_eq!(format!("{:X}", sig), meta[0].0);
let dec = card_fp.decryption().expect("decryption fingerprint");
assert_eq!(format!("{:X}", dec), meta[1].0);
let auth = card_fp
.authentication()
.expect("authentication fingerprint");
assert_eq!(format!("{:X}", auth), meta[2].0);
// get_key_generation_times
let card_kg = CardApp::get_key_generation_times(&ard)?;
let sig: u32 =
card_kg.signature().expect("signature creation time").into();
assert_eq!(sig, meta[0].1);
let dec: u32 = card_kg
.decryption()
.expect("decryption creation time")
.into();
assert_eq!(dec, meta[1].1);
let auth: u32 = card_kg
.authentication()
.expect("authentication creation time")
.into();
assert_eq!(auth, meta[2].1);
Ok(())
}
fn check_key_upload_algo_attrs() -> Result<()> {
// get_algorithm_attributes
// FIXME
Ok(())
}
fn test_upload_keys_rsa_2k(ca: &mut CardApp) -> Result<TestOutput> {
let verify = ca.verify_pw3("12345678")?;
verify.check_ok()?;
let cert = Cert::from_file("data/rsa2k.sec")?;
let meta = util::upload_subkeys(ca, &cert)?;
check_key_upload_metadata(ca, &meta)?;
check_key_upload_algo_attrs()?;
Ok(vec![])
}
fn test_upload_keys_25519() {
// FIXME
unimplemented!()
// check if card supports 25519, if not that's ok, return this
// information and don't try upload.
// upload key
// test upload general - checks
}
fn test_keygen() {
// FIXME
// (implementation of this functionality is still missing in openpgp-card)
unimplemented!()
}
fn test_reset(ca: &mut CardApp) -> Result<TestOutput> {
let res = ca.factory_reset()?;
Ok(vec![])
}
/// Sets name, lang, sex, url; then reads the fields from the card and
/// compares the values with the expected values.
///
/// Returns an empty TestOutput, throws errors for unexpected Status codes
/// and for unequal field values.
fn test_set_user_data(ca: &mut CardApp) -> Result<TestOutput> {
let res = ca.verify_pw3("12345678")?;
res.check_ok()?;
// name
let res = ca.set_name("Bar<<Foo")?;
res.check_ok()?;
// lang
let res = ca.set_lang("deen")?;
res.check_ok()?;
// sex
let res = ca.set_sex(Sex::Female)?;
res.check_ok()?;
// url
let res = ca.set_url("https://duckduckgo.com/")?;
res.check_ok()?;
// read all the fields back again, expect equal data
let ch = ca.get_cardholder_related_data()?;
assert_eq!(ch.name, Some("Bar<<Foo".to_string()));
assert_eq!(ch.lang, Some(vec![['d', 'e'], ['e', 'n']]));
assert_eq!(ch.sex, Some(Sex::Female));
let url = ca.get_url()?;
assert_eq!(url, "https://duckduckgo.com/".to_string());
Ok(vec![])
}
/// Outputs:
/// - verify pw3 (check) -> Status
/// - verify pw1 (check) -> Status
fn test_verify(ca: &mut CardApp) -> Result<TestOutput> {
// Steps:
//
// - try to set name without verify, assert result is not ok
// - verify pw3 + pin -> Status
// - verify pw3 (check) -> Status
// - set name -> Status
// - get name -> Text(name)
// - verify pw1 + pin -> Status
// - verify pw1 (check) -> Status
// - set name -> Status
// - get name -> Text(name)
let mut out = vec![];
// try to set name without verify, assert result is not ok!
let res = ca.set_name("Notverified<<Hello")?;
assert_eq!(res.status(), [0x69, 0x82]); // "Security status not satisfied"
let res = ca.verify_pw3("12345678")?;
res.check_ok()?;
let check = ca.check_pw3()?;
// don't "check_ok()" - yubikey5 returns an error code!
out.push(TestResult::Status(check.status()));
let res = ca.set_name("Admin<<Hello")?;
res.check_ok()?;
let cardholder = ca.get_cardholder_related_data()?;
assert_eq!(cardholder.name, Some("Admin<<Hello".to_string()));
let res = ca.verify_pw1("123456")?;
res.check_ok()?;
let check = ca.check_pw3()?;
// don't "check_ok()" - yubikey5 returns an error code
out.push(TestResult::Status(check.status()));
let res = ca.set_name("There<<Hello")?;
res.check_ok()?;
let cardholder = ca.get_cardholder_related_data()?;
assert_eq!(cardholder.name, Some("There<<Hello".to_string()));
Ok(out)
}
fn run_test(
cards: &[&str],
t: fn(&mut CardApp) -> Result<TestOutput>,
) -> Result<TestsOutput> {
let mut out = HashMap::new();
for card in PcscClient::list_cards()? {
let card_client = Box::new(card) as CardClientBox;
let mut ca = CardApp::new(card_client);
// Select OpenPGP applet
let res = ca.select()?;
res.check_ok()?;
// Set Card Capabilities (chaining, command length, ..)
let ard = ca.get_app_data()?;
ca = ca.init_caps(&ard)?;
let ard = ca.get_app_data()?;
let app_id = CardApp::get_aid(&ard)?;
if cards.contains(&app_id.ident().as_str()) {
println!("Running Test on {}:", app_id.ident());
let res = t(&mut ca);
out.insert(app_id.ident(), res?);
}
}
Ok(out)
}
fn main() -> Result<()> {
env_logger::init();
// list of card idents to runs the tests on
let cards = vec![
"0006:16019180", /* Yubikey 5 */
"0005:0000A835", /* FLOSS Card 3.4 */
"FFFE:57183146", /* Gnuk Rysim (green) */
// "FFFE:4231EB6E", /* Gnuk FST */
];
// println!("reset");
// let _ = run_test(&cards, test_reset)?;
//
// println!("verify");
// let verify_out = run_test(&cards, test_verify)?;
// println!("{:x?}", verify_out);
//
// println!("set user data");
// let userdata_out = run_test(&cards, test_set_user_data)?;
// println!("{:x?}", userdata_out);
// upload RSA keys
println!("upload RSA2k key");
let upload_out = run_test(&cards, test_upload_keys_rsa_2k)?;
println!("{:x?}", upload_out);
// sign
// decrypt
// upload 25519 keys
// sign
// decrypt
// upload some key with pw
Ok(())
}

View file

@ -0,0 +1,72 @@
use anyhow::{anyhow, Result};
use sequoia_openpgp::cert::amalgamation::key::ValidKeyAmalgamation;
use sequoia_openpgp::packet::key::{SecretParts, UnspecifiedRole};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::Cert;
use openpgp_card::card_app::CardApp;
use openpgp_card::KeyType;
use openpgp_card_sequoia::vka_as_uploadable_key;
use std::time::SystemTime;
pub const SP: &StandardPolicy = &StandardPolicy::new();
pub(crate) fn upload_subkeys(
ca: &mut CardApp,
cert: &Cert,
) -> Result<Vec<(String, u32)>> {
let mut out = vec![];
let mut gentime = 0;
for kt in [
KeyType::Signing,
KeyType::Decryption,
KeyType::Authentication,
] {
let vka = get_subkey(cert, kt)?;
// store fingerprint as return-value
let fp = vka.fingerprint().to_hex();
// store key creation time as return-value
let creation = vka
.creation_time()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
out.push((fp, creation));
// upload key
let cuk = vka_as_uploadable_key(vka, None);
let res = ca.upload_key(cuk, kt)?;
}
Ok(out)
}
fn get_subkey(
cert: &Cert,
key_type: KeyType,
) -> Result<ValidKeyAmalgamation<'_, SecretParts, UnspecifiedRole, bool>> {
// Find all suitable (sub)keys for key_type.
let mut valid_ka = cert
.keys()
.with_policy(SP, None)
.secret()
.alive()
.revoked(false);
valid_ka = match key_type {
KeyType::Decryption => valid_ka.for_storage_encryption(),
KeyType::Signing => valid_ka.for_signing(),
KeyType::Authentication => valid_ka.for_authentication(),
_ => return Err(anyhow!("Unexpected KeyType")),
};
// FIXME: for now, we just pick the first (sub)key from the list
if let Some(vka) = valid_ka.next() {
Ok(vka)
} else {
Err(anyhow!("No suitable (sub)key found"))
}
}