From 8c112d519d803c970c9171fd7d5dd028c71bee53 Mon Sep 17 00:00:00 2001 From: Kai Kriegel Date: Tue, 27 Apr 2021 00:37:58 +0200 Subject: [PATCH] added basic iso7816_4 card support --- src/error.rs | 99 +++++++++++- src/iso7816_4/apducommand.rs | 261 +++++++++++++++++++++++++++++++ src/iso7816_4/apduresponse.rs | 100 ++++++++++++ src/iso7816_4/apdustatuswords.rs | 153 ++++++++++++++++++ src/iso7816_4/mod.rs | 3 + src/lib.rs | 25 ++- 6 files changed, 634 insertions(+), 7 deletions(-) create mode 100644 src/iso7816_4/apducommand.rs create mode 100644 src/iso7816_4/apduresponse.rs create mode 100644 src/iso7816_4/apdustatuswords.rs diff --git a/src/error.rs b/src/error.rs index c68f0df..e0fa1e6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,33 @@ use std::io; use std::fmt; -use hex::FromHexError; #[derive(Debug)] pub enum Error { IO(io::Error), Boxed(Box), + Simple(simple_error::SimpleError), BlockModeError(block_modes::BlockModeError), InvalidKeyIvLength(block_modes::InvalidKeyIvLength), - FromHexError(hex::FromHexError) + FromHexError(hex::FromHexError), + PadError(block_modes::block_padding::PadError), + InvalidStatusWord, + IllegalCommandCode, + IntegrityError, + NoSuchKey, + LengthError, + PermissionDenied, + ParameterError, + AuthenticationDelay, + AuthenticationError, + BoundaryError, + CommandAborted, + DuplicateError, + FileNotFound, + InvalidApplicationID, + InvalidAPDUResponse, + InvalidKeyID, + InvalidPICCChallenge, + InvalidFileID, } impl fmt::Display for Error { @@ -16,19 +35,79 @@ impl fmt::Display for Error { match self { Error::IO(e) => { write!(f, "IO Error: {}", e) - }, + } Error::Boxed(e) => { write!(f, "{}", e) - }, + } + Error::Simple(e) => { + write!(f, "Generic Error: {}", e) + } Error::BlockModeError(e) => { write!(f, "CBC Error: {}", e) - }, + } Error::InvalidKeyIvLength(e) => { write!(f, "CBC InvalidKeyIvLength Error: {}", e) } Error::FromHexError(e) => { write!(f, "Hex conversion Error: {}", e) } + Error::PadError(e) => { + write!(f, "Padding to blocksize failed: {:#?}", e) + } + Error::InvalidStatusWord => { + write!(f, "Invalid APDU Statusword") + } + Error::IllegalCommandCode => { + write!(f, "SW2: Command code not supported.") + } + Error::IntegrityError => { + write!(f, "SW2: CRC or MAC does not match data. Paddingbytes not valid.") + } + Error::NoSuchKey => { + write!(f, "SW2: Invalid key number specified.") + } + Error::LengthError => { + write!(f, "SW2: Length of command string invalid.") + } + Error::PermissionDenied => { + write!(f, "SW2: Current configuration / status does not allow the requested command.") + } + Error::ParameterError => { + write!(f, "SW2: Value of the parameter(s) invalid.") + } + Error::AuthenticationDelay => { + write!(f, "SW2: Currently not allowed to authenticate. Keeptrying until full delay is spent.") + } + Error::AuthenticationError => { + write!(f, "SW2: Current authentication status does not allow the requested command.") + } + Error::BoundaryError => { + write!(f, "SW2: Attempt to read/write data from/to beyond the file’s/record’s limits. Attempt to exceed the limits of a value file.") + } + Error::CommandAborted => { + write!(f, "SW2: Previous Command was not fully completed.Not all Frames were requested or provided by the PCD.") + } + Error::DuplicateError => { + write!(f, "SW2: Creation of file/application failed because file/application with same number already exists") + } + Error::FileNotFound => { + write!(f, "SW2: Specified file number does not exist.") + } + Error::InvalidApplicationID => { + write!(f, "Application ID was larger then 24 bit") + } + Error::InvalidAPDUResponse => { + write!(f, "Got malformed APDUResponse") + } + Error::InvalidKeyID => { + write!(f, "Invalid KeyID: Was larger then 0x0E") + } + Error::InvalidPICCChallenge => { + write!(f, "Authentication failed, PICC Challenge is invalid.") + } + Error::InvalidFileID => { + write!(f, "Invalid FileID: Was larger then 0x20") + } } } } @@ -39,6 +118,10 @@ impl From for Error { } } +impl From for Error { + fn from(e: simple_error::SimpleError) -> Error { Error::Simple(e) } +} + impl From for Error { fn from(e: block_modes::BlockModeError) -> Error { Error::BlockModeError(e) @@ -52,7 +135,11 @@ impl From for Error { } impl From for Error { - fn from(e: hex::FromHexError) -> Error { Error::FromHexError(e)} + fn from(e: hex::FromHexError) -> Error { Error::FromHexError(e) } +} + +impl From for Error { + fn from(e: block_modes::block_padding::PadError) -> Error { Error::PadError(e) } } pub(crate) type Result = std::result::Result; diff --git a/src/iso7816_4/apducommand.rs b/src/iso7816_4/apducommand.rs new file mode 100644 index 0000000..0c1f861 --- /dev/null +++ b/src/iso7816_4/apducommand.rs @@ -0,0 +1,261 @@ +use std::fmt; +use std::fmt::Formatter; + +#[repr(u8)] +#[derive(Eq, PartialEq, Hash, Debug)] +pub enum IsoCase { + /// No command data. No response data. + /// + /// + /// lc is valued to 0. + /// le is valued to 0. + /// No data byte is present. + /// + /// + Case1 = 0, + + /// No command data. Expected response data. + /// + /// + /// lc is valued to 0. + /// le is valued from 1 to 256. + /// No data byte is present. + /// + /// + Case2Short = 1, + + /// Command data. No response data. + /// + /// + /// lc is valued from 1 to 255. + /// le is valued to 0. + /// lc data bytes are present. + /// + /// + Case3Short = 2, + + /// Command data. Expected response data. + /// + /// + /// lc is valued from 1 to 255. + /// le is valued from 1 to 256. + /// lc data bytes are present. + /// + /// + Case4Short = 3, + + /// No command data. Expected response data. + /// + /// + /// lc is valued to 0. + /// le is valued from 1 to 65536. + /// No data byte is present. + /// + /// + Case2Extended = 4, + + /// Command data. No response data. + /// + /// + /// lc is valued from 1 to 65536. + /// le is valued to 0. + /// lc data bytes are present. + /// + /// + Case3Extended = 5, + + /// Command data. Expected response data. + /// + /// + /// lc is valued from 1 to 65535. + /// le is valued from 1 to 65536. + /// lc data bytes are present. + /// + /// + Case4Extended = 6, +} + +impl Default for IsoCase { + fn default() -> Self { IsoCase::Case1 } +} + +#[repr(u8)] +#[derive(Eq, PartialEq, Hash, Debug)] +pub enum SCardProtocol { + /// + /// protocol not defined. + /// + Unset = 0x0000, + + /// T=0 active protocol. + T0 = 0x0001, + + /// T=1 active protocol. + T1 = 0x0002, + + /// Raw active protocol. Use with memory type cards. + Raw = 0x0004, + + /// T=15 protocol. + T15 = 0x0008, + + /// ( | ). IFD (Interface device) determines protocol. + Any = (0x0001 | 0x0002), +} + +impl Default for SCardProtocol { + fn default() -> Self { SCardProtocol::Unset } +} + +#[derive(Eq, PartialEq, Hash, Debug, Default)] +pub struct APDUCommand { + pub case: IsoCase, + pub protocol: SCardProtocol, + pub cla: u8, + pub ins: u8, + pub p1: u8, + pub p2: u8, + pub data: Option>, + pub lc: i32, + pub le: i32, +} +//FIXME: Do we want pub fields or constructor? +// We want pub fields! + constructor? + +// impl APDUCommand { +// pub fn new(case: IsoCase) -> Self { +// APDUCommand { case, protocol: (), cla: (), ins: (), p1: (), p2: (), data: (), lc: (), le: () } +// } +// } + +impl fmt::Display for APDUCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.case { + IsoCase::Case1 => { + write!(f, "(CASE: 1) cla: {:#X} | ins: {:#X} | p1: {:#X} | p2: {:#X}", self.cla, self.ins, self.p1, self.p2) + } + IsoCase::Case2Short | IsoCase::Case2Extended => { + write!(f, "(CASE: 2) cla: {:#X} | ins: {:#X} | p1: {:#X} | p2: {:#X} | LE: {:#X}", self.cla, self.ins, self.p1, self.p2, self.le) + } + IsoCase::Case3Short | IsoCase::Case3Extended => { + write!(f, "(CASE: 3) cla: {:#X} | ins: {:#X} | p1: {:#X} | p2: {:#X} | LC: {:#X} | data: [ ", self.cla, self.ins, self.p1, self.p2, self.lc)?; + for b in self.data.as_ref().unwrap() { + write!(f, "{:#X} ", b)?; + } + write!(f, "]") + } + IsoCase::Case4Short | IsoCase::Case4Extended => { + write!(f, "(CASE: 4) cla: {:#X} | ins: {:#X} | p1: {:#X} | p2: {:#X} | LC: {:#X} | data: [ ", self.cla, self.ins, self.p1, self.p2, self.lc)?; + for b in self.data.as_ref().unwrap() { + write!(f, "{:#X} ", b)?; + } + write!(f, "] | LE: {:#X}", self.le) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::iso7816_4::apducommand::{APDUCommand, SCardProtocol, IsoCase}; + + #[test] + fn compare() { + let command1 = APDUCommand{ + case: IsoCase::Case4Short, + protocol: SCardProtocol::Unset, + cla: 0x90, + ins: 0xAA, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + let command2 = APDUCommand{ + case: IsoCase::Case4Short, + protocol: SCardProtocol::Unset, + cla: 0x90, + ins: 0xAA, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + assert_eq!(command1, command2); + } + + #[test] + fn compare_diff() { + let command1 = APDUCommand{ + case: IsoCase::Case4Short, + protocol: SCardProtocol::Unset, + cla: 0x90, + ins: 0xAA, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + let command2 = APDUCommand{ + case: IsoCase::Case4Short, + protocol: SCardProtocol::Unset, + cla: 0x90, + ins: 0x1A, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + assert_ne!(command1, command2); + } + + #[test] + fn to_string_case1() { + let command1 = APDUCommand{ + case: IsoCase::Case1, + cla: 0x90, + ins: 0x1A, + ..Default::default() + }; + + println!("{}", command1.to_string()); + assert_eq!("(CASE: 1) cla: 0x90 | ins: 0x1A | p1: 0x0 | p2: 0x0", command1.to_string()); + } + + #[test] + fn to_string_case2() { + let command1 = APDUCommand{ + case: IsoCase::Case2Short, + cla: 0x90, + ins: 0x1A, + ..Default::default() + }; + + println!("{}", command1.to_string()); + assert_eq!("(CASE: 2) cla: 0x90 | ins: 0x1A | p1: 0x0 | p2: 0x0 | LE: 0x0", command1.to_string()); + } + + #[test] + fn to_string_case3() { + let command1 = APDUCommand{ + case: IsoCase::Case3Short, + cla: 0x90, + ins: 0x1A, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + println!("{}", command1.to_string()); + assert_eq!("(CASE: 3) cla: 0x90 | ins: 0x1A | p1: 0x0 | p2: 0x0 | LC: 0x0 | data: [ 0x1 0x2 0x3 ]", command1.to_string()); + } + + #[test] + fn to_string_case4() { + let command1 = APDUCommand{ + case: IsoCase::Case4Short, + cla: 0x90, + ins: 0x1A, + data: Some(vec![0x01, 0x02, 0x03]), + ..Default::default() + }; + + println!("{}", command1.to_string()); + assert_eq!("(CASE: 4) cla: 0x90 | ins: 0x1A | p1: 0x0 | p2: 0x0 | LC: 0x0 | data: [ 0x1 0x2 0x3 ] | LE: 0x0", command1.to_string()); + } +} \ No newline at end of file diff --git a/src/iso7816_4/apduresponse.rs b/src/iso7816_4/apduresponse.rs new file mode 100644 index 0000000..38cbc37 --- /dev/null +++ b/src/iso7816_4/apduresponse.rs @@ -0,0 +1,100 @@ +use crate::iso7816_4::apdustatuswords::{APDUStatusWord, APDUStatusWord2}; +use crate::error::{Result}; +use num_traits::FromPrimitive; +use crate::error::Error::{InvalidStatusWord, IllegalCommandCode, IntegrityError, NoSuchKey, LengthError, PermissionDenied, ParameterError, AuthenticationDelay, AuthenticationError, BoundaryError, CommandAborted, DuplicateError, FileNotFound}; +use std::fmt; + +#[derive(Eq, PartialEq, Hash, Debug)] +pub struct APDUResponse { + pub body: Vec, + pub sw1: u8, + pub sw2: u8, +} + +impl APDUResponse { + pub fn body(&self) -> &Vec { + &self.body + } +} + +impl From for Vec { + fn from(resp: APDUResponse) -> Self { + let mut v: Vec = vec![]; + v.extend(resp.body); + v.push(resp.sw1); + v.push(resp.sw2); + v + } +} + +impl fmt::Display for APDUResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.body.is_empty() { + true => { + write!(f, "SW1: {:#X} | SW2: 0x{:#X}", self.sw1, self.sw2) + }, + false => { + write!(f, "SW1: {:#X} | SW2: {:#X} | Body: {:#X?}", self.sw1, self.sw2, self.body) + } + } + } +} + +impl APDUResponse { + pub fn new(raw: &[u8]) -> Self { + APDUResponse { + body: raw[..raw.len() - 1].to_vec(), + sw1: raw[raw.len() - 2], + sw2: raw[raw.len() - 3] + } + } + + pub fn get_statusword(&self) -> Result { + match self.sw1 { + 0x61 => Ok(APDUStatusWord::DataReady), + 0x62 => Ok(APDUStatusWord::StorageNotChanged), + 0x63 => { + if (self.sw2 & 0xF0) == 0xC0 { Ok(APDUStatusWord::CounterReached) } else { Ok(APDUStatusWord::StorageChanged) } + }, + 0x64 => Ok(APDUStatusWord::ExecutionErrorWithoutChange), + 0x65 => Ok(APDUStatusWord::ExecutionErrorWithChange), + 0x6C => Ok(APDUStatusWord::InvalidLe), + _ => FromPrimitive::from_u16(((self.sw1 as u16) << 8) | self.sw2 as u16).ok_or(InvalidStatusWord) + } + } + + pub fn get_data_length(&self) -> u8 { + self.sw2 + } + + pub fn get_counter(&self) -> u8 { + self.sw2 & 0x0F + } + + pub fn get_correct_le(&self) -> u8 { + self.sw2 + } + + pub fn check(&self) -> Result<()> { + if self.sw1 == 0x91 { + return match FromPrimitive::from_u8(self.sw2) { + Some(APDUStatusWord2::OperationOk) => { Ok(()) } + Some(APDUStatusWord2::NoChanges) => { Ok(()) } + Some(APDUStatusWord2::IllegalCommandCode) => {Err(IllegalCommandCode)} + Some(APDUStatusWord2::IntegrityError) => {Err(IntegrityError)} + Some(APDUStatusWord2::NoSuchKey) => {Err(NoSuchKey)} + Some(APDUStatusWord2::LengthError) => {Err(LengthError)} + Some(APDUStatusWord2::PermissionDenied) => {Err(PermissionDenied)} + Some(APDUStatusWord2::ParameterError) => {Err(ParameterError)} + Some(APDUStatusWord2::AuthenticationDelay) => {Err(AuthenticationDelay)} + Some(APDUStatusWord2::AuthenticationError) => {Err(AuthenticationError)} + Some(APDUStatusWord2::AdditionalFrame) => {Ok(())} + Some(APDUStatusWord2::BoundaryError) => {Err(BoundaryError)} + Some(APDUStatusWord2::CommandAborted) => {Err(CommandAborted)} + Some(APDUStatusWord2::DuplicateError) => {Err(DuplicateError)} + Some(APDUStatusWord2::FileNotFound) => {Err(FileNotFound)} + None => { Err(InvalidStatusWord)} + } + } else { Err(InvalidStatusWord) } + } +} \ No newline at end of file diff --git a/src/iso7816_4/apdustatuswords.rs b/src/iso7816_4/apdustatuswords.rs new file mode 100644 index 0000000..d2c627f --- /dev/null +++ b/src/iso7816_4/apdustatuswords.rs @@ -0,0 +1,153 @@ +use num_derive::FromPrimitive; + +#[derive(FromPrimitive)] +#[repr(u16)] +pub enum APDUStatusWord { + /// Kommando erfolgreich ausgefhrt. xx Datenbytes knnen mit dem GET RESPONSE-Kommando abgeholt werden. Statuswort zur Steuerung des T=0-Protokolls + DataReady = 0x6100, + + /// Die zurckgegebenen Daten knnen fehlerhaft sein. + FaultyData = 0x6281, + + /// Da das Dateiende vorher erreicht wurde, konnten nur weniger als Le Bytes gelesen werden. + UnexpectedEndOfFile = 0x6282, + + /// Die ausgewhlte Datei ist gesperrt (englisch invalidated, wrtlich ungltig). + InvalidatedFile = 0x6283, + + /// Die File Control Information (FCI) ist inkonform zu ISO 7816-4. + FciNotConform = 0x6284, + + /// Warnung; Zustand des nichtflchtigen Speichers nicht verndert + StorageNotChanged = 0x6200, + + /// Zhler hat den Wert x erreicht (die genaue Bedeutung ist vom Kommando abhngig) + CounterReached = 0x63C0, + + /// Warnung; Zustand des nichtflchtigen Speichers verndert + StorageChanged = 0x6300, + + /// Ausfhrungsfehler; Zustand des nichtflchtigen Speichers nicht verndert + ExecutionErrorWithoutChange = 0x6400, + + /// Speicherfehler + MemoryError = 0x6581, + + /// Ausfhrungsfehler; Zustand des nichtflchtigen Speichers verndert + ExecutionErrorWithChange = 0x6500, + + /// Befehlslnge (Lc) oder erwartete Antwortlnge (Le) falsch + InvalidLcLe = 0x6700, + + /// Funktionen im Class-Byte werden nicht untersttzt + ClassFeatureNotSupported = 0x6800, + + /// Logische Kanle werden nicht untersttzt + LogicChannelNotSupported = 0x6881, + + /// Secure Messaging wird nicht untersttzt + SecureMessagingNotSupported = 0x6882, + + /// Kommando nicht erlaubt + CommandNotAllowed = 0x6900, + + /// Kommando inkompatibel zur Dateistruktur + CommandIncompatible = 0x6981, + + /// Sicherheitszustand nicht erfllt + SafetyStatusNotFulfilled = 0x6982, + + /// Authentisierungsmethode ist gesperrt + AuthenticationMethodLocked = 0x6983, + + /// Referenzierte Daten sind gesperrt + ReferencedFileLocked = 0x6984, + + /// Nutzungsbedingungen sind nicht erfllt + TermsOfServiceNotFulfilled = 0x6985, + + /// Kommando nicht erlaubt (kein EF selektiert) + CommandNotAllowedNoEfSelected = 0x6986, + + /// Erwartete Secure-Messaging-Objekte nicht gefunden + ExpectedSecureMessagingObjectsNotFound = 0x6987, + + /// Secure-Messaging-Datenobjekte sind inkorrekt + InvalidSecureMessagingObjects = 0x6988, + + /// Falsche Parameter P1/P2 + WrongParameters = 0x6A00, + + /// Falsche Daten + WrongData = 0x6A80, + + /// Funktion wird nicht untersttzt + FeatureNotSupported = 0x6A81, + + /// Datei wurde nicht gefunden + FileNotFound = 0x6A82, + + /// Datensatz (engl. record) der Datei nicht gefunden + RecordNotFound = 0x6A83, + + /// Nicht gengend Speicherplatz in der Datei + InsufficientSpace = 0x6A84, + + /// Lc nicht konsistent mit der TLV-Struktur + LcTlvInconsistent = 0x6A85, + + /// Inkorrekte Parameter P1/P2 + IncorrectParameters = 0x6A86, + + /// Lc inkonsistent mit P1/P2 + LcParametersInconsistent = 0x6A87, + + /// Referenzierte Daten nicht gefunden + ReferencedFileNotFound = 0x6A88, + + /// Parameter P1/P2 falsch + WrongParameters2 = 0x6B00, + + + /// Falsche Lnge Le; xx gibt die korrekte Lnge an Statuswort zur Steuerung des T=0-Protokolls + InvalidLe = 0x6C00, + + + /// Das Kommando (INS) wird nicht untersttzt + InstructionNotSupported = 0x6D00, + + + /// Die Kommandoklasse (CLA) wird nicht untersttzt + ClassNotSupported = 0x6E00, + + + /// Kommando wurde mit unbekanntem Fehler abgebrochen + UnknownError = 0x6F00, + + /// Kommando erfolgreich ausgefhrt + SUCCESS = 0x9000, + + /// OK + OK = 0x9100, + +} + +#[derive(FromPrimitive)] +#[repr(u8)] +pub enum APDUStatusWord2 { + OperationOk = 0x00, + NoChanges = 0x0C, + IllegalCommandCode = 0x1C, + IntegrityError = 0x1E, + NoSuchKey = 0x40, + LengthError = 0x7E, + PermissionDenied = 0x9D, + ParameterError = 0x9E, + AuthenticationDelay = 0xAD, + AuthenticationError = 0xAE, + AdditionalFrame = 0xAF, + BoundaryError = 0xBE, + CommandAborted = 0xCA, + DuplicateError = 0xDE, + FileNotFound = 0xF0, +} \ No newline at end of file diff --git a/src/iso7816_4/mod.rs b/src/iso7816_4/mod.rs index e69de29..371ce5c 100644 --- a/src/iso7816_4/mod.rs +++ b/src/iso7816_4/mod.rs @@ -0,0 +1,3 @@ +pub mod apducommand; +pub mod apduresponse; +pub mod apdustatuswords; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3d32c68..d5daeb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,27 @@ +use crate::iso7816_4::apduresponse::APDUResponse; +use crate::iso7816_4::apducommand::APDUCommand; +use error::Result; + pub mod crypto; pub mod iso7816_4; pub mod desfire; -pub mod error; \ No newline at end of file +pub mod error; + +pub trait Card { +/// +/// Connect to Smartcard +/// +fn connect(&self); + +/// +/// Disconnect from Smartcard +/// +fn disconnect(&self); + +/// +/// Transmit APDU Command to Smartcard +/// +/// Application Protocol Data Unit Command - ISO 7816 +/// Application Protocol Data Unit Response - ISO 7816 +fn transmit(&self, apdu_cmd: APDUCommand) -> Result; +}