initial support for binary version of FabFire

This commit is contained in:
Kai Jan Kriegel 2022-09-23 20:03:45 +02:00
parent 0d2cd6f376
commit 1444db7d09
3 changed files with 558 additions and 0 deletions

View File

@ -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",
));

View File

@ -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<Box<[u8]>>,
key_new: Option<Box<[u8]>>,
}
struct KeyInfo {
key_id: u8,
key: Box<[u8]>,
}
struct AuthInfo {
rnd_a: Vec<u8>,
rnd_b: Vec<u8>,
iv: Vec<u8>,
}
enum Step {
New,
SelectApp,
VerifyMagic,
GetURN,
GetToken,
Authenticate1,
Authenticate2,
}
pub struct FabFire {
step: Step,
card_info: Option<CardInfo>,
key_info: Option<KeyInfo>,
auth_info: Option<AuthInfo>,
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<Box<dyn Authentication>, 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::<u8>::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<u8>: {:?}",
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::<u8>::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<u8>: {:?}", 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::<u8>::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<u8>: {:?}", 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::<u8>::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<u8>: {:?}", 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::<AuthId>(Arc::new(
token.trim_matches(char::from(0)).to_string(),
));
let key = match session.get_property_or_callback::<FabFireCardKey>()
{
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::<u8>::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<u8>: {:?}", 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::<u8>::from(rnd_a),
rnd_b,
iv,
});
match Vec::<u8>::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<u8>: {:?}", 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));
}
}

View File

@ -11,6 +11,7 @@ use crate::authentication::fabfire::FabFireCardKey;
use crate::users::db::User; use crate::users::db::User;
mod fabfire; mod fabfire;
mod fabfire_bin;
struct Callback { struct Callback {
users: Users, users: Users,