added make_credential_full
improved error messages fixed cbor issues added hmac example
This commit is contained in:
parent
61d81bda87
commit
4b58dd12f3
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ctap_hmac"
|
name = "ctap_hmac"
|
||||||
description = "A Rust implementation of the FIDO2 CTAP protocol, including the HMAC extension"
|
description = "A Rust implementation of the FIDO2 CTAP protocol, including the HMAC extension"
|
||||||
version = "0.1.1"
|
version = "0.2.1"
|
||||||
license = "Apache-2.0/MIT"
|
license = "Apache-2.0/MIT"
|
||||||
homepage = "https://github.com/ArdaXi/ctap/pull/2"
|
homepage = "https://github.com/ArdaXi/ctap/pull/2"
|
||||||
repository = "https://github.com/shimunn/ctap"
|
repository = "https://github.com/shimunn/ctap"
|
||||||
@ -19,3 +19,5 @@ cbor-codec = "0.7"
|
|||||||
ring = "0.13"
|
ring = "0.13"
|
||||||
untrusted = "0.6"
|
untrusted = "0.6"
|
||||||
rust-crypto = "0.2"
|
rust-crypto = "0.2"
|
||||||
|
hex = "0.4.0"
|
||||||
|
csv-core = "0.1.6"
|
||||||
|
62
examples/hmac.rs
Normal file
62
examples/hmac.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
extern crate ctap_hmac as ctap;
|
||||||
|
|
||||||
|
use crypto::digest::Digest;
|
||||||
|
use crypto::sha2::Sha256;
|
||||||
|
use ctap::extensions::hmac::{FidoHmacCredential, HmacExtension};
|
||||||
|
use ctap_hmac::{AuthenticatorOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity};
|
||||||
|
use hex;
|
||||||
|
use std::env::args;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::io::stdin;
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
fn main() -> ctap::FidoResult<()> {
|
||||||
|
let mut devices = ctap::get_devices()?;
|
||||||
|
let device_info = &mut devices.next().expect("No authenicator found");
|
||||||
|
let mut device = ctap::FidoDevice::new(device_info)?;
|
||||||
|
let options = || Some(AuthenticatorOptions { uv: true, rk: true });
|
||||||
|
let mut credential = match args().skip(1).next().map(|h| FidoHmacCredential {
|
||||||
|
id: hex::decode(&h).expect("Invalid credential"),
|
||||||
|
rp_id: "ctap_demo".into(),
|
||||||
|
}) {
|
||||||
|
Some(cred) => cred,
|
||||||
|
_ => {
|
||||||
|
let rp = PublicKeyCredentialRpEntity {
|
||||||
|
id: "ctap_demo",
|
||||||
|
name: Some("ctap_hmac crate"),
|
||||||
|
icon: None,
|
||||||
|
};
|
||||||
|
let user = PublicKeyCredentialUserEntity {
|
||||||
|
id: &[0u8],
|
||||||
|
name: "commandline",
|
||||||
|
icon: None,
|
||||||
|
display_name: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Authorize using your device");
|
||||||
|
let credential: FidoHmacCredential = device
|
||||||
|
.make_hmac_credential_full(rp, user, &[0u8; 32], &[], options())
|
||||||
|
.map(|cred| cred.into())?;
|
||||||
|
println!("Credential: {}\nNote: You can pass this credential as first argument in order to reproduce results", hex::encode(&credential.id));
|
||||||
|
credential
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let credential = credential;
|
||||||
|
print!("Type in your message: ");
|
||||||
|
stdout().flush();
|
||||||
|
let mut message = String::new();
|
||||||
|
stdin()
|
||||||
|
.read_line(&mut message)
|
||||||
|
.expect("Couldn't get your message\nNote: this demo does not accept binary data");
|
||||||
|
println!("Authorize using your device");
|
||||||
|
|
||||||
|
let mut salt = [0u8; 32];
|
||||||
|
let mut digest = Sha256::new();
|
||||||
|
digest.input(&message.as_bytes());
|
||||||
|
digest.result(&mut salt);
|
||||||
|
let hash = device
|
||||||
|
.get_hmac_assertion(&credential, &salt, None, options())?
|
||||||
|
.0;
|
||||||
|
println!("Hash: {}", hex::encode(&hash));
|
||||||
|
Ok(())
|
||||||
|
}
|
20
src/cbor.rs
20
src/cbor.rs
@ -82,7 +82,11 @@ impl<'a> MakeCredentialRequest<'a> {
|
|||||||
let mut length = 4;
|
let mut length = 4;
|
||||||
length += !self.exclude_list.is_empty() as usize;
|
length += !self.exclude_list.is_empty() as usize;
|
||||||
length += !self.extensions.is_empty() as usize;
|
length += !self.extensions.is_empty() as usize;
|
||||||
length += self.options.is_some() as usize;
|
length += self
|
||||||
|
.options
|
||||||
|
.as_ref()
|
||||||
|
.map(|opt| opt.encoded())
|
||||||
|
.unwrap_or(false) as usize;
|
||||||
length += self.pin_auth.is_some() as usize;
|
length += self.pin_auth.is_some() as usize;
|
||||||
length += self.pin_protocol.is_some() as usize;
|
length += self.pin_protocol.is_some() as usize;
|
||||||
encoder.object(length)?;
|
encoder.object(length)?;
|
||||||
@ -145,7 +149,7 @@ impl MakeCredentialResponse {
|
|||||||
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
Err(FidoErrorKind::CborError(status))?
|
Err(FidoErrorKind::CborError(CborErrorCode::from(status)))?
|
||||||
}
|
}
|
||||||
let mut decoder = Decoder::new(Config::default(), reader);
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
let mut response = MakeCredentialResponse::default();
|
let mut response = MakeCredentialResponse::default();
|
||||||
@ -182,7 +186,11 @@ impl<'a> GetAssertionRequest<'a> {
|
|||||||
let mut length = 2;
|
let mut length = 2;
|
||||||
length += !self.allow_list.is_empty() as usize;
|
length += !self.allow_list.is_empty() as usize;
|
||||||
length += !self.extensions.is_empty() as usize;
|
length += !self.extensions.is_empty() as usize;
|
||||||
length += self.options.is_some() as usize;
|
length += self
|
||||||
|
.options
|
||||||
|
.as_ref()
|
||||||
|
.map(|opt| opt.encoded())
|
||||||
|
.unwrap_or(false) as usize;
|
||||||
length += self.pin_auth.is_some() as usize;
|
length += self.pin_auth.is_some() as usize;
|
||||||
length += self.pin_protocol.is_some() as usize;
|
length += self.pin_protocol.is_some() as usize;
|
||||||
encoder.object(length)?;
|
encoder.object(length)?;
|
||||||
@ -236,7 +244,7 @@ impl GetAssertionResponse {
|
|||||||
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
Err(FidoErrorKind::CborError(status))?
|
Err(FidoErrorKind::CborError(CborErrorCode::from(status)))?
|
||||||
}
|
}
|
||||||
let mut decoder = Decoder::new(Config::default(), reader);
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
let mut response = GetAssertionResponse::default();
|
let mut response = GetAssertionResponse::default();
|
||||||
@ -272,7 +280,7 @@ impl GetInfoResponse {
|
|||||||
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
Err(FidoErrorKind::CborError(status))?
|
Err(FidoErrorKind::CborError(CborErrorCode::from(status)))?
|
||||||
}
|
}
|
||||||
let mut decoder = Decoder::new(Config::default(), reader);
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
let mut response = GetInfoResponse::default();
|
let mut response = GetInfoResponse::default();
|
||||||
@ -360,7 +368,7 @@ impl ClientPinResponse {
|
|||||||
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
|
||||||
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
Err(FidoErrorKind::CborError(status))?
|
Err(FidoErrorKind::CborError(CborErrorCode::from(status)))?
|
||||||
}
|
}
|
||||||
let mut decoder = Decoder::new(Config::default(), reader);
|
let mut decoder = Decoder::new(Config::default(), reader);
|
||||||
let mut response = ClientPinResponse::default();
|
let mut response = ClientPinResponse::default();
|
||||||
|
49
src/ctap_error_codes.csv
Normal file
49
src/ctap_error_codes.csv
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
Code,Name,Description
|
||||||
|
0x00,"CTAP1_ERR_SUCCESS, CTAP2_OK",Indicates successful response.
|
||||||
|
0x01,CTAP1_ERR_INVALID_COMMAND,The command is not a valid CTAP command.
|
||||||
|
0x02,CTAP1_ERR_INVALID_PARAMETER,The command included an invalid parameter.
|
||||||
|
0x03,CTAP1_ERR_INVALID_LENGTH,Invalid message or item length.
|
||||||
|
0x04,CTAP1_ERR_INVALID_SEQ,Invalid message sequencing.
|
||||||
|
0x05,CTAP1_ERR_TIMEOUT,Message timed out.
|
||||||
|
0x06,CTAP1_ERR_CHANNEL_BUSY,Channel busy.
|
||||||
|
0x0A,CTAP1_ERR_LOCK_REQUIRED,Command requires channel lock.
|
||||||
|
0x0B,CTAP1_ERR_INVALID_CHANNEL,Command not allowed on this cid.
|
||||||
|
0x11,CTAP2_ERR_CBOR_UNEXPECTED_TYPE,Invalid/unexpected CBOR error.
|
||||||
|
0x12,CTAP2_ERR_INVALID_CBOR,Error when parsing CBOR.
|
||||||
|
0x14,CTAP2_ERR_MISSING_PARAMETER,Missing non-optional parameter.
|
||||||
|
0x15,CTAP2_ERR_LIMIT_EXCEEDED,Limit for number of items exceeded.
|
||||||
|
0x16,CTAP2_ERR_UNSUPPORTED_EXTENSION,Unsupported extension.
|
||||||
|
0x19,CTAP2_ERR_CREDENTIAL_EXCLUDED,Valid credential found in the exclude list.
|
||||||
|
0x21,CTAP2_ERR_PROCESSING,Processing (Lengthy operation is in progress).
|
||||||
|
0x22,CTAP2_ERR_INVALID_CREDENTIAL,Credential not valid for the authenticator.
|
||||||
|
0x23,CTAP2_ERR_USER_ACTION_PENDING,Authentication is waiting for user interaction.
|
||||||
|
0x24,CTAP2_ERR_OPERATION_PENDING,"Processing, lengthy operation is in progress."
|
||||||
|
0x25,CTAP2_ERR_NO_OPERATIONS,No request is pending.
|
||||||
|
0x26,CTAP2_ERR_UNSUPPORTED_ALGORITHM,Authenticator does not support requested algorithm.
|
||||||
|
0x27,CTAP2_ERR_OPERATION_DENIED,Not authorized for requested operation.
|
||||||
|
0x28,CTAP2_ERR_KEY_STORE_FULL,Internal key storage is full.
|
||||||
|
0x29,CTAP2_ERR_NOT_BUSY,Authenticator cannot cancel as it is not busy.
|
||||||
|
0x2A,CTAP2_ERR_NO_OPERATION_PENDING,No outstanding operations.
|
||||||
|
0x2B,CTAP2_ERR_UNSUPPORTED_OPTION,Unsupported option.
|
||||||
|
0x2C,CTAP2_ERR_INVALID_OPTION,Not a valid option for current operation.
|
||||||
|
0x2D,CTAP2_ERR_KEEPALIVE_CANCEL,Pending keep alive was cancelled.
|
||||||
|
0x2E,CTAP2_ERR_NO_CREDENTIALS,No valid credentials provided.
|
||||||
|
0x2F,CTAP2_ERR_USER_ACTION_TIMEOUT,Timeout waiting for user interaction.
|
||||||
|
0x30,CTAP2_ERR_NOT_ALLOWED,"Continuation command, such as, authenticatorGetNextAssertion not allowed."
|
||||||
|
0x31,CTAP2_ERR_PIN_INVALID,PIN Invalid.
|
||||||
|
0x32,CTAP2_ERR_PIN_BLOCKED,PIN Blocked.
|
||||||
|
0x33,CTAP2_ERR_PIN_AUTH_INVALID,"PIN authentication, pinAuth, verification failed."
|
||||||
|
0x34,CTAP2_ERR_PIN_AUTH_BLOCKED,"PIN authentication,pinAuth, blocked. Requires power recycle to reset."
|
||||||
|
0x35,CTAP2_ERR_PIN_NOT_SET,No PIN has been set.
|
||||||
|
0x36,CTAP2_ERR_PIN_REQUIRED,PIN is required for the selected operation.
|
||||||
|
0x37,CTAP2_ERR_PIN_POLICY_VIOLATION,PIN policy violation. Currently only enforces minimum length.
|
||||||
|
0x38,CTAP2_ERR_PIN_TOKEN_EXPIRED,pinToken expired on authenticator.
|
||||||
|
0x39,CTAP2_ERR_REQUEST_TOO_LARGE,Authenticator cannot handle this request due to memory constraints.
|
||||||
|
0x3A,CTAP2_ERR_ACTION_TIMEOUT,The current operation has timed out.
|
||||||
|
0x3B,CTAP2_ERR_UP_REQUIRED,User presence is required for the requested operation.
|
||||||
|
0x7F,CTAP1_ERR_OTHER,Other unspecified error.
|
||||||
|
0xDF,CTAP2_ERR_SPEC_LAST,CTAP 2 spec last error.
|
||||||
|
0xE0,CTAP2_ERR_EXTENSION_FIRST,Extension specific error.
|
||||||
|
0xEF,CTAP2_ERR_EXTENSION_LAST,Extension specific error.
|
||||||
|
0xF0,CTAP2_ERR_VENDOR_FIRST,Vendor specific error.
|
||||||
|
0xFF,CTAP2_ERR_VENDOR_LAST,Vendor specific error.
|
|
75
src/error.rs
75
src/error.rs
@ -5,7 +5,8 @@
|
|||||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||||
// copied, modified, or distributed except according to those terms.
|
// copied, modified, or distributed except according to those terms.
|
||||||
use cbor_codec::{DecodeError, EncodeError};
|
use cbor_codec::{DecodeError, EncodeError};
|
||||||
|
use csv_core::{ReadFieldResult, Reader};
|
||||||
|
use failure::_core::fmt::{Error, Formatter};
|
||||||
use failure::{Backtrace, Context, Fail};
|
use failure::{Backtrace, Context, Fail};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@ -15,6 +16,9 @@ pub type FidoResult<T> = Result<T, FidoError>;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FidoError(Context<FidoErrorKind>);
|
pub struct FidoError(Context<FidoErrorKind>);
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Fail, Eq, PartialEq)]
|
||||||
|
pub struct CborErrorCode(u8);
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
|
||||||
pub enum FidoErrorKind {
|
pub enum FidoErrorKind {
|
||||||
#[fail(display = "Read/write error with device.")]
|
#[fail(display = "Read/write error with device.")]
|
||||||
@ -43,8 +47,8 @@ pub enum FidoErrorKind {
|
|||||||
DecryptPin,
|
DecryptPin,
|
||||||
#[fail(display = "Supplied key has incorrect type.")]
|
#[fail(display = "Supplied key has incorrect type.")]
|
||||||
KeyType,
|
KeyType,
|
||||||
#[fail(display = "Device returned error: 0x{:x}", _0)]
|
#[fail(display = "Device returned error: {}", _0)]
|
||||||
CborError(u8),
|
CborError(CborErrorCode),
|
||||||
#[fail(display = "Device does not support FIDO2")]
|
#[fail(display = "Device does not support FIDO2")]
|
||||||
DeviceUnsupported,
|
DeviceUnsupported,
|
||||||
#[fail(display = "This operating requires a PIN but none was provided.")]
|
#[fail(display = "This operating requires a PIN but none was provided.")]
|
||||||
@ -99,3 +103,68 @@ impl From<DecodeError> for FidoError {
|
|||||||
FidoError(err.context(FidoErrorKind::CborDecode))
|
FidoError(err.context(FidoErrorKind::CborDecode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<u8> for CborErrorCode {
|
||||||
|
fn from(code: u8) -> Self {
|
||||||
|
Self(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CborErrorCode {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
|
||||||
|
let messages = include_str!("ctap_error_codes.csv");
|
||||||
|
let mut rdr = Reader::new();
|
||||||
|
let mut bytes = messages.as_bytes();
|
||||||
|
let mut col: usize = 0;
|
||||||
|
let mut row: usize = 0;
|
||||||
|
let mut correct_row: bool = false;
|
||||||
|
let mut field = [0u8; 1024];
|
||||||
|
let hex = format!("{:x?}", self.0);
|
||||||
|
let mut name: Option<String> = None;
|
||||||
|
let mut desc: Option<String> = None;
|
||||||
|
loop {
|
||||||
|
let (result, nin, read) = rdr.read_field(&bytes, &mut field);
|
||||||
|
bytes = &bytes[nin..];
|
||||||
|
match result {
|
||||||
|
ReadFieldResult::InputEmpty => {}
|
||||||
|
ReadFieldResult::OutputFull => panic!("field too large"),
|
||||||
|
ReadFieldResult::Field { record_end } => {
|
||||||
|
let text = String::from_utf8(field[..read].iter().cloned().collect()).unwrap();
|
||||||
|
if row > 0 {
|
||||||
|
match col {
|
||||||
|
0 if i64::from_str_radix(&text[2..], 16)
|
||||||
|
.expect("malformed ctap_error_codes.csv")
|
||||||
|
== self.0 as i64 =>
|
||||||
|
{
|
||||||
|
correct_row = true
|
||||||
|
}
|
||||||
|
1 | 2 if correct_row => {
|
||||||
|
if let Some(_) = name {
|
||||||
|
desc = Some(text);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
name = Some(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col += 1;
|
||||||
|
if record_end {
|
||||||
|
col = 0;
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReadFieldResult::End => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((code, name, desc)) =
|
||||||
|
name.and_then(|name| desc.map(|desc| (self.0, name, desc)))
|
||||||
|
{
|
||||||
|
write!(f, "CborError: 0x{:x?}: {}", code, desc);
|
||||||
|
} else {
|
||||||
|
write!(f, "CborError: 0x{:x?}", self.0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use crate::cbor;
|
use crate::{
|
||||||
|
cbor, AuthenticatorOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity,
|
||||||
|
};
|
||||||
use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult};
|
use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult};
|
||||||
use cbor_codec::value::{Bytes, Int, Key, Text, Value};
|
use cbor_codec::value::{Bytes, Int, Key, Text, Value};
|
||||||
use cbor_codec::Encoder;
|
use cbor_codec::Encoder;
|
||||||
@ -31,23 +33,37 @@ pub trait HmacExtension {
|
|||||||
"hmac-secret"
|
"hmac-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extension_input() -> &'static Value {
|
||||||
|
&Value::Bool(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates data for the extension field as part of the assertion request
|
/// Generates data for the extension field as part of the assertion request
|
||||||
fn get_dict(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value> {
|
fn get_dict(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value> {
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
map.insert(
|
map.insert(
|
||||||
Key::Text(Text::Text(Self::extension_name().to_owned())),
|
Key::Text(Text::Text(
|
||||||
|
<Self as HmacExtension>::extension_name().to_owned(),
|
||||||
|
)),
|
||||||
self.get_data(salt, salt2)?,
|
self.get_data(salt, salt2)?,
|
||||||
);
|
);
|
||||||
Ok(Value::Map(map))
|
Ok(Value::Map(map))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps [`get_dict`]
|
|
||||||
fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value>;
|
fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value>;
|
||||||
|
|
||||||
/// Convenience function to create an credential with default rp_id and user_name
|
/// Convenience function to create an credential with default rp_id and user_name
|
||||||
/// Use `FidoDevice::make_credential` if you need more control
|
/// Use `FidoDevice::make_credential` if you need more control
|
||||||
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential>;
|
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential>;
|
||||||
|
|
||||||
|
fn make_hmac_credential_full(
|
||||||
|
&mut self,
|
||||||
|
rp: cbor::PublicKeyCredentialRpEntity,
|
||||||
|
user: cbor::PublicKeyCredentialUserEntity,
|
||||||
|
client_data_hash: &[u8],
|
||||||
|
exclude_list: &[cbor::PublicKeyCredentialDescriptor],
|
||||||
|
options: Option<cbor::AuthenticatorOptions>,
|
||||||
|
) -> FidoResult<FidoCredential>;
|
||||||
|
|
||||||
/// Request an assertion from the authenticator for a given credential and salt(s).
|
/// Request an assertion from the authenticator for a given credential and salt(s).
|
||||||
/// at least one `salt` must be provided, consider using a hashing function like SHA256
|
/// at least one `salt` must be provided, consider using a hashing function like SHA256
|
||||||
/// to ensure that your salt will fit 32 bytes.
|
/// to ensure that your salt will fit 32 bytes.
|
||||||
@ -63,6 +79,7 @@ pub trait HmacExtension {
|
|||||||
credential: &FidoHmacCredential,
|
credential: &FidoHmacCredential,
|
||||||
salt: &[u8; 32],
|
salt: &[u8; 32],
|
||||||
salt2: Option<&[u8; 32]>,
|
salt2: Option<&[u8; 32]>,
|
||||||
|
options: Option<AuthenticatorOptions>,
|
||||||
) -> FidoResult<([u8; 32], Option<[u8; 32]>)>;
|
) -> FidoResult<([u8; 32], Option<[u8; 32]>)>;
|
||||||
|
|
||||||
/// Convenience function for `get_hmac_assertion` that will accept arbitrary
|
/// Convenience function for `get_hmac_assertion` that will accept arbitrary
|
||||||
@ -76,7 +93,12 @@ pub trait HmacExtension {
|
|||||||
let mut digest = Sha256::new();
|
let mut digest = Sha256::new();
|
||||||
digest.input(input);
|
digest.input(input);
|
||||||
digest.result(&mut salt);
|
digest.result(&mut salt);
|
||||||
self.get_hmac_assertion(credential, &salt, None)
|
self.get_hmac_assertion(
|
||||||
|
credential,
|
||||||
|
&salt,
|
||||||
|
None,
|
||||||
|
Some(AuthenticatorOptions { uv: true, rk: true }),
|
||||||
|
)
|
||||||
.map(|secret| secret.0)
|
.map(|secret| secret.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,15 +157,53 @@ impl HmacExtension for FidoDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential> {
|
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential> {
|
||||||
self.make_credential("hmac", &[0u8], "commandline", &[0u8; 32])
|
let rp = PublicKeyCredentialRpEntity {
|
||||||
|
id: "hmac",
|
||||||
|
name: None,
|
||||||
|
icon: None,
|
||||||
|
};
|
||||||
|
let user = PublicKeyCredentialUserEntity {
|
||||||
|
id: &[0u8],
|
||||||
|
name: "commandline",
|
||||||
|
icon: None,
|
||||||
|
display_name: None,
|
||||||
|
};
|
||||||
|
let options = Some(AuthenticatorOptions {
|
||||||
|
uv: true,
|
||||||
|
rk: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.make_hmac_credential_full(rp, user, &[0u8; 32], &[], options)
|
||||||
.map(|cred| cred.into())
|
.map(|cred| cred.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_hmac_credential_full(
|
||||||
|
&mut self,
|
||||||
|
rp: cbor::PublicKeyCredentialRpEntity,
|
||||||
|
user: cbor::PublicKeyCredentialUserEntity,
|
||||||
|
client_data_hash: &[u8],
|
||||||
|
exclude_list: &[cbor::PublicKeyCredentialDescriptor],
|
||||||
|
options: Option<cbor::AuthenticatorOptions>,
|
||||||
|
) -> FidoResult<FidoCredential> {
|
||||||
|
self.make_credential_full(
|
||||||
|
rp,
|
||||||
|
user,
|
||||||
|
client_data_hash,
|
||||||
|
exclude_list,
|
||||||
|
&[(
|
||||||
|
<Self as HmacExtension>::extension_name(),
|
||||||
|
<Self as HmacExtension>::extension_input(),
|
||||||
|
)],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_hmac_assertion(
|
fn get_hmac_assertion(
|
||||||
&mut self,
|
&mut self,
|
||||||
credential: &FidoHmacCredential,
|
credential: &FidoHmacCredential,
|
||||||
salt: &[u8; 32],
|
salt: &[u8; 32],
|
||||||
salt2: Option<&[u8; 32]>,
|
salt2: Option<&[u8; 32]>,
|
||||||
|
options: Option<AuthenticatorOptions>,
|
||||||
) -> FidoResult<([u8; 32], Option<[u8; 32]>)> {
|
) -> FidoResult<([u8; 32], Option<[u8; 32]>)> {
|
||||||
let client_data_hash = [0u8; 32];
|
let client_data_hash = [0u8; 32];
|
||||||
while self.shared_secret.is_none() {
|
while self.shared_secret.is_none() {
|
||||||
@ -170,10 +230,7 @@ impl HmacExtension for FidoDevice {
|
|||||||
client_data_hash: &client_data_hash,
|
client_data_hash: &client_data_hash,
|
||||||
allow_list: &allow_list,
|
allow_list: &allow_list,
|
||||||
extensions: &[(<Self as HmacExtension>::extension_name(), &ext_data)],
|
extensions: &[(<Self as HmacExtension>::extension_name(), &ext_data)],
|
||||||
options: Some(cbor::AuthenticatorOptions {
|
options: options,
|
||||||
rk: false,
|
|
||||||
uv: true,
|
|
||||||
}),
|
|
||||||
pin_auth,
|
pin_auth,
|
||||||
pin_protocol: pin_auth.and(Some(0x01)),
|
pin_protocol: pin_auth.and(Some(0x01)),
|
||||||
};
|
};
|
||||||
|
@ -1 +1,2 @@
|
|||||||
pub mod hmac;
|
pub mod hmac;
|
||||||
|
pub use hmac::*;
|
||||||
|
68
src/lib.rs
68
src/lib.rs
@ -64,6 +64,10 @@ use std::io::{Cursor, Write};
|
|||||||
use std::u16;
|
use std::u16;
|
||||||
use std::u8;
|
use std::u8;
|
||||||
|
|
||||||
|
pub use self::cbor::{
|
||||||
|
AuthenticatorOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity,
|
||||||
|
PublicKeyCredentialUserEntity,
|
||||||
|
};
|
||||||
pub use self::error::*;
|
pub use self::error::*;
|
||||||
use self::hid_linux as hid;
|
use self::hid_linux as hid;
|
||||||
use self::packet::CtapCommand;
|
use self::packet::CtapCommand;
|
||||||
@ -232,16 +236,7 @@ impl FidoDevice {
|
|||||||
user_name: &str,
|
user_name: &str,
|
||||||
client_data_hash: &[u8],
|
client_data_hash: &[u8],
|
||||||
) -> FidoResult<FidoCredential> {
|
) -> FidoResult<FidoCredential> {
|
||||||
if self.needs_pin && self.pin_token.is_none() {
|
//TODO: implement all options: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
|
||||||
Err(FidoErrorKind::PinRequired)?
|
|
||||||
}
|
|
||||||
if client_data_hash.len() != 32 {
|
|
||||||
Err(FidoErrorKind::CborEncode)?
|
|
||||||
}
|
|
||||||
let pin_auth = self
|
|
||||||
.pin_token
|
|
||||||
.as_ref()
|
|
||||||
.map(|token| token.auth(&client_data_hash));
|
|
||||||
let rp = cbor::PublicKeyCredentialRpEntity {
|
let rp = cbor::PublicKeyCredentialRpEntity {
|
||||||
id: rp_id,
|
id: rp_id,
|
||||||
name: None,
|
name: None,
|
||||||
@ -253,21 +248,60 @@ impl FidoDevice {
|
|||||||
icon: None,
|
icon: None,
|
||||||
display_name: None,
|
display_name: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let options = Some(AuthenticatorOptions {
|
||||||
|
uv: true,
|
||||||
|
rk: false,
|
||||||
|
});
|
||||||
|
self.make_credential_full(rp, user, client_data_hash, &[], &[], options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// created, for convenience it will be returned with the credential.
|
||||||
|
/// `user_id` and `user_name` are not required when requesting attestations
|
||||||
|
/// but they MAY be displayed to the user and MAY be stored on the device
|
||||||
|
/// to be returned with an attestation if the device supports this.
|
||||||
|
/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`,
|
||||||
|
/// this is only used to verify the attestation provided by the
|
||||||
|
/// authenticator. When not implementing WebAuthN this can be any random
|
||||||
|
/// 32-byte array.
|
||||||
|
///
|
||||||
|
/// This method will fail if a PIN is required but the device is not
|
||||||
|
/// unlocked or if the device returns malformed data.
|
||||||
|
pub fn make_credential_full(
|
||||||
|
&mut self,
|
||||||
|
rp: cbor::PublicKeyCredentialRpEntity,
|
||||||
|
user: cbor::PublicKeyCredentialUserEntity,
|
||||||
|
client_data_hash: &[u8],
|
||||||
|
exclude_list: &[cbor::PublicKeyCredentialDescriptor],
|
||||||
|
extensions: &[(&str, &cbor_codec::value::Value)],
|
||||||
|
options: Option<cbor::AuthenticatorOptions>,
|
||||||
|
) -> FidoResult<FidoCredential> {
|
||||||
|
if self.needs_pin && self.pin_token.is_none() {
|
||||||
|
Err(FidoErrorKind::PinRequired)?
|
||||||
|
}
|
||||||
|
if client_data_hash.len() != 32 {
|
||||||
|
Err(FidoErrorKind::CborEncode)?
|
||||||
|
}
|
||||||
let pub_key_cred_params = [("public-key", -7)];
|
let pub_key_cred_params = [("public-key", -7)];
|
||||||
|
let pin_auth = self
|
||||||
|
.pin_token
|
||||||
|
.as_ref()
|
||||||
|
.map(|token| token.auth(&client_data_hash));
|
||||||
|
let rp_id = rp.id.to_owned();
|
||||||
let request = cbor::MakeCredentialRequest {
|
let request = cbor::MakeCredentialRequest {
|
||||||
client_data_hash,
|
client_data_hash,
|
||||||
rp,
|
rp,
|
||||||
user,
|
user,
|
||||||
pub_key_cred_params: &pub_key_cred_params,
|
pub_key_cred_params: &pub_key_cred_params,
|
||||||
exclude_list: Default::default(),
|
exclude_list: exclude_list,
|
||||||
extensions: Default::default(),
|
extensions: extensions,
|
||||||
options: Some(cbor::AuthenticatorOptions {
|
options: options,
|
||||||
rk: false,
|
|
||||||
uv: true,
|
|
||||||
}),
|
|
||||||
pin_auth,
|
pin_auth,
|
||||||
pin_protocol: pin_auth.and(Some(0x01)),
|
pin_protocol: pin_auth.and(Some(0x01)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = match self.cbor(cbor::Request::MakeCredential(request))? {
|
let response = match self.cbor(cbor::Request::MakeCredential(request))? {
|
||||||
cbor::Response::MakeCredential(resp) => resp,
|
cbor::Response::MakeCredential(resp) => resp,
|
||||||
_ => Err(FidoErrorKind::CborDecode)?,
|
_ => Err(FidoErrorKind::CborDecode)?,
|
||||||
@ -281,7 +315,7 @@ impl FidoDevice {
|
|||||||
.bytes();
|
.bytes();
|
||||||
Ok(FidoCredential {
|
Ok(FidoCredential {
|
||||||
id: response.auth_data.attested_credential_data.credential_id,
|
id: response.auth_data.attested_credential_data.credential_id,
|
||||||
rp_id: String::from(rp_id),
|
rp_id: rp_id,
|
||||||
public_key: Vec::from(&public_key[..]),
|
public_key: Vec::from(&public_key[..]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user