Skip to content

Commit

Permalink
Improve flexibility of command_responce tests using a serialized format
Browse files Browse the repository at this point in the history
  • Loading branch information
sosthene-nitrokey committed Oct 17, 2022
1 parent 4454679 commit 2a5ba7d
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 55 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ stoppable_thread = "0.2.1"
test-log = "0.2.10"
trussed = { version = "0.1.0", features = ["virt"] }
rand = "0.8.5"
ron = "0.8"
serde_cbor = "0.11"
hex = { version = "0.4", features = ["serde"] }

[features]
std = []
Expand Down
40 changes: 40 additions & 0 deletions tests/command_response.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (C) 2022 Nitrokey GmbH
// SPDX-License-Identifier: LGPL-3.0-only

[
IoTest(
name: "GET CHALLENGE",
cmd_resp: [
IoData(
input: "00 84 0000 0A",
output: And([NonZero, Len(0x0A)])
),
IoData(
input: "00 84 0000 00 0400",
output: And([NonZero, Len(0x0400)])
)
]
),
IoTest(
name: "AES",
cmd_resp: [
// Verify Admin Pin
IoData(input: "00200083 08 3132333435363738"),
// Verify User Pin
IoData(input: "00200082 06 313233343536"),
// Set aes key
IoData(input: "0C DA 00D5 20 FFEEDDCCBBAA00998877665544332211FFEEDDCCBBAA00998877665544332211"),
// Encrypt with AES
IoData(
input: "00 2A 86 80 10 00112233445566778899AABBCCDDEEFF 00",
output: Data("02 d9d2ca17e160427aee649db6912dbfad"),
),
// Decrypt with AES
IoData(
input: "00 2A 80 86 11 02 d9d2ca17e160427aee649db6912dbfad 00",
output: Data("00112233445566778899AABBCCDDEEFF"),
),

]
)
]
210 changes: 155 additions & 55 deletions tests/command_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,162 @@
// SPDX-License-Identifier: LGPL-3.0-only
#![cfg(feature = "virtual")]

use hex_literal::hex;
use serde::Deserialize;

// iso7816::Status doesn't support serde
#[derive(Deserialize, Debug, PartialEq)]
enum Status {
Success,
MoreAvailable(u8),
VerificationFailed,
RemainingRetries(u8),
UnspecifiedNonpersistentExecutionError,
UnspecifiedPersistentExecutionError,
WrongLength,
LogicalChannelNotSupported,
SecureMessagingNotSupported,
CommandChainingNotSupported,
SecurityStatusNotSatisfied,
ConditionsOfUseNotSatisfied,
OperationBlocked,
IncorrectDataParameter,
FunctionNotSupported,
NotFound,
NotEnoughMemory,
IncorrectP1OrP2Parameter,
KeyReferenceNotFound,
InstructionNotSupportedOrInvalid,
ClassNotSupported,
UnspecifiedCheckingError,
}

impl TryFrom<u16> for Status {
type Error = u16;
fn try_from(sw: u16) -> Result<Self, Self::Error> {
Ok(match sw {
0x6300 => Self::VerificationFailed,
sw @ 0x63c0..=0x63cf => Self::RemainingRetries((sw as u8) & 0xf),

0x6400 => Self::UnspecifiedNonpersistentExecutionError,
0x6500 => Self::UnspecifiedPersistentExecutionError,

0x6700 => Self::WrongLength,

0x6881 => Self::LogicalChannelNotSupported,
0x6882 => Self::SecureMessagingNotSupported,
0x6884 => Self::CommandChainingNotSupported,

0x6982 => Self::SecurityStatusNotSatisfied,
0x6985 => Self::ConditionsOfUseNotSatisfied,
0x6983 => Self::OperationBlocked,

0x6a80 => Self::IncorrectDataParameter,
0x6a81 => Self::FunctionNotSupported,
0x6a82 => Self::NotFound,
0x6a84 => Self::NotEnoughMemory,
0x6a86 => Self::IncorrectP1OrP2Parameter,
0x6a88 => Self::KeyReferenceNotFound,

0x6d00 => Self::InstructionNotSupportedOrInvalid,
0x6e00 => Self::ClassNotSupported,
0x6f00 => Self::UnspecifiedCheckingError,

0x9000 => Self::Success,
sw @ 0x6100..=0x61FF => Self::MoreAvailable(sw as u8),
other => return Err(other),
})
}
}

impl Default for Status {
fn default() -> Status {
Status::Success
}
}

#[derive(Deserialize, Debug)]
struct IoTest {
name: String,
cmd_resp: Vec<IoData>,
}

#[derive(Deserialize, Debug)]
enum OutputMatcher {
And(Vec<OutputMatcher>),
Or(Vec<OutputMatcher>),
Len(usize),
Data(String),
NonZero,
}

impl Default for OutputMatcher {
fn default() -> Self {
OutputMatcher::Len(0)
}
}

fn parse_hex(data: &str) -> Vec<u8> {
let tmp: String = data.split_whitespace().collect();
hex::decode(&tmp).unwrap()
}

