From de535c49e760abc21c39dd85522a6c0bceac8d48 Mon Sep 17 00:00:00 2001 From: shimun Date: Fri, 3 Apr 2020 15:08:56 +0200 Subject: [PATCH] added test subcommand --- Cargo.lock | 7 ++- Cargo.toml | 5 +- src/lib.rs | 146 +-------------------------------------------- src/main.rs | 98 ++++++++++++++++++++---------- src/pamfido2.rs | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+), 179 deletions(-) create mode 100644 src/pamfido2.rs diff --git a/Cargo.lock b/Cargo.lock index 0e4fedf..24ce716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,7 +191,7 @@ dependencies = [ [[package]] name = "ctap_hmac" version = "0.3.0" -source = "git+https://git.shimun.net/shimun/ctap?rev=1a8e83d81efa3ae2cd4d764ddbae72c95a5af3e1#1a8e83d81efa3ae2cd4d764ddbae72c95a5af3e1" +source = "git+https://git.shimun.net/shimun/ctap?rev=65ef57403182fce13b5266ba7838558ba2ad008f#65ef57403182fce13b5266ba7838558ba2ad008f" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "cbor-codec 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -405,8 +405,9 @@ dependencies = [ name = "pam_fido2" version = "0.2.2" dependencies = [ - "ctap_hmac 0.3.0 (git+https://git.shimun.net/shimun/ctap?rev=1a8e83d81efa3ae2cd4d764ddbae72c95a5af3e1)", + "ctap_hmac 0.3.0 (git+https://git.shimun.net/shimun/ctap?rev=65ef57403182fce13b5266ba7838558ba2ad008f)", "ctrlc 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "pamsm 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -891,7 +892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" "checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" "checksum csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -"checksum ctap_hmac 0.3.0 (git+https://git.shimun.net/shimun/ctap?rev=1a8e83d81efa3ae2cd4d764ddbae72c95a5af3e1)" = "" +"checksum ctap_hmac 0.3.0 (git+https://git.shimun.net/shimun/ctap?rev=65ef57403182fce13b5266ba7838558ba2ad008f)" = "" "checksum ctrlc 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7a4ba686dff9fa4c1c9636ce1010b0cf98ceb421361b0bb3d6faeec43bd217a7" "checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" "checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" diff --git a/Cargo.toml b/Cargo.toml index b5c138d..1bd9fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,16 @@ path = "src/lib.rs" crate-type = ["cdylib"] [[bin]] -name = "pam_fido2-credential" +name = "pam_fido2-cli" path = "src/main.rs" [dependencies] -ctap_hmac = { git = "https://git.shimun.net/shimun/ctap", rev = "1a8e83d81efa3ae2cd4d764ddbae72c95a5af3e1", features = ["assert_devices"] } +ctap_hmac = { git = "https://git.shimun.net/shimun/ctap", rev = "65ef57403182fce13b5266ba7838558ba2ad008f", features = ["request_multiple"] } pamsm = "0.2.0" regex = "1.3.1" rand = "0.7.2" hex = "0.4.0" structopt = "0.3.11" ctrlc = "3.1.4" +failure = "0.1.6" diff --git a/src/lib.rs b/src/lib.rs index 7166a0f..a7ce442 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,149 +1,7 @@ #[macro_use] extern crate pamsm; extern crate ctap_hmac as ctap; -use ctap::{get_assertion_devices, FidoAssertionRequestBuilder, FidoDevice, FidoErrorKind}; -use pamsm::{Pam, PamError, PamFlag, PamServiceModule}; -use rand::Rng; -use regex::Regex; -use std::collections::HashMap; -use std::fs::File; -use std::io::{prelude::*, BufReader}; -use std::path::Path; -use std::time::{Duration, SystemTime}; - -struct PamFido2; - -struct Settings { - pub device_timeout: Duration, - pub user_credentials: Vec<(String, String)>, -} - -impl Default for Settings { - fn default() -> Self { - Settings { - device_timeout: Duration::from_secs(15), - user_credentials: vec![("091566e43802c5a29971c1e08d7865d959af862cc28af22dacf413ac26b90f6dea7d1ac491d9d3712c63f7b8d6cfadf86d057d099d382246dbe9c87f133ed167881b65030000".into(),".*".into())], - } - } -} - -impl Settings { - pub fn from_args(args: Vec) -> Self { - let args = args - .into_iter() - .filter_map(|arg| { - let mut parts = arg.split("="); - parts - .by_ref() - .next() - .map(|key| (key.to_string(), parts.collect::>().join("="))) - }) - .collect::>(); - let mut settings = Settings::load( - args.get("authfile") - .map(AsRef::as_ref) - .unwrap_or("/etc/fido2_pam.conf"), - ); - let timeout = args - .get("timeout") - .map(|t| { - t.parse::() - .expect("invalid config timeout is supposed to be an int") - }) - .map(Duration::from_secs); - if let Some(timeout) = timeout { - settings.device_timeout = timeout; - }; - settings - } - pub fn load(path: impl AsRef) -> Settings { - let mut creds = Vec::new(); - let file = File::open(path.as_ref()).unwrap(); - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = line.unwrap(); - let mut parts = line.split(":"); - if let Some(user) = parts.by_ref().next() { - creds.push(( - (&parts.collect::>()[..].join(":")).to_string(), - user.to_string(), - )); - } - } - - Settings { - user_credentials: creds, - ..Default::default() - } - } - - pub fn get_credentials(&self, user: &str) -> Vec { - let mut creds = Vec::new(); - for (cred, pattern) in self.user_credentials.iter() { - let re = Regex::new(&pattern).expect(&["Invalid regex pattern:", &pattern].join(" ")); - if re.is_match(user) { - let mut parts = cred.split(":"); - //TODO: use expect - let id = parts.by_ref().next().unwrap(); - let key = parts.by_ref().next().unwrap(); - creds.push(ctap::FidoCredential { - id: hex::decode(id).unwrap(), - public_key: hex::decode(key).ok(), - }); - } - } - creds - } -} - -impl PamServiceModule for PamFido2 { - fn authenticate(self: &Self, pamh: Pam, _: PamFlag, args: Vec) -> PamError { - let settings = Settings::from_args(args); - let begin = SystemTime::now(); - let challenge = rand::thread_rng().gen::<[u8; 32]>(); - let credentials = settings.get_credentials( - &pamh - .get_cached_user() - .ok() - .map(|name| name.unwrap().to_str().unwrap().to_string()) - .expect("Faied to get username"), - ); - if credentials.is_empty() { - return PamError::AUTH_ERR; - } - let slice = credentials.iter().collect::>(); - let request = FidoAssertionRequestBuilder::default() - .rp_id("fido2pam") - .client_data_hash(&challenge[..]) - .credentials(&slice[..]) - .build() - .unwrap(); - loop { - let mut devices = match ctap::get_devices() { - Ok(devices) => devices - .filter_map(|handle| FidoDevice::new(&handle).ok()) - .collect::>(), - Err(_) => return PamError::AUTH_ERR, - }; - match get_assertion_devices(&request, devices.iter_mut()) { - Ok(_) => return PamError::SUCCESS, - Err(_) if begin.elapsed().unwrap() > settings.device_timeout => { - return PamError::AUTH_ERR - } - Err(e) if e.kind() == FidoErrorKind::DeviceUnsupported => continue, - Err(e) => eprintln!("{:?}", e), - } - } - } -} +mod pamfido2; +use pamfido2::PamFido2; pamsm_init!(Box::new(PamFido2)); - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/src/main.rs b/src/main.rs index 15fa730..b939610 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,47 +1,85 @@ +#[macro_use] +extern crate pamsm; extern crate ctap_hmac as ctap; -use ctap::{FidoCredentialRequestBuilder, FidoDevice}; +mod pamfido2; + +use ctap::{make_credential_devices, FidoCredentialRequestBuilder, FidoDevice}; use hex; -use std::env::args; +use pamfido2::PamFido2; + +use failure::ResultExt; +use std::error::Error; use std::io::stdout; use std::io::Write; +use std::path::PathBuf; use structopt; use structopt::StructOpt; #[derive(Debug, StructOpt)] #[structopt(name = "fido2pam")] struct CliOpt { - /// Whether the authenticator should promt the user for verification - #[structopt(short, long = "user-verification")] - uv: bool, - - /// Username for which the credential will be requested - username: String, + #[structopt(subcommand)] + action: Action, } -fn main() -> Result<(), ctap::FidoError> { - let opt = CliOpt::from_args(); - let device_info = ctap::get_devices()?.next().expect("no device"); - let mut device = ctap::FidoDevice::new(&device_info)?; +#[derive(Debug, StructOpt)] +enum Action { + /// Generate a new credential + Credential { + /// Whether the authenticator should promt the user for verification + #[structopt(short, long = "user-verification")] + uv: bool, - let req = FidoCredentialRequestBuilder::default() - .rp_id("fido2pam") - .user_name(opt.username.as_ref()) - .uv(opt.uv) - .build() - .unwrap(); + /// Username for which the credential will be requested + username: String, + }, + /// Test your config + Test { + /// Path to the file containing the credentials generated by `credential` + auth_file: PathBuf, + /// The username to test for + username: String, + }, +} - let cred = device.make_credential(&req)?; +fn main() -> Result<(), Box> { + match CliOpt::from_args().action { + Action::Credential { uv, username } => { + let mut devices = ctap::get_devices() + .compat()? + .filter_map(|handle| FidoDevice::new(&handle).ok()) + .collect::>(); + let req = FidoCredentialRequestBuilder::default() + .rp_id("fido2pam") + .user_name(username.as_ref()) + .uv(uv) + .build() + .unwrap(); - stdout() - .write_all( - &[ - &opt.username, - &hex::encode(&cred.id)[..], - &hex::encode(&cred.public_key.unwrap())[..], - ] - .join(":") - .as_bytes(), - ) - .unwrap(); + let cred = make_credential_devices(&req, devices.iter_mut()).compat()?; + + stdout() + .write_all( + &[ + &username, + &hex::encode(&cred.id)[..], + &hex::encode(&cred.public_key.unwrap())[..], + ] + .join(":") + .as_bytes(), + ) + .unwrap(); + } + Action::Test { + auth_file: authfile, + username, + } => { + let res = PamFido2.authenticate( + &username, + vec![["authfile=", authfile.to_str().unwrap()].join("")], + )?; + println!("{}", res) + } + }; Ok(()) } diff --git a/src/pamfido2.rs b/src/pamfido2.rs new file mode 100644 index 0000000..470314b --- /dev/null +++ b/src/pamfido2.rs @@ -0,0 +1,154 @@ +use ctap::{get_assertion_devices, FidoAssertionRequestBuilder, FidoDevice, FidoErrorKind}; +use pamsm::{Pam, PamError, PamFlag, PamServiceModule}; +use rand::Rng; +use regex::Regex; +use std::collections::HashMap; +use std::error::Error; +use std::fs::File; +use std::io::{self, prelude::*, BufReader}; +use std::path::Path; +use std::time::{Duration, SystemTime}; + +use failure::ResultExt; + +pub struct PamFido2; + +struct Settings { + pub device_timeout: Duration, + pub user_credentials: Vec<(String, String)>, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + device_timeout: Duration::from_secs(15), + user_credentials: vec![("091566e43802c5a29971c1e08d7865d959af862cc28af22dacf413ac26b90f6dea7d1ac491d9d3712c63f7b8d6cfadf86d057d099d382246dbe9c87f133ed167881b65030000".into(),".*".into())], + } + } +} + +impl Settings { + pub fn from_args(args: Vec) -> Result> { + let args = args + .into_iter() + .filter_map(|arg| { + let mut parts = arg.split("="); + parts + .by_ref() + .next() + .map(|key| (key.to_string(), parts.collect::>().join("="))) + }) + .collect::>(); + let mut settings = Settings::load( + args.get("authfile") + .map(AsRef::as_ref) + .unwrap_or("/etc/fido2_pam.conf"), + )?; + let timeout = args + .get("timeout") + .map(|t| { + t.parse::() + .expect("invalid config timeout is supposed to be an int") + }) + .map(Duration::from_secs); + if let Some(timeout) = timeout { + settings.device_timeout = timeout; + }; + Ok(settings) + } + pub fn load(path: impl AsRef) -> io::Result { + let mut creds = Vec::new(); + let file = File::open(path.as_ref())?; + let reader = BufReader::new(file); + + for line in reader.lines() { + let line = line?; + let mut parts = line.split(":"); + if let Some(user) = parts.by_ref().next() { + creds.push(( + (&parts.collect::>()[..].join(":")).to_string(), + user.to_string(), + )); + } + } + + Ok(Settings { + user_credentials: creds, + ..Default::default() + }) + } + + pub fn get_credentials(&self, user: &str) -> Vec { + let mut creds = Vec::new(); + for (cred, pattern) in self.user_credentials.iter() { + let re = Regex::new(&pattern).expect(&["Invalid regex pattern:", &pattern].join(" ")); + if re.is_match(user) { + let mut parts = cred.split(":"); + //TODO: use expect + let id = parts.by_ref().next().unwrap(); + let key = parts.by_ref().next().unwrap(); + creds.push(ctap::FidoCredential { + id: hex::decode(id).unwrap(), + public_key: hex::decode(key).ok(), + }); + } + } + creds + } +} + +impl PamFido2 { + pub fn authenticate( + &self, + username: &str, + args: Vec, + ) -> Result> { + let settings = Settings::from_args(args)?; + let begin = SystemTime::now(); + let challenge = rand::thread_rng().gen::<[u8; 32]>(); + let credentials = settings.get_credentials(username); + if credentials.is_empty() { + return Ok(PamError::AUTH_ERR); + } + let slice = credentials.iter().collect::>(); + let request = FidoAssertionRequestBuilder::default() + .rp_id("fido2pam") + .client_data_hash(&challenge[..]) + .credentials(&slice[..]) + .build() + .unwrap(); + + loop { + let mut devices = ctap::get_devices() + .compat()? + .filter_map(|handle| FidoDevice::new(&handle).ok()) + .collect::>(); + match get_assertion_devices(&request, devices.iter_mut()) { + Ok(_) => return Ok(PamError::SUCCESS), + Err(_) if begin.elapsed().unwrap() > settings.device_timeout => { + return Ok(PamError::AUTH_ERR) + } + Err(e) if e.kind() == FidoErrorKind::DeviceUnsupported => continue, + Err(e) => Err(e).compat()?, + } + } + } +} + +impl PamServiceModule for PamFido2 { + fn authenticate(self: &Self, pamh: Pam, _: PamFlag, args: Vec) -> PamError { + let username = &pamh + .get_cached_user() + .ok() + .map(|name| name.unwrap().to_str().unwrap().to_string()) + .expect("Faied to get username"); + + match self.authenticate(username, args) { + Ok(e) => e, + Err(e) => { + eprintln!("{}", e); + PamError::AUTH_ERR + } + } + } +}