rework apdu-dispatch, add apdu chaining

- APDU parsing now only occurs in apdu-dispatch, instead of both iso14443 and usbd-ccid
- apdu-dispatch now handles apdu chaining transparently to the apps.
- More tests added to cover chaining and apdu parsing
This commit is contained in:
Conor Patrick
2020-10-09 16:20:12 +02:00
committed by Nicolas Stalder
parent 76d9f28692
commit 08d3631ef2
5 changed files with 930 additions and 431 deletions
+1 -1
View File
@@ -3,5 +3,5 @@
For the tests to run locally for ApduDispatch, you need to enable std for logs.
```
cargo test --features std,logging/std --target $(rustc -Vv | awk 'NR==5{print $2}')
cargo test --features std,logging/std,log-all --target $(rustc -Vv | awk 'NR==5{print $2}')
```
+194 -31
View File
@@ -17,6 +17,8 @@ use iso7816::{
Instruction,
Response,
Status,
response,
command::FromSliceError,
};
#[derive(Copy, Clone)]
@@ -25,6 +27,12 @@ pub enum InterfaceType{
Contactless,
}
pub enum ApduType{
Select(Aid),
GetResponse,
Other,
}
use crate::logger::info;
use interchange::Responder;
@@ -36,15 +44,20 @@ pub struct ApduDispatch {
contact: Responder<ContactInterchange>,
contactless: Responder<ContactlessInterchange>,
current_interface: InterfaceType,
chain_buffer: response::Data,
is_chaining_response: bool,
}
impl ApduDispatch
{
fn aid_to_select(apdu: &Command) -> Option<Aid> {
fn apdu_type(apdu: &Command) -> ApduType {
if apdu.instruction() == Instruction::Select && (apdu.p1 & 0x04) != 0 {
Some(Aid::from_slice(apdu.data()).unwrap())
ApduType::Select(Aid::from_slice(apdu.data()).unwrap())
} else if apdu.instruction() == Instruction::GetResponse {
ApduType::GetResponse
} else {
None
ApduType::Other
}
}
@@ -57,6 +70,8 @@ impl ApduDispatch
contact: contact,
contactless: contactless,
current_interface: InterfaceType::Contact,
chain_buffer: response::Data::new(),
is_chaining_response: false,
}
}
@@ -95,18 +110,116 @@ impl ApduDispatch
contactless_busy || contact_busy
}
fn buffer_chained_apdu_if_needed(&mut self, command: Command, inferface: InterfaceType) -> Option<Command>{
self.current_interface = inferface;
// iso 7816-4 5.1.1
// check Apdu level chaining and buffer if necessary.
if command.class().chain().last_or_only() {
if self.chain_buffer.len() > 0 && !self.is_chaining_response{
// Merge the chained buffer with the new apdu.
self.chain_buffer.extend_from_slice(command.data()).unwrap();
let length: u16 = (self.chain_buffer.len() - 7) as u16;
self.chain_buffer[0] = command.class().into_inner();
self.chain_buffer[1] = command.instruction().into();
self.chain_buffer[2] = command.p1;
self.chain_buffer[3] = command.p2;
// chain_buffer[4] == 0
self.chain_buffer[5] = ((length & 0xff00) >> 8) as u8;
self.chain_buffer[6] = (length & 0xff) as u8;
info!("merging {} bytes", length).ok();
let merged_apdu = Command::try_from(&self.chain_buffer).unwrap();
self.chain_buffer.clear();
// Response now needs to be chained.
self.is_chaining_response = true;
Some(merged_apdu)
} else {
Some(command)
}
} else {
match inferface {
// acknowledge
InterfaceType::Contact => {
self.contact.respond(Response::Data(Default::default()).into_message())
.expect("Could not respond");
}
InterfaceType::Contactless => {
self.contactless.respond(Response::Data(Default::default()).into_message())
.expect("Could not respond");
}
}
if self.is_chaining_response {
info!("Was chaining the last response, but aborting that now for this new request.").ok();
self.is_chaining_response = false;
self.chain_buffer.clear();
}
if self.chain_buffer.len() == 0 {
// Prepend an extended length apdu header.
self.chain_buffer.push(0x00).ok(); // cla
self.chain_buffer.push(0x00).ok(); // ins
self.chain_buffer.push(0x00).ok(); // p1
self.chain_buffer.push(0x00).ok(); // p2
self.chain_buffer.push(0x00).ok(); // 0x00
self.chain_buffer.push(0x00).ok(); // length upper byte
self.chain_buffer.push(0x00).ok(); // length lower byte
}
info!("chaining {} bytes", command.data().len()).ok();
self.chain_buffer.extend_from_slice(&command.data()).ok();
None
}
}
fn parse_apdu(message: &iso7816::command::Data) -> core::result::Result<Command,Response> {
match Command::try_from(message) {
Ok(command) => {
Ok(command)
},
Err(_error) => {
logging::info!("apdu bad").ok();
match _error {
FromSliceError::TooShort => { info!("TooShort").ok(); },
FromSliceError::InvalidClass => { info!("InvalidClass").ok(); },
FromSliceError::InvalidFirstBodyByteForExtended => { info!("InvalidFirstBodyByteForExtended").ok(); },
FromSliceError::CanThisReallyOccur => { info!("CanThisReallyOccur").ok(); },
}
Err(Response::Status(Status::UnspecifiedCheckingError))
}
}
}
fn check_for_request(&mut self) -> Option<Command> {
if !self.busy() {
// prioritize contactless interface
if let Some(apdu) = self.contactless.take_request() {
self.current_interface = InterfaceType::Contactless;
Some(apdu)
} else if let Some(apdu) = self.contact.take_request() {
self.current_interface = InterfaceType::Contact;
Some(apdu)
// Check to see if we have gotten a message, giving priority to contactless.
let (message, interface) = if let Some(message) = self.contactless.take_request() {
(message, InterfaceType::Contactless)
} else if let Some(message) = self.contact.take_request() {
(message, InterfaceType::Contact)
} else {
None
return None;
};
// Parse the message as an APDU.
match Self::parse_apdu(message.as_ref()) {
Ok(command) => {
// The Apdu may be standalone or part of a chain.
self.buffer_chained_apdu_if_needed(command, interface)
},
Err(response) => {
// If not a valid APDU, return error and don't pass to app.
match self.current_interface {
InterfaceType::Contactless =>
self.contactless.respond(response.into_message()).expect("cant respond"),
InterfaceType::Contact =>
self.contact.respond(response.into_message()).expect("cant respond"),
}
None
}
}
} else {
@@ -129,10 +242,10 @@ impl ApduDispatch
let response = match request {
// have new command APDU
Some(apdu) => {
// two cases: SELECT or not SELECT
match Self::aid_to_select(&apdu) {
// three cases: SELECT, GET RESPONSE, or Other
match Self::apdu_type(&apdu) {
// SELECT case
Some(aid) => {
ApduType::Select(aid) => {
// three cases:
// - currently selected app has different AID -> deselect it, to give it
// the chance to clear sensitive state
@@ -166,8 +279,25 @@ impl ApduDispatch
}
// command that is not a SELECT command
None => {
ApduType::GetResponse => {
// The reader/host is using chaining. On behalf of the app,
// we will return the response in chunks.
if self.chain_buffer.len() == 0 || !self.is_chaining_response {
info!("Unexpected GetResponse").ok();
Err(Status::UnspecifiedCheckingError)
} else {
// This is a bit unclear, but am returning this
// just to continue the chaining response.
Ok(AppletResponse::Respond(Default::default()))
}
}
// command that is not a special command -- goes to applet.
ApduType::Other => {
// Invalidate the chain_buffer
self.chain_buffer.clear();
// if there is a selected app, send it the command
if let Some(applet) = Self::find_applet(self.current_aid.as_ref(), applets) {
applet.call(apdu)
@@ -192,29 +322,62 @@ impl ApduDispatch
}
};
match response {
let message = match response {
Ok(AppletResponse::Respond(response)) => {
use InterfaceType::*;
match self.current_interface {
Contactless =>
self.contactless.respond(Response::Data(response)).expect("cant respond"),
Contact =>
self.contact.respond(Response::Data(response)).expect("cant respond"),
// Consider if we need to reply via chaining method.
// If the reader is using chaining, we will simply
// reply 61XX, and put the response in a buffer.
// It is up to the reader to then send GetResponse
// requests, to which we will return up to 256 bytes at a time.
if self.is_chaining_response {
if self.chain_buffer.len() == 0 {
self.chain_buffer.extend_from_slice(&response).ok();
info!("Putting response of {} bytes into chain buffer", response.len()).ok();
}
// Send 256 bytes max at a time.
let boundary = core::cmp::min(256, self.chain_buffer.len());
let to_send = &self.chain_buffer[..boundary];
let remaining = &self.chain_buffer[boundary..];
let mut message = response::Data::from_slice(to_send).unwrap();
let return_code = if remaining.len() > 255 {
// XX = 00 indicates more than 255 bytes of data
0x6100u16
} else if remaining.len() > 0 {
0x6100u16 + (remaining.len() as u16)
} else {
// Last chunk has success code
0x9000
};
message.extend_from_slice(&return_code.to_be_bytes()).ok();
self.chain_buffer = response::Data::from_slice(remaining).unwrap();
message
} else {
// Just reply normally
Response::Data(response).into_message()
}
}
Ok(AppletResponse::Defer) => {}
Ok(AppletResponse::Defer) => {
return;
}
Err(status) => {
info!("applet error").ok();
use InterfaceType::*;
match self.current_interface {
Contactless =>
self.contactless.respond(Response::Status(status)).expect("cant respond"),
Contact =>
self.contact.respond(Response::Status(status)).expect("cant respond"),
}
Response::Status(status).into_message()
}
};
match self.current_interface {
InterfaceType::Contactless =>
self.contactless.respond(message).expect("cant respond"),
InterfaceType::Contact =>
self.contact.respond(message).expect("cant respond"),
}
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
interchange::interchange! {
ContactInterchange: (iso7816::Command, iso7816::Response)
ContactInterchange: (iso7816::command::Data, iso7816::response::Data)
}
interchange::interchange! {
ContactlessInterchange: (iso7816::Command, iso7816::Response)
ContactlessInterchange: (iso7816::command::Data, iso7816::response::Data)
}
+733
View File
File diff suppressed because it is too large Load Diff
-397
View File
@@ -1,397 +0,0 @@
use apdu_dispatch::applet::{
Applet,
Aid,
Response as AppletResponse,
Result as AppletResult,
};
use apdu_dispatch::types::{
ContactlessInterchange,
ContactInterchange,
};
use iso7816::{
Command,
Status,
};
use interchange::Interchange;
use heapless::ByteBuf;
#[macro_use]
extern crate serial_test;
#[allow(dead_code)]
enum TestInstruction {
Echo = 0x10,
Add = 0x11,
GetData = 0x12,
}
fn dump_hex(data: &[u8]){
for i in 0 .. data.len() {
print!("{:02X} ", data[i]);
}
println!();
}
pub struct TestApp1 {}
impl Aid for TestApp1 {
fn aid(&self) -> &'static [u8] {
&[ 0x0Au8, 1, 0, 0, 1]
}
fn right_truncated_length(&self) -> usize {
5
}
}
impl Applet for TestApp1 {
fn select(&mut self, _apdu: Command) -> AppletResult {
Ok(Default::default())
}
fn deselect(&mut self) {
}
fn call (&mut self, apdu: Command) -> AppletResult {
println!("TestApp1::call");
match apdu.instruction().into() {
0x10 => {
let mut buf = ByteBuf::new();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.extend_from_slice(apdu.data()).unwrap();
Ok(AppletResponse::Respond(buf))
}
_ =>
Err(Status::InstructionNotSupportedOrInvalid)
}
}
fn poll (&mut self) -> AppletResult {
panic!("Should not have idle polls here!");
}
}
pub struct TestApp2 {}
impl Aid for TestApp2 {
fn aid(&self) -> &'static [u8] {
&[ 0x0Au8, 1, 0, 0, 2]
}
fn right_truncated_length(&self) -> usize {
5
}
}
impl Applet for TestApp2 {
fn select(&mut self, _apdu: Command) -> AppletResult {
Ok(Default::default())
}
fn deselect(&mut self) {
}
fn call (&mut self, apdu: Command) -> AppletResult {
println!("TestApp2::call");
match apdu.instruction().into() {
0x20 => {
let mut buf = ByteBuf::new();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.push(0).unwrap();
buf.extend_from_slice(apdu.data()).unwrap();
Ok(AppletResponse::Respond(buf))
},
_ =>
Err(Status::InstructionNotSupportedOrInvalid)
}
}
fn poll (&mut self) -> AppletResult {
panic!("Should not have idle polls here!");
}
}
pub struct PanicApp {}
impl Aid for PanicApp{
fn aid(&self) -> &'static [u8] {
&[ 0x0Au8, 1, 0, 0, 3]
}
fn right_truncated_length(&self) -> usize {
5
}
}
impl Applet for PanicApp {
fn select(&mut self, _apdu: Command) -> AppletResult {
panic!("Dont call the panic app");
}
fn deselect(&mut self) {
panic!("Dont call the panic app");
}
fn call (&mut self, _apdu: Command) -> AppletResult {
panic!("Dont call the panic app");
}
fn poll (&mut self) -> AppletResult {
panic!("Dont call the panic app");
}
}
fn run_apdus(
apdu_response_pairs: &[&[u8]],
){
assert!(apdu_response_pairs.len() > 0);
assert!((apdu_response_pairs.len() & 1) == 0);
let (mut contact_requester, contact_responder) = ContactInterchange::claim(0)
.expect("could not setup ccid ApduInterchange");
let (_contactless_requester, contactless_responder) = ContactlessInterchange::claim(0)
.expect("could not setup iso14443 ApduInterchange");
let mut apdu_dispatch = apdu_dispatch::dispatch::ApduDispatch::new(contact_responder, contactless_responder);
let mut app1 = TestApp1{};
let mut app2 = TestApp2{};
let mut app3 = PanicApp{};
// for i in 0..apdu_response_pairs.len() {
// print!("- ");
// dump_hex(apdu_response_pairs[i]);
// }
for i in (0..apdu_response_pairs.len()).step_by(2) {
let raw_req = apdu_response_pairs[i];
let raw_expected_res = apdu_response_pairs[i + 1];
let command = Command::try_from(raw_req).unwrap();
// let expected_response = Response::Data::from_slice(&raw_res);
print!("<< ");
dump_hex(&raw_req);
contact_requester.request(command).expect("could not deposit command");
apdu_dispatch.poll(&mut[&mut app1, &mut app2, &mut app3]);
let response = contact_requester.take_response().unwrap().into_message();
print!(">> ");
dump_hex(&response);
if raw_expected_res != response.as_slice()
{
print!("expected: ");
dump_hex(&raw_expected_res);
print!("got: ");
dump_hex(&response);
panic!("Expected responses do not match");
}
}
}
#[test]
#[serial]
fn test_select_1(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x01],
// Ok
&[0x90, 0x00],
]
)
}
#[test]
#[serial]
fn test_select_2(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x02],
// Ok
&[0x90, 0x00],
]
)
}
#[test]
#[serial]
fn test_select_not_found(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x01, 0x00],
// Not found
&[0x6A, 0x82],
]
)
}
#[test]
#[serial]
fn test_echo_1(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x01],
// Ok
&[0x90, 0x00],
// Echo
&[0x00u8, 0x10, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
// Echo + Ok
&[0x00u8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x90, 0x00],
]
)
}
#[test]
#[serial]
fn test_echo_wrong_instruction(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x01],
// Ok
&[0x90, 0x00],
// Echo
&[0x00u8, 0x20, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
// Wrong Ins
&[0x6d, 0x00],
]
)
}
#[test]
#[serial]
fn test_echo_2(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x02],
// Ok
&[0x90, 0x00],
// Echo
&[0x00u8, 0x20, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
// Echo + Ok
&[0x00u8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x90, 0x00],
]
)
}
#[test]
#[serial]
fn test_echo_wrong_instruction_2(){
run_apdus(
&[
// Select
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x02],
// Ok
&[0x90, 0x00],
// Echo
&[0x00u8, 0x10, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
// Wrong Ins
&[0x6d, 0x00],
]
)
}
#[test]
#[serial]
fn test_unsolicited_instruction(){
run_apdus(
&[
// Echo
&[0x00u8, 0x10, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
// Not found
&[0x6a, 0x82],
]
)
}
#[test]
#[serial]
fn test_deselect (){
run_apdus(
&[
// Select 1
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x01],
&[0x90, 0x00],
// Echo 1
&[0x00u8, 0x10, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
&[0x00u8, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x90, 0x00],
// Select 2
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x02],
&[0x90, 0x00],
// Echo 1
&[0x00u8, 0x10, 0x00, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05],
&[0x6d, 0x00],
]
)
}
#[test]
#[serial]
fn test_extended_length_echo (){
run_apdus(
&[
// Select 1
&[0x00u8, 0xA4, 0x04, 0x00, 0x05, 0x0A, 0x01, 0x00, 0x00, 0x01],
&[0x90, 0x00],
// To be echo'd
&[0x00u8, 0x10, 0x00, 0x00, 0x00, 0x01, 0x23,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,
],
// echo Success
&[0x00, 0x00, 0x00, 0x00, 0x00,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,
0x90,00
]
]
)
}