Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
1a8e83d81e | |||
501b28e0d9 | |||
d4c9dd913f |
@ -21,6 +21,10 @@ untrusted = "0.6"
|
|||||||
rust-crypto = "0.2"
|
rust-crypto = "0.2"
|
||||||
csv-core = "0.1.6"
|
csv-core = "0.1.6"
|
||||||
derive_builder = "0.9.0"
|
derive_builder = "0.9.0"
|
||||||
|
crossbeam = { version = "0.7.3", optional = true }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
crossbeam = "0.7.3"
|
crossbeam = "0.7.3"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
assert_devices = ["crossbeam"]
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
extern crate ctap_hmac as ctap;
|
extern crate ctap_hmac as ctap;
|
||||||
|
|
||||||
|
use crossbeam::thread;
|
||||||
use crypto::digest::Digest;
|
use crypto::digest::Digest;
|
||||||
use crypto::sha2::Sha256;
|
use crypto::sha2::Sha256;
|
||||||
use ctap::{FidoCredential, FidoCredentialRequestBuilder, FidoAssertionRequestBuilder, AuthenticatorOptions, FidoDevice, FidoError, FidoResult};
|
use ctap::{
|
||||||
|
FidoAssertionRequestBuilder, FidoCredential, FidoCredentialRequestBuilder, FidoDevice,
|
||||||
|
FidoError, FidoResult,
|
||||||
|
};
|
||||||
use failure::_core::time::Duration;
|
use failure::_core::time::Duration;
|
||||||
use hex;
|
use hex;
|
||||||
use std::env::args;
|
use std::env::args;
|
||||||
@ -11,42 +15,41 @@ use std::io::stdin;
|
|||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use crossbeam::thread;
|
|
||||||
|
|
||||||
const RP_ID: &str = "ctap_demo";
|
const RP_ID: &str = "ctap_demo";
|
||||||
|
|
||||||
fn run() -> ctap::FidoResult<()> {
|
fn run() -> ctap::FidoResult<()> {
|
||||||
let mut credentials = args().skip(1).map(|id| FidoCredential {
|
let mut credentials = args()
|
||||||
id: hex::decode(&id).expect("Invalid credential"),
|
.skip(1)
|
||||||
public_key: None,
|
.map(|id| FidoCredential {
|
||||||
}).collect::<Vec<_>>();
|
id: hex::decode(&id).expect("Invalid credential"),
|
||||||
|
public_key: None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
if credentials.len() == 0 {
|
if credentials.len() == 0 {
|
||||||
credentials = ctap::get_devices()?.map(|h| FidoDevice::new(&h).and_then(|mut dev| FidoCredentialRequestBuilder::default()
|
credentials = ctap::get_devices()?
|
||||||
.rp_id(RP_ID).build().unwrap().make_credential(&mut dev))).collect::<FidoResult<Vec<FidoCredential>>>()?;
|
.map(|h| {
|
||||||
}
|
|
||||||
let credentials = credentials.iter().collect::<Vec<_>>();
|
|
||||||
let (s, r) = channel();
|
|
||||||
thread::scope(|scope| {
|
|
||||||
let handles = ctap::get_devices()?.map(|h| {
|
|
||||||
let req = FidoAssertionRequestBuilder::default().rp_id(RP_ID).credentials(&credentials[..]).build().unwrap();
|
|
||||||
let s = s.clone();
|
|
||||||
scope.spawn(move |_| {
|
|
||||||
FidoDevice::new(&h).and_then(|mut dev| {
|
FidoDevice::new(&h).and_then(|mut dev| {
|
||||||
req.get_assertion(&mut dev).map(|res| {
|
FidoCredentialRequestBuilder::default()
|
||||||
s.send(res.clone());
|
.rp_id(RP_ID)
|
||||||
res
|
.build()
|
||||||
})
|
.unwrap()
|
||||||
|
.make_credential(&mut dev)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}).collect::<Vec<_>>();
|
.collect::<FidoResult<Vec<FidoCredential>>>()?;
|
||||||
for h in handles {
|
|
||||||
h.join();
|
|
||||||
}
|
|
||||||
Ok::<(), FidoError>(())
|
|
||||||
}).unwrap();
|
|
||||||
for res in r.iter().take(credentials.len()) {
|
|
||||||
dbg!(res);
|
|
||||||
}
|
}
|
||||||
|
let credentials = credentials.iter().collect::<Vec<_>>();
|
||||||
|
let req = FidoAssertionRequestBuilder::default()
|
||||||
|
.rp_id(RP_ID)
|
||||||
|
.credentials(&credentials[..])
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let mut devices = ctap::get_devices()?
|
||||||
|
.map(|handle| FidoDevice::new(&handle))
|
||||||
|
.collect::<FidoResult<Vec<_>>>()?;
|
||||||
|
let (cred, _) = ctap::get_assertion_devices(&req, devices.iter_mut())?;
|
||||||
|
println!("Success, got assertion for: {}", hex::encode(&cred.id));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +45,9 @@ pub enum FidoErrorKind {
|
|||||||
EncryptPin,
|
EncryptPin,
|
||||||
#[fail(display = "Failed to decrypt PIN.")]
|
#[fail(display = "Failed to decrypt PIN.")]
|
||||||
DecryptPin,
|
DecryptPin,
|
||||||
#[fail(display = "Supplied key has incorrect type.")]
|
|
||||||
VerifySignature,
|
|
||||||
#[fail(display = "Failed to verify response signature.")]
|
#[fail(display = "Failed to verify response signature.")]
|
||||||
|
VerifySignature,
|
||||||
|
#[fail(display = "Supplied key has incorrect type.")]
|
||||||
KeyType,
|
KeyType,
|
||||||
#[fail(display = "Device returned error: {}", _0)]
|
#[fail(display = "Device returned error: {}", _0)]
|
||||||
CborError(CborErrorCode),
|
CborError(CborErrorCode),
|
||||||
|
111
src/lib.rs
111
src/lib.rs
@ -61,6 +61,7 @@ pub mod extensions;
|
|||||||
mod hid_common;
|
mod hid_common;
|
||||||
mod hid_linux;
|
mod hid_linux;
|
||||||
mod packet;
|
mod packet;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -68,13 +69,11 @@ use std::io::{Cursor, Write};
|
|||||||
use std::u16;
|
use std::u16;
|
||||||
use std::u8;
|
use std::u8;
|
||||||
|
|
||||||
use self::cbor::{
|
use self::cbor::{AuthenticatorOptions, PublicKeyCredentialDescriptor};
|
||||||
PublicKeyCredentialDescriptor,
|
|
||||||
};
|
|
||||||
pub use self::error::*;
|
pub use self::error::*;
|
||||||
pub use self::cbor::AuthenticatorOptions;
|
|
||||||
use self::hid_linux as hid;
|
use self::hid_linux as hid;
|
||||||
use self::packet::CtapCommand;
|
use self::packet::CtapCommand;
|
||||||
|
pub use self::util::*;
|
||||||
use crate::cbor::{AuthenticatorData, GetAssertionRequest};
|
use crate::cbor::{AuthenticatorData, GetAssertionRequest};
|
||||||
use failure::{Fail, ResultExt};
|
use failure::{Fail, ResultExt};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
@ -99,7 +98,6 @@ pub struct FidoCredential {
|
|||||||
/// The public key provided by the authenticator, in uncompressed form.
|
/// The public key provided by the authenticator, in uncompressed form.
|
||||||
pub public_key: Option<Vec<u8>>,
|
pub public_key: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An opened FIDO authenticator.
|
/// An opened FIDO authenticator.
|
||||||
pub struct FidoDevice {
|
pub struct FidoDevice {
|
||||||
device: fs::File,
|
device: fs::File,
|
||||||
@ -111,6 +109,46 @@ pub struct FidoDevice {
|
|||||||
aaguid: [u8; 16],
|
aaguid: [u8; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FidoCancelHandle {
|
||||||
|
device: fs::File,
|
||||||
|
packet_size: u16,
|
||||||
|
channel_id: [u8; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FidoCancelHandle {
|
||||||
|
pub fn cancel(&mut self) -> FidoResult<()> {
|
||||||
|
let payload = &[1u8];
|
||||||
|
let to_send = payload.len() as u16;
|
||||||
|
let max_payload = (self.packet_size - 7) as usize;
|
||||||
|
let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload));
|
||||||
|
packet::write_init_packet(
|
||||||
|
&mut self.device,
|
||||||
|
64,
|
||||||
|
&self.channel_id,
|
||||||
|
&CtapCommand::Cancel,
|
||||||
|
to_send,
|
||||||
|
frame,
|
||||||
|
)?;
|
||||||
|
if payload.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let max_payload = (self.packet_size - 5) as usize;
|
||||||
|
for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) {
|
||||||
|
packet::write_cont_packet(&mut self.device, 64, &self.channel_id, seq, frame)?;
|
||||||
|
}
|
||||||
|
self.device.flush().context(FidoErrorKind::WritePacket)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_after<T>(&mut self, body: impl Fn(()) -> T) -> FidoResult<T> {
|
||||||
|
let res = body(());
|
||||||
|
match self.cancel() {
|
||||||
|
Ok(_) => Ok(res),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Request a new credential from the authenticator. The `rp_id` should be
|
/// Request a new credential from the authenticator. The `rp_id` should be
|
||||||
/// a stable string used to identify the party for whom the credential is
|
/// a stable string used to identify the party for whom the credential is
|
||||||
/// created, for convenience it will be returned with the credential.
|
/// created, for convenience it will be returned with the credential.
|
||||||
@ -198,13 +236,8 @@ pub struct FidoAssertionRequest<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FidoAssertionRequest<'a> {
|
impl<'a> FidoAssertionRequest<'a> {
|
||||||
pub fn get_assertion(
|
pub fn get_assertion(&self, device: &mut FidoDevice) -> FidoResult<&'a FidoCredential> {
|
||||||
&self,
|
device.get_assertion(self).map(|res| res.0)
|
||||||
device: &mut FidoDevice,
|
|
||||||
) -> FidoResult<&'a FidoCredential> {
|
|
||||||
device
|
|
||||||
.get_assertion(self)
|
|
||||||
.map(|res| res.0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,10 +357,21 @@ impl FidoDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cancel_handle(&mut self) -> FidoResult<FidoCancelHandle> {
|
||||||
|
Ok(self
|
||||||
|
.device
|
||||||
|
.try_clone()
|
||||||
|
.map(|device| FidoCancelHandle {
|
||||||
|
device,
|
||||||
|
packet_size: self.packet_size,
|
||||||
|
channel_id: self.channel_id,
|
||||||
|
})
|
||||||
|
.context(FidoErrorKind::Io)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn make_credential(
|
pub fn make_credential(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: &FidoCredentialRequest<'_>
|
request: &FidoCredentialRequest<'_>,
|
||||||
) -> FidoResult<FidoCredential> {
|
) -> FidoResult<FidoCredential> {
|
||||||
let rp = cbor::PublicKeyCredentialRpEntity {
|
let rp = cbor::PublicKeyCredentialRpEntity {
|
||||||
id: request.rp_id,
|
id: request.rp_id,
|
||||||
@ -343,7 +387,8 @@ impl FidoDevice {
|
|||||||
|
|
||||||
let options = Some(AuthenticatorOptions {
|
let options = Some(AuthenticatorOptions {
|
||||||
up: false,
|
up: false,
|
||||||
uv: request.uv, rk: request.rk
|
uv: request.uv,
|
||||||
|
rk: request.rk,
|
||||||
});
|
});
|
||||||
if self.needs_pin && self.pin_token.is_none() {
|
if self.needs_pin && self.pin_token.is_none() {
|
||||||
Err(FidoErrorKind::PinRequired)?
|
Err(FidoErrorKind::PinRequired)?
|
||||||
@ -364,13 +409,17 @@ impl FidoDevice {
|
|||||||
rp,
|
rp,
|
||||||
user,
|
user,
|
||||||
pub_key_cred_params: &pub_key_cred_params,
|
pub_key_cred_params: &pub_key_cred_params,
|
||||||
exclude_list: &request.exclude_list.iter()
|
exclude_list: &request
|
||||||
|
.exclude_list
|
||||||
|
.iter()
|
||||||
.map(|cred| PublicKeyCredentialDescriptor {
|
.map(|cred| PublicKeyCredentialDescriptor {
|
||||||
cred_type: "public-key".into(),
|
cred_type: "public-key".into(),
|
||||||
id: cred.id.clone(),
|
id: cred.id.clone(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()[..],
|
.collect::<Vec<_>>()[..],
|
||||||
extensions: &request.extension_data.iter()
|
extensions: &request
|
||||||
|
.extension_data
|
||||||
|
.iter()
|
||||||
.map(|(name, data)| (*name, *data))
|
.map(|(name, data)| (*name, *data))
|
||||||
.collect::<Vec<_>>()[..],
|
.collect::<Vec<_>>()[..],
|
||||||
options,
|
options,
|
||||||
@ -388,7 +437,7 @@ impl FidoDevice {
|
|||||||
.attested_credential_data
|
.attested_credential_data
|
||||||
.credential_public_key,
|
.credential_public_key,
|
||||||
)?
|
)?
|
||||||
.bytes();
|
.bytes();
|
||||||
Ok(FidoCredential {
|
Ok(FidoCredential {
|
||||||
id: response.auth_data.attested_credential_data.credential_id,
|
id: response.auth_data.attested_credential_data.credential_id,
|
||||||
public_key: Some(Vec::from(&public_key[..])),
|
public_key: Some(Vec::from(&public_key[..])),
|
||||||
@ -409,7 +458,6 @@ impl FidoDevice {
|
|||||||
/// This method will fail if a PIN is required but the device is not
|
/// This method will fail if a PIN is required but the device is not
|
||||||
/// unlocked or if the device returns malformed data.
|
/// unlocked or if the device returns malformed data.
|
||||||
|
|
||||||
|
|
||||||
pub fn get_assertion<'a>(
|
pub fn get_assertion<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
assertion: &FidoAssertionRequest<'a>,
|
assertion: &FidoAssertionRequest<'a>,
|
||||||
@ -446,7 +494,7 @@ impl FidoDevice {
|
|||||||
options: Some(AuthenticatorOptions {
|
options: Some(AuthenticatorOptions {
|
||||||
rk: assertion.rk,
|
rk: assertion.rk,
|
||||||
uv: assertion.uv,
|
uv: assertion.uv,
|
||||||
up: assertion.up
|
up: assertion.up,
|
||||||
}),
|
}),
|
||||||
pin_auth: pin_auth,
|
pin_auth: pin_auth,
|
||||||
pin_protocol: pin_auth.and(Some(0x01)),
|
pin_protocol: pin_auth.and(Some(0x01)),
|
||||||
@ -467,19 +515,28 @@ impl FidoDevice {
|
|||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
|
|
||||||
credential.and_then(|cred| {
|
credential
|
||||||
cred.public_key.as_ref().map(|public_key|
|
.and_then(|cred| {
|
||||||
Some(crypto::verify_signature(
|
if cred
|
||||||
|
.public_key
|
||||||
|
.as_ref()
|
||||||
|
.map(|public_key| {
|
||||||
|
crypto::verify_signature(
|
||||||
&public_key,
|
&public_key,
|
||||||
&assertion.client_data_hash,
|
&assertion.client_data_hash,
|
||||||
&response.auth_data_bytes,
|
&response.auth_data_bytes,
|
||||||
&response.signature,
|
&response.signature,
|
||||||
)
|
)
|
||||||
).unwrap_or(true)).iter().filter_map( |valid| match valid {
|
})
|
||||||
true => Some(cred),
|
.unwrap_or(true)
|
||||||
false => None,
|
{
|
||||||
}).next()
|
Some(cred)
|
||||||
}).ok_or(FidoError::from(FidoErrorKind::VerifySignature)).map(|cred| (cred, response.auth_data))
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or(FidoError::from(FidoErrorKind::VerifySignature))
|
||||||
|
.map(|cred| (cred, response.auth_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cbor(&mut self, request: cbor::Request) -> FidoResult<cbor::Response> {
|
fn cbor(&mut self, request: cbor::Request) -> FidoResult<cbor::Response> {
|
||||||
|
44
src/util.rs
Normal file
44
src/util.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use crate::cbor::AuthenticatorData;
|
||||||
|
use crate::{FidoAssertionRequest, FidoCredential, FidoDevice, FidoErrorKind, FidoResult};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
#[cfg(feature = "assert_devices")]
|
||||||
|
use crossbeam::thread;
|
||||||
|
|
||||||
|
/// Will send the `assertion` to all supplied `devices` and return either the first successful assertion or the last error
|
||||||
|
#[cfg(feature = "assert_devices")]
|
||||||
|
pub fn get_assertion_devices<'a>(
|
||||||
|
assertion: &'a FidoAssertionRequest,
|
||||||
|
devices: impl Iterator<Item = &'a mut FidoDevice>,
|
||||||
|
) -> FidoResult<(&'a FidoCredential, AuthenticatorData)> {
|
||||||
|
thread::scope(
|
||||||
|
|scope| -> FidoResult<(&'a FidoCredential, AuthenticatorData)> {
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
let handles = devices
|
||||||
|
.map(|device| {
|
||||||
|
let cancel = device.cancel_handle()?;
|
||||||
|
let tx = tx.clone();
|
||||||
|
let thread_handle =
|
||||||
|
scope.spawn(move |_| tx.send(device.get_assertion(assertion)));
|
||||||
|
Ok((cancel, thread_handle))
|
||||||
|
})
|
||||||
|
.collect::<FidoResult<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let mut err = None;
|
||||||
|
for res in rx.iter().take(handles.len()) {
|
||||||
|
match res {
|
||||||
|
Ok(_) => {
|
||||||
|
for (mut cancel, join) in handles {
|
||||||
|
// Canceling out of courtesy don't care if it fails
|
||||||
|
let _ = cancel.cancel();
|
||||||
|
let _ = join.join();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
e => err = Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err.unwrap_or(Err(FidoErrorKind::DeviceUnsupported.into()))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user