From 1444db7d0977bbf5d55b4e0d02b91f3bd950b199 Mon Sep 17 00:00:00 2001 From: Kai Jan Kriegel Date: Fri, 23 Sep 2022 20:03:45 +0200 Subject: [PATCH] initial support for binary version of FabFire --- bffhd/authentication/fabfire_bin/mod.rs | 44 ++ bffhd/authentication/fabfire_bin/server.rs | 513 +++++++++++++++++++++ bffhd/authentication/mod.rs | 1 + 3 files changed, 558 insertions(+) create mode 100644 bffhd/authentication/fabfire_bin/mod.rs create mode 100644 bffhd/authentication/fabfire_bin/server.rs diff --git a/bffhd/authentication/fabfire_bin/mod.rs b/bffhd/authentication/fabfire_bin/mod.rs new file mode 100644 index 0000000..0c5f271 --- /dev/null +++ b/bffhd/authentication/fabfire_bin/mod.rs @@ -0,0 +1,44 @@ +mod server; +pub use server::FabFire; + +use rsasl::mechname::Mechname; +use rsasl::registry::{Mechanism, MECHANISMS}; +use rsasl::session::Side; + +const MECHNAME: &'static Mechname = &Mechname::const_new_unchecked(b"X-FABFIRE-BIN"); + +#[linkme::distributed_slice(MECHANISMS)] +pub static FABFIRE_BIN: Mechanism = Mechanism { + mechanism: MECHNAME, + priority: 300, + // In this situation there's one struct for both sides, however you can just as well use + // different types than then have different `impl Authentication` instead of checking a value + // in self. + client: None, + server: Some(FabFire::new_server), + first: Side::Client, +}; + +use rsasl::property::{Property, PropertyDefinition, PropertyQ}; +use std::marker::PhantomData; +// All Property types must implement Debug. +#[derive(Debug)] +// The `PhantomData` in the constructor is only used so external crates can't construct this type. +pub struct FabFireCardKey(PhantomData<()>); +impl PropertyQ for FabFireCardKey { + // This is the type stored for this property. This could also be the struct itself if you + // so choose + type Item = [u8; 16]; + // You need to return the constant you define below here for things to work properly + fn property() -> Property { + FABFIRECARDKEY + } +} +// This const is used by your mechanism to query and by your users to set your property. It +// thus needs to be exported from your crate +pub const FABFIRECARDKEY: Property = Property::new(&PropertyDefinition::new( + // Short name, used in `Debug` output + "FabFireCardKey", + // A longer user-facing name used in `Display` output + "A AES128 key for a FabFire card", +)); diff --git a/bffhd/authentication/fabfire_bin/server.rs b/bffhd/authentication/fabfire_bin/server.rs new file mode 100644 index 0000000..e4695d6 --- /dev/null +++ b/bffhd/authentication/fabfire_bin/server.rs @@ -0,0 +1,513 @@ +use desfire::desfire::desfire::MAX_BYTES_PER_TRANSACTION; +use desfire::desfire::Desfire; +use desfire::error::Error as DesfireError; +use desfire::iso7816_4::apduresponse::APDUResponse; +use rsasl::error::{MechanismError, MechanismErrorKind, SASLError, SessionError}; +use rsasl::mechanism::Authentication; +use rsasl::property::AuthId; +use rsasl::session::{SessionData, StepResult}; +use rsasl::SASL; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt::{Debug, Display, Formatter}; +use std::io::Write; +use std::sync::Arc; + +use crate::authentication::fabfire::FabFireCardKey; + +enum FabFireError { + ParseError, + SerializationError, + DeserializationError(serde_json::Error), + CardError(DesfireError), + InvalidMagic(String), + InvalidToken(String), + InvalidURN(String), + InvalidCredentials(String), + Session(SessionError), +} + +impl Debug for FabFireError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FabFireError::ParseError => write!(f, "ParseError"), + FabFireError::SerializationError => write!(f, "SerializationError"), + FabFireError::DeserializationError(e) => write!(f, "DeserializationError: {}", e), + FabFireError::CardError(err) => write!(f, "CardError: {}", err), + FabFireError::InvalidMagic(magic) => write!(f, "InvalidMagic: {}", magic), + FabFireError::InvalidToken(token) => write!(f, "InvalidToken: {}", token), + FabFireError::InvalidURN(urn) => write!(f, "InvalidURN: {}", urn), + FabFireError::InvalidCredentials(credentials) => { + write!(f, "InvalidCredentials: {}", credentials) + } + FabFireError::Session(err) => write!(f, "Session: {}", err), + } + } +} + +impl Display for FabFireError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FabFireError::ParseError => write!(f, "ParseError"), + FabFireError::SerializationError => write!(f, "SerializationError"), + FabFireError::DeserializationError(e) => write!(f, "DeserializationError: {}", e), + FabFireError::CardError(err) => write!(f, "CardError: {}", err), + FabFireError::InvalidMagic(magic) => write!(f, "InvalidMagic: {}", magic), + FabFireError::InvalidToken(token) => write!(f, "InvalidToken: {}", token), + FabFireError::InvalidURN(urn) => write!(f, "InvalidURN: {}", urn), + FabFireError::InvalidCredentials(credentials) => { + write!(f, "InvalidCredentials: {}", credentials) + } + FabFireError::Session(err) => write!(f, "Session: {}", err), + } + } +} + +impl MechanismError for FabFireError { + fn kind(&self) -> MechanismErrorKind { + match self { + FabFireError::ParseError => MechanismErrorKind::Parse, + FabFireError::SerializationError => MechanismErrorKind::Protocol, + FabFireError::DeserializationError(_) => MechanismErrorKind::Parse, + FabFireError::CardError(_) => MechanismErrorKind::Protocol, + FabFireError::InvalidMagic(_) => MechanismErrorKind::Protocol, + FabFireError::InvalidToken(_) => MechanismErrorKind::Protocol, + FabFireError::InvalidURN(_) => MechanismErrorKind::Protocol, + FabFireError::InvalidCredentials(_) => MechanismErrorKind::Protocol, + FabFireError::Session(_) => MechanismErrorKind::Protocol, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct CardInfo { + #[serde(rename = "UID", with = "hex")] + uid: [u8; 7], + key_old: Option>, + key_new: Option>, +} + +struct KeyInfo { + key_id: u8, + key: Box<[u8]>, +} + +struct AuthInfo { + rnd_a: Vec, + rnd_b: Vec, + iv: Vec, +} + +enum Step { + New, + SelectApp, + VerifyMagic, + GetURN, + GetToken, + Authenticate1, + Authenticate2, +} + +pub struct FabFire { + step: Step, + card_info: Option, + key_info: Option, + auth_info: Option, + app_id: u32, + local_urn: String, + desfire: Desfire, +} + +const MAGIC: &'static str = "FABACCESS\0DESFIRE\01.0\0"; + +impl FabFire { + pub fn new_server(_sasl: &SASL) -> Result, SASLError> { + Ok(Box::new(Self { + step: Step::New, + card_info: None, + key_info: None, + auth_info: None, + app_id: 0x464142, + local_urn: "urn:fabaccess:lab:innovisionlab".to_string(), + desfire: Desfire { + card: None, + session_key: None, + cbc_iv: None, + }, + })) + } +} + +impl Authentication for FabFire { + fn step( + &mut self, + session: &mut SessionData, + input: Option<&[u8]>, + writer: &mut dyn Write, + ) -> StepResult { + match self.step { + Step::New => { + tracing::trace!("Step: New"); + //receive card info (especially card UID) from reader + return match input { + None => Err(SessionError::InputDataRequired), + Some(_) => { + //select application + return match self.desfire.select_application_cmd(self.app_id) { + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => { + self.step = Step::SelectApp; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!( + "Failed to convert APDUCommand to Vec: {:?}", + e + ); + return Err(FabFireError::SerializationError.into()); + } + }, + Err(e) => { + tracing::error!("Failed to generate APDUCommand: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }; + } + }; + } + Step::SelectApp => { + tracing::trace!("Step: SelectApp"); + // check that we successfully selected the application + + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + apdu_response + .check() + .map_err(|e| FabFireError::CardError(e))?; + + // request the contents of the file containing the magic string + const MAGIC_FILE_ID: u8 = 0x01; + + return match self + .desfire + .read_data_chunk_cmd(MAGIC_FILE_ID, 0, MAGIC.len()) + { + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => { + self.step = Step::VerifyMagic; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!("Failed to convert APDUCommand to Vec: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }, + Err(e) => { + tracing::error!("Failed to generate APDUCommand: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }; + } + Step::VerifyMagic => { + tracing::trace!("Step: VerifyMagic"); + // verify the magic string to determine that we have a valid fabfire card + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + match apdu_response.check() { + Ok(_) => { + match apdu_response.body { + Some(data) => { + if std::str::from_utf8(data.as_slice()) != Ok(MAGIC) { + tracing::error!("Invalid magic string"); + return Err(FabFireError::ParseError.into()); + } + } + None => { + tracing::error!("No data returned from card"); + return Err(FabFireError::ParseError.into()); + } + }; + } + Err(e) => { + tracing::error!("Got invalid APDUResponse: {:?}", e); + return Err(FabFireError::ParseError.into()); + } + } + + // request the contents of the file containing the URN + const URN_FILE_ID: u8 = 0x02; + + return match self.desfire.read_data_chunk_cmd( + URN_FILE_ID, + 0, + self.local_urn.as_bytes().len(), + ) { + // TODO: support urn longer than 47 Bytes + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => { + self.step = Step::GetURN; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!("Failed to convert APDUCommand to Vec: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }, + Err(e) => { + tracing::error!("Failed to generate APDUCommand: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }; + } + Step::GetURN => { + tracing::trace!("Step: GetURN"); + // parse the urn and match it to our local urn + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + match apdu_response.check() { + Ok(_) => { + match apdu_response.body { + Some(data) => { + let received_urn = String::from_utf8(data).unwrap(); + if received_urn != self.local_urn { + tracing::error!( + "URN mismatch: {:?} != {:?}", + received_urn, + self.local_urn + ); + return Err(FabFireError::ParseError.into()); + } + } + None => { + tracing::error!("No data returned from card"); + return Err(FabFireError::ParseError.into()); + } + }; + } + Err(e) => { + tracing::error!("Got invalid APDUResponse: {:?}", e); + return Err(FabFireError::ParseError.into()); + } + } + // request the contents of the file containing the URN + const TOKEN_FILE_ID: u8 = 0x03; + + let buf = match self.desfire.read_data_chunk_cmd( + TOKEN_FILE_ID, + 0, + MAX_BYTES_PER_TRANSACTION, + ) { + // TODO: support data longer than 47 Bytes + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => { + self.step = Step::GetToken; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!("Failed to convert APDUCommand to Vec: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }, + Err(e) => { + tracing::error!("Failed to generate APDUCommand: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }; + } + Step::GetToken => { + // println!("Step: GetToken"); + // parse the token and select the appropriate user + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + match apdu_response.check() { + Ok(_) => { + match apdu_response.body { + Some(data) => { + let token = String::from_utf8(data).unwrap(); + session.set_property::(Arc::new( + token.trim_matches(char::from(0)).to_string(), + )); + let key = match session.get_property_or_callback::() + { + Ok(Some(key)) => Box::from(key.as_slice()), + Ok(None) => { + tracing::error!("No keys on file for token"); + return Err(FabFireError::InvalidCredentials( + "No keys on file for token".to_string(), + ) + .into()); + } + Err(e) => { + tracing::error!("Failed to get key: {:?}", e); + return Err(FabFireError::Session(e).into()); + } + }; + self.key_info = Some(KeyInfo { key_id: 0x01, key }); + } + None => { + tracing::error!("No data in response"); + return Err(FabFireError::ParseError.into()); + } + }; + } + Err(e) => { + tracing::error!("Failed to check response: {:?}", e); + return Err(FabFireError::ParseError.into()); + } + } + + return match self + .desfire + .authenticate_iso_aes_challenge_cmd(self.key_info.as_ref().unwrap().key_id) + { + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => { + self.step = Step::Authenticate1; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!("Failed to convert to Vec: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }, + Err(e) => { + tracing::error!("Failed to create authenticate command: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + }; + } + Step::Authenticate1 => { + tracing::trace!("Step: Authenticate1"); + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + return match apdu_response.check() { + Ok(_) => { + match apdu_response.body { + Some(data) => { + let rnd_b_enc = data.as_slice(); + + //FIXME: This is ugly, we should find a better way to make the function testable + //TODO: Check if we need a CSPRNG here + let rnd_a: [u8; 16] = rand::random(); + + let (cmd_challenge_response, rnd_b, iv) = self + .desfire + .authenticate_iso_aes_response_cmd( + rnd_b_enc, + &*(self.key_info.as_ref().unwrap().key), + &rnd_a, + ) + .unwrap(); + self.auth_info = Some(AuthInfo { + rnd_a: Vec::::from(rnd_a), + rnd_b, + iv, + }); + match Vec::::try_from(cmd_challenge_response) { + Ok(data) => { + self.step = Step::Authenticate2; + writer + .write_all(&data) + .map_err(|e| SessionError::Io { source: e })?; + Ok(rsasl::session::Step::NeedsMore(Some(send_buf.len()))) + }, + Err(e) => { + tracing::error!("Failed to convert to Vec: {:?}", e); + return Err(FabFireError::SerializationError.into()); + } + } + } + None => { + tracing::error!("Got invalid response: {:?}", apdu_response); + Err(FabFireError::ParseError.into()) + } + } + } + Err(e) => { + tracing::error!("Failed to check response: {:?}", e); + Err(FabFireError::ParseError.into()) + } + } + } + Step::Authenticate2 => { + // println!("Step: Authenticate2"); + let apdu_response = match input { + Some(data) => APDUResponse::new(data), + None => return Err(SessionError::InputDataRequired), + }; + + match apdu_response.check() { + Ok(_) => { + match apdu_response.body { + Some(data) => match self.auth_info.as_ref() { + None => { + return Err(FabFireError::ParseError.into()); + } + Some(auth_info) => { + if self + .desfire + .authenticate_iso_aes_verify( + data.as_slice(), + auth_info.rnd_a.as_slice(), + auth_info.rnd_b.as_slice(), + &*(self.key_info.as_ref().unwrap().key), + auth_info.iv.as_slice(), + ) + .is_ok() + { + return Ok(rsasl::session::Step::Done(Some( + send_buf.len(), + ))); + } + }, + }, + None => { + tracing::error!("got empty response"); + return Err(FabFireError::ParseError.into()); + } + }; + } + Err(_e) => { + tracing::error!("Got invalid response: {:?}", apdu_response); + return Err( + FabFireError::InvalidCredentials(format!("{}", apdu_response)).into(), + ); + } + } + } + } + + return Ok(rsasl::session::Step::Done(None)); + } +} diff --git a/bffhd/authentication/mod.rs b/bffhd/authentication/mod.rs index 68ff703..7c94a25 100644 --- a/bffhd/authentication/mod.rs +++ b/bffhd/authentication/mod.rs @@ -11,6 +11,7 @@ use crate::authentication::fabfire::FabFireCardKey; use crate::users::db::User; mod fabfire; +mod fabfire_bin; struct Callback { users: Users,