impl OutputMatcher {
fn validate(&self, data: &[u8]) -> bool {
match self {
Self::NonZero => data.iter().max() != Some(&0),
Self::Data(expected) => {
println!("Validating output with {expected}");
data == parse_hex(expected)
}
Self::Len(len) => data.len() == *len,
Self::And(matchers) => matchers.iter().filter(|m| !m.validate(data)).count() == 0,
Self::Or(matchers) => matchers.iter().filter(|m| m.validate(data)).count() != 0,
}
}
}

#[derive(Deserialize, Debug)]
struct IoData {
input: String,
#[serde(default)]
output: OutputMatcher,
#[serde(default)]
expected_status: Status,
}

#[test_log::test]
fn command_response() {
trussed::virt::with_ram_client("opcard", |client| {
let mut card = opcard::Card::new(client, opcard::Options::default());
let reset_command: iso7816::Command<4> =
iso7816::Command::try_from(&hex!("00 44 0000")).unwrap();
let mut rep: heapless::Vec<u8, 0> = heapless::Vec::new();
card.handle(&reset_command, &mut rep).unwrap();

let get_challenge: iso7816::Command<5> =
iso7816::Command::try_from(&hex!("00 84 0000 0A")).unwrap();
let mut rep: heapless::Vec<u8, 16> = heapless::Vec::new();
card.handle(&get_challenge, &mut rep).unwrap();
assert_eq!(rep.len(), 10);
// Sanity check that it's not uninitialized or something
assert_ne!(rep, [0; 10]);

let get_challenge: iso7816::Command<5> =
iso7816::Command::try_from(&hex!("00 84 0000 00 0400")).unwrap();
let mut rep: heapless::Vec<u8, 1024> = heapless::Vec::new();
card.handle(&get_challenge, &mut rep).unwrap();
assert_eq!(rep.len(), 1024);
// Sanity check that it's not uninitialized or something
assert_ne!(rep, [0; 1024]);
rep.clear();

let admin_pin_cmd: iso7816::Command<32> =
iso7816::Command::try_from(hex!("00200083 08 3132333435363738").as_slice()).unwrap();
card.handle(&admin_pin_cmd, &mut rep).unwrap();
rep.clear();

let user_pin_cmd: iso7816::Command<32> =
iso7816::Command::try_from(hex!("00200082 06 313233343536").as_slice()).unwrap();
card.handle(&user_pin_cmd, &mut rep).unwrap();

let set_aes_key = Vec::from(hex!(
"0C DA 00D5 20 FFEEDDCCBBAA00998877665544332211FFEEDDCCBBAA00998877665544332211"
));
let import_cmd: iso7816::Command<32> = iso7816::Command::try_from(&set_aes_key).unwrap();
card.handle(&import_cmd, &mut rep).unwrap();

let encrypt_aes = Vec::from(hex!("00 2A 86 80 10 00112233445566778899AABBCCDDEEFF 00"));
let encrypt_cmd: iso7816::Command<16> = iso7816::Command::try_from(&encrypt_aes).unwrap();
let mut rep: heapless::Vec<u8, 17> = heapless::Vec::new();
card.handle(&encrypt_cmd, &mut rep).unwrap();
assert_eq!(rep, hex!("02 d9d2ca17e160427aee649db6912dbfad"));

let mut decrypt_aes = Vec::from(hex!("00 2A 80 86 11"));
decrypt_aes.extend_from_slice(&rep);
decrypt_aes.push(0x00);

let decrypt_cmd: iso7816::Command<17> = iso7816::Command::try_from(&decrypt_aes).unwrap();
let mut rep: heapless::Vec<u8, 16> = heapless::Vec::new();
card.handle(&decrypt_cmd, &mut rep).unwrap();
assert_eq!(rep, hex!("00112233445566778899AABBCCDDEEFF"));
})
let data = std::fs::read_to_string("tests/command_response.ron").unwrap();
let tests: Vec<IoTest> = ron::from_str(&data).unwrap();
for t in tests {
println!("Running {}", t.name);
trussed::virt::with_ram_client("opcard", |client| {
let mut card = opcard::Card::new(client, opcard::Options::default());
for io in t.cmd_resp {
println!("Command: {:?}", io.input);
let mut rep: heapless::Vec<u8, 1024> = heapless::Vec::new();
let cmd: iso7816::Command<1024> = iso7816::Command::try_from(&parse_hex(&io.input))
.unwrap_or_else(|err| {
panic!(
"Bad command: {err:?}, for command: {}",
hex::encode(&io.input)
)
});
let status: Status = card
.handle(&cmd, &mut rep)
.err()
.map(|s| TryFrom::<u16>::try_from(s.into()).unwrap())
.unwrap_or_default();

println!("Output: {:?}\nStatus: {status:?}", hex::encode(&rep));

if !io.output.validate(&rep) {
panic!("Bad output. Expected {:?}", io.output);
}
if status != io.expected_status {
panic!("Bad status. Expected {:?}", io.expected_status);
}
}
});
}
}

0 comments on commit 2a5ba7d

Please sign in to comment.