diff --git a/Cargo.lock b/Cargo.lock index 17a5241..909cc1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,18 @@ dependencies = [ "pretty", ] +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug 0.3.0", +] + [[package]] name = "annotate-snippets" version = "0.9.1" @@ -253,7 +265,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding", + "block-padding 0.1.5", "byte-tools", "byteorder", "generic-array 0.12.4", @@ -277,6 +289,16 @@ dependencies = [ "generic-array 0.14.5", ] +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding 0.2.1", + "cipher", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -286,6 +308,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "blocking" version = "1.1.0" @@ -378,6 +406,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.5", +] + [[package]] name = "clap" version = "2.34.0" @@ -447,6 +484,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "des" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac41dd49fb554432020d52c875fc290e110113f864c6b1b525cd62c7e7747a5d" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "desfire" +version = "0.1.0" +source = "git+https://gitlab.com/fabinfra/fabaccess/nfc_rs.git?branch=main#34d1d7f3a062f007fcdc229995f9013970e460a5" +dependencies = [ + "aes", + "block-modes", + "des", + "hex", + "num-derive", + "num-traits", + "rand", + "simple-error", +] + [[package]] name = "dhall" version = "0.11.0" @@ -497,6 +560,7 @@ dependencies = [ "capnp-rpc", "capnpc", "clap", + "desfire", "easy-parallel", "flexbuffers", "futures 0.3.21", @@ -504,8 +568,10 @@ dependencies = [ "futures-test", "futures-util", "genawaiter", + "hex", "lazy_static", "libc", + "linkme", "lmdb-rkv", "rand", "rsasl", @@ -1131,6 +1197,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -1703,6 +1789,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-error" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc47a29ce97772ca5c927f75bac34866b16d64e07f330c3248e2d7226623901b" + [[package]] name = "slab" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 4a5223c..03f35d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,8 +47,8 @@ uuid = { version = "0.8.2", features = ["serde", "v4"] } clap = "2.33.3" # TODO update this if bindgen breaks (again) -rsasl = "2.0.0-preview2" -#rsasl = { path = "../../rsasl" } +rsasl = { version = "2.0.0-preview2", features = ["unstable_custom_mechanism", "registry_static"] } +#rsasl = { path = "../../rsasl", features = ["unstable_custom_mechanism", "registry_static"] } rumqttc = { version = "0.10", features = ["url"] } async-compat = "0.2.1" @@ -75,6 +75,11 @@ rustls = "0.19" rustls-pemfile = "0.2" async-rustls = "0.2" +# Desfire +desfire = { git = "https://gitlab.com/fabinfra/fabaccess/nfc_rs.git", branch = "main" } +hex = "0.4.3" +linkme = "0.2" + [build-dependencies] capnpc = "0.14.4" # Used in build.rs to iterate over all files in schema/ diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..9a52f58 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,7 @@ +# Setup build image for multistage build +FROM rust:latest +# install build deps +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -yqq --no-install-recommends capnproto build-essential cmake clang libclang-dev libgsasl7-dev + +COPY ../nfc_rs /nfc_rs \ No newline at end of file diff --git a/src/api/auth.rs b/src/api/auth.rs index 662d3d5..8f260ba 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -30,6 +30,9 @@ use crate::db::Databases; use crate::db::user::{Internal as UserDB, User}; use crate::db::access::AccessControl as AccessDB; +mod fabfire; +use fabfire::FABFIRE; + pub struct AppData { userdb: Arc, } @@ -83,6 +86,7 @@ pub struct Auth { impl Auth { pub fn new(log: Logger, dbs: Databases, session: Rc>>) -> Self { let mut ctx = SASL::new(); + ctx.register(&FABFIRE); ctx.install_callback(Arc::new(CB::new(dbs.userdb.clone()))); Self { log, ctx, session, userdb: dbs.userdb.clone(), access: dbs.access.clone() } @@ -111,9 +115,10 @@ impl authentication_system::Server for Auth { for (i, m) in mechvec.into_iter().enumerate() { res_mechs.set(i as u32, m); }*/ - // For now, only PLAIN - let mut res_mechs = res.get().init_mechs(1); + // For now, only PLAIN and X-FABFIRE + let mut res_mechs = res.get().init_mechs(2); res_mechs.set(0, "PLAIN"); + res_mechs.set(1, "X-FABFIRE"); Promise::ok(()) } @@ -128,7 +133,7 @@ impl authentication_system::Server for Auth { // Extract the MECHANISM the client wants to use and start a session. // Or fail at that and thrown an exception TODO: return Outcome let mech = pry!(req.get_mechanism()); - if pry!(req.get_mechanism()) != "PLAIN" { + if mech != "PLAIN" || mech != "X-FABFIRE" { return Promise::err(capnp::Error { kind: capnp::ErrorKind::Failed, description: format!("Invalid SASL mech"), diff --git a/src/api/auth/fabfire.rs b/src/api/auth/fabfire.rs new file mode 100644 index 0000000..9c9c43d --- /dev/null +++ b/src/api/auth/fabfire.rs @@ -0,0 +1,20 @@ +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"); + +#[linkme::distributed_slice(MECHANISMS)] +pub static FABFIRE: 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, +}; diff --git a/src/api/auth/fabfire/server.rs b/src/api/auth/fabfire/server.rs new file mode 100644 index 0000000..ed76e82 --- /dev/null +++ b/src/api/auth/fabfire/server.rs @@ -0,0 +1,420 @@ +use std::fmt::{Debug, Display, Formatter}; +use std::io::Write; +use rsasl::error::{MechanismError, MechanismErrorKind, SASLError, SessionError}; +use rsasl::mechanism::Authentication; +use rsasl::SASL; +use rsasl::session::{SessionData, StepResult}; +use serde::{Deserialize, Serialize}; +use desfire::desfire::Desfire; +use desfire::iso7816_4::apducommand::APDUCommand; +use desfire::iso7816_4::apduresponse::APDUResponse; +use desfire::error::{Error as DesfireError, Error}; +use std::convert::TryFrom; +use std::ops::Deref; + +enum FabFireError { + ParseError, + SerializationError, + CardError(DesfireError), +} + +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::CardError(err) => write!(f, "CardError: {}", 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::CardError(err) => write!(f, "CardError: {}", err), + } + } +} + +impl MechanismError for FabFireError { + fn kind(&self) -> MechanismErrorKind { + match self { + FabFireError::ParseError => MechanismErrorKind::Parse, + FabFireError::SerializationError => MechanismErrorKind::Protocol, + FabFireError::CardError(_) => MechanismErrorKind::Protocol, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct CardInfo { + card_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 +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "Cmd")] +enum CardCommand { + message { + msg_id: Option, + clr_txt: Option, + addn_txt: Option, + }, + sendPICC { + data: String + }, + haltPICC, + Key { + data: String + }, + ConfirmUser +} + +enum Step { + New, + SelectApp, + VerifyMagic, + GetURN, + GetToken, + Authenticate1, + Authenticate2, + Authenticate3, +} + +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: 1, 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 => { + //receive card info (especially card UID) from reader + return match input { + None => { Err(SessionError::InputDataRequired) }, + Some(cardinfo) => { + self.card_info = match serde_json::from_slice(cardinfo) { + Ok(card_info) => Some(card_info), + Err(_) => { + return Err(FabFireError::ParseError.into()) + } + }; + self.step = Step::SelectApp; + Ok(rsasl::session::Step::NeedsMore(None)) + } + } + } + Step::SelectApp => { + //select application + let buf = match self.desfire.select_application_cmd(self.app_id) { + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::VerifyMagic; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + } + Step::VerifyMagic => { + // check that we successfully selected the application + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + response.check().map_err(|e| FabFireError::CardError(e))?; + + // request the contents of the file containing the magic string + const MAGIC_FILE_ID: u8 = 0x01; + + let buf = match self.desfire.read_data_chunk_cmd(MAGIC_FILE_ID, 0, MAGIC.len()) { + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::GetURN; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + } + Step::GetURN => { + // verify the magic string to determine that we have a valid fabfire card + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + match response.check() { + Ok(_) => { + match response.body { + Some(data) => { + if std::str::from_utf8(data.as_slice()) != Ok(MAGIC) { + return Err(FabFireError::ParseError.into()); + } + } + None => { + return Err(FabFireError::ParseError.into()) + } + }; + } + Err(_) => { + return Err(FabFireError::ParseError.into()); + } + } + + + // request the contents of the file containing the URN + const URN_FILE_ID: u8 = 0x02; + + let buf = 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) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::GetToken; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + } + Step::GetToken => { + // parse the urn and match it to our local urn + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + match response.check() { + Ok(_) => { + match response.body { + Some(data) => { + if String::from_utf8(data).unwrap() != self.local_urn { + return Err(FabFireError::ParseError.into()); + } + } + None => { + return Err(FabFireError::ParseError.into()) + } + }; + } + Err(_) => { + 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, 47) { // TODO: support data longer than 47 Bytes + Ok(buf) => match Vec::::try_from(buf) { + Ok(data) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::Authenticate1; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + } + Step::Authenticate1 => { + // parse the token and select the appropriate user + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + match response.check() { + Ok(_) => { + match response.body { + Some(data) => { + if String::from_utf8(data).unwrap() != "LoremIpsum" { // FIXME: match against user db + return Err(FabFireError::ParseError.into()); + } + } + None => { + return Err(FabFireError::ParseError.into()) + } + }; + } + Err(_) => { + return Err(FabFireError::ParseError.into()); + } + } + + let buf = 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) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::Authenticate2; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + + } + Step::Authenticate2 => { + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + match response.check() { + Ok(_) => { + match 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(); + println!("RND_A: {:x?}", rnd_a); + + 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}); + let buf = match Vec::::try_from(cmd_challenge_response) { + Ok(data) => data, + Err(_) => { + return Err(FabFireError::SerializationError.into()) + } + }; + let cmd = CardCommand::sendPICC { data: hex::encode_upper(buf) }; + return match serde_json::to_writer(writer, &cmd) { + Ok(_) => { + self.step = Step::Authenticate3; + Ok(rsasl::session::Step::NeedsMore(None)) + } + Err(_) => { + Err(FabFireError::SerializationError.into()) + } + } + } + None => { + return Err(FabFireError::ParseError.into()) + } + }; + } + Err(_) => { + return Err(FabFireError::ParseError.into()); + } + } + } + Step::Authenticate3 => { + let response = match input { + None => {return Err(SessionError::InputDataRequired)}, + Some(buf) => APDUResponse::new(buf) + }; + match response.check() { + Ok(_) => { + match 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() { + // TODO: Do stuff with the info that we are authenticated + return Ok(rsasl::session::Step::Done(None)); + } + } + } + } + None => { + return Err(FabFireError::ParseError.into()) + } + }; + } + Err(_) => { + return Err(FabFireError::ParseError.into()); + } + } + } + } + + return Ok(rsasl::session::Step::Done(None)); + } + +} \ No newline at end of file