Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ env_logger = "^0.6"
getopts = "^0.2"
assert_matches = "1.2"
rpassword = "5.0"
flate3 = "1"
aes-gcm = "0.10"
# Workaround for 'broken' generic-array 0.14.9, see ctap2_discoverable_creds.rs for details
generic-array = { version = "1.3", features = ["compat-0_14"] }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
3 changes: 3 additions & 0 deletions examples/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ fn main() {
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Ok(StatusUpdate::LargeBlobData(_, _)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
136 changes: 121 additions & 15 deletions examples/ctap2_discoverable_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,31 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
Aes256Gcm,
};
use authenticator::{
authenticatorservice::{AuthenticatorService, RegisterArgs, SignArgs},
crypto::COSEAlgorithm,
ctap2::server::{
AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
PublicKeyCredentialParameters, PublicKeyCredentialUserEntity, RelyingParty,
ResidentKeyRequirement, Transport, UserVerificationRequirement,
ctap2::{
commands::large_blobs::LargeBlobArrayElement,
server::{
AuthenticationExtensionsClientInputs, AuthenticatorExtensionsCredBlob,
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, Transport,
UserVerificationRequirement,
},
},
statecallback::StateCallback,
Pin, StatusPinUv, StatusUpdate,
};
use getopts::Options;
use generic_array::GenericArray;
use getopts::{Matches, Options};
use sha2::{Digest, Sha256};
use std::sync::mpsc::{channel, RecvError};
use std::{convert::TryInto, io::Write};
use std::{env, io, thread};
use std::io::Write;

fn print_usage(program: &str, opts: Options) {
println!("------------------------------------------------------------------------");
Expand Down Expand Up @@ -60,7 +69,12 @@ fn ask_user_choice(choices: &[PublicKeyCredentialUserEntity]) -> Option<usize> {
}
}

fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) {
fn register_user(
manager: &mut AuthenticatorService,
username: &str,
timeout_ms: u64,
matches: &Matches,
) {
println!();
println!("*********************************************************************");
println!("Asking a security key to register now with user: {username}");
Expand All @@ -76,6 +90,8 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
);
let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into();

let has_large_blob = matches.opt_present("large_blob_key");
let name = username.to_string();
let (status_tx, status_rx) = channel::<StatusUpdate>();
thread::spawn(move || loop {
match status_rx.recv() {
Expand Down Expand Up @@ -131,6 +147,43 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select result notice")
}
Ok(StatusUpdate::LargeBlobData(tx, key)) => {
if has_large_blob {
// Let origData equal the opaque large-blob data.
let orig_data = format!("This is the large blob for {name}").into_bytes();
// Let origSize be the length, in bytes, of origData.
let orig_size = orig_data.len() as u64;
// Let plaintext equal origData after compression with DEFLATE [RFC1951].
let plaintext = flate3::deflate(&orig_data);
// Let nonce be a fresh, random, 12-byte value.
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
// Let ciphertext be the AEAD_AES_256_GCM authenticated encryption of plaintext using key, nonce, and the associated data as specified above.
//
// Note: Because of bug https://github.com/RustCrypto/traits/issues/2036 and/or https://github.com/fizyk20/generic-array/issues/158 we can't use the
// simple version below, but have to request the new generic-array 1.x in
// our Cargo.toml and use it directly here, as aes_gcm uses the old version
// that got 'broken' by a dot-release
// let gcm_key = Key::<Aes256Gcm>::from_slice(&key);
// let cipher = Aes256Gcm::new(gcm_key);
let cipher = Aes256Gcm::new(GenericArray::from_slice(&key).as_ref());
let mut payload = Payload::from(plaintext.as_ref());
// Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize).
let mut aad = b"blob".to_vec();
aad.extend_from_slice(&orig_size.to_le_bytes());
payload.aad = &aad;
let ciphertext = cipher
.encrypt(&nonce, payload)
.expect("Failed to encrypt plaintext large blob");
let elem = LargeBlobArrayElement {
ciphertext,
nonce: nonce.to_vec().try_into().unwrap(),
orig_size,
};
tx.send(elem).expect("Failed to send large blob element");
} else {
panic!("Unexpected large blob data request");
}
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down Expand Up @@ -168,6 +221,10 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
resident_key_req: ResidentKeyRequirement::Required,
extensions: AuthenticationExtensionsClientInputs {
cred_props: Some(true),
cred_blob: matches.opt_present("cred_blob").then(|| {
AuthenticatorExtensionsCredBlob::AsBytes("My short credBlob".as_bytes().to_vec())
}),
large_blob_key: matches.opt_present("large_blob_key").then_some(true),
..Default::default()
},
pin: None,
Expand Down Expand Up @@ -199,6 +256,35 @@ fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms:
}

println!("Register result: {:?}", &attestation_object);

if matches.opt_present("large_blob_key") {
println!("Adding large blob key");
}
}

fn extract_associated_large_blobs(key: Vec<u8>, array: Vec<LargeBlobArrayElement>) -> Vec<String> {
let valid_elements = array
.iter()
.filter_map(|e| {
// Note: Because of bug https://github.com/RustCrypto/traits/issues/2036 and/or https://github.com/fizyk20/generic-array/issues/158 we can't use the
// simple version below, but have to request the new generic-array 1.x in
// our Cargo.toml and use it directly here, as aes_gcm uses the old version
// that got 'broken' by a dot-release
// let gcm_key = Key::<Aes256Gcm>::from_slice(&key);
// let cipher = Aes256Gcm::new(gcm_key);
let cipher = Aes256Gcm::new(GenericArray::from_slice(&key).as_ref());
let mut payload = Payload::from(e.ciphertext.as_slice());
// Associated data: The value 0x626c6f62 ("blob") || uint64LittleEndian(origSize).
let mut aad = b"blob".to_vec();
aad.extend_from_slice(&e.orig_size.to_le_bytes());
payload.aad = &aad;
let plaintext = cipher.decrypt(e.nonce.as_slice().into(), payload).ok();
plaintext
})
.map(|d| flate3::inflate(&d)) // TODO: Check resulting length and compare to orig_size
.map(|d| String::from_utf8_lossy(&d).to_string())
.collect();
valid_elements
}

fn main() {
Expand All @@ -214,11 +300,9 @@ fn main() {
"timeout in seconds",
"SEC",
);
opts.optflag(
"s",
"skip_reg",
"Skip registration");

opts.optflag("s", "skip_reg", "Skip registration");
opts.optflag("b", "cred_blob", "With credBlob");
opts.optflag("l", "large_blob_key", "With largeBlobKey-extension");
opts.optflag("h", "help", "print this help menu");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Expand Down Expand Up @@ -247,7 +331,7 @@ fn main() {

if !matches.opt_present("skip_reg") {
for username in &["A. User", "A. Nother", "Dr. Who"] {
register_user(&mut manager, username, timeout_ms)
register_user(&mut manager, username, timeout_ms, &matches)
}
}

Expand Down Expand Up @@ -322,6 +406,9 @@ fn main() {
let idx = ask_user_choice(&users);
index_sender.send(idx).expect("Failed to send choice");
}
Ok(StatusUpdate::LargeBlobData(..)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand All @@ -337,7 +424,13 @@ fn main() {
allow_list,
user_verification_req: UserVerificationRequirement::Required,
user_presence_req: true,
extensions: Default::default(),
extensions: AuthenticationExtensionsClientInputs {
cred_blob: matches
.opt_present("cred_blob")
.then_some(AuthenticatorExtensionsCredBlob::AsBool(true)),
large_blob_key: matches.opt_present("large_blob_key").then_some(true),
..Default::default()
},
pin: None,
use_ctap1_fallback: false,
};
Expand All @@ -364,9 +457,22 @@ fn main() {
println!("Found credentials:");
println!(
"{:?}",
assertion_object.assertion.user.clone().unwrap().name.unwrap() // Unwrapping here, as these shouldn't fail
assertion_object
.assertion
.user
.clone()
.unwrap()
.name
.unwrap() // Unwrapping here, as these shouldn't fail
);
println!("-----------------------------------------------------------------");
if matches.opt_present("large_blob_key") {
let large_blobs = extract_associated_large_blobs(
assertion_object.large_blob_key.unwrap(),
assertion_object.large_blob_array.unwrap(),
);
println!("Associated large blobs: {large_blobs:?}");
}
println!("Done.");
break;
}
Expand Down
3 changes: 3 additions & 0 deletions examples/interactive_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,9 @@ fn interactive_status_callback(status_rx: Receiver<StatusUpdate>) {
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Ok(StatusUpdate::LargeBlobData(_, _)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
3 changes: 3 additions & 0 deletions examples/prf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ fn main() {
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Ok(StatusUpdate::LargeBlobData(..)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
3 changes: 3 additions & 0 deletions examples/set_pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ fn main() {
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Ok(StatusUpdate::LargeBlobData(_, _)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
3 changes: 3 additions & 0 deletions examples/test_exclude_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ fn main() {
Ok(StatusUpdate::SelectResultNotice(_, _)) => {
panic!("Unexpected select device notice")
}
Ok(StatusUpdate::LargeBlobData(_, _)) => {
panic!("Unexpected large blob data request")
}
Err(RecvError) => {
println!("STATUS: end");
return;
Expand Down
8 changes: 7 additions & 1 deletion src/ctap2/attestation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::server::AuthenticatorExtensionsCredBlob;
use super::utils::{from_slice_stream, read_be_u16, read_be_u32, read_byte};
use crate::crypto::{COSEAlgorithm, CryptoError, SharedSecret};
use crate::ctap2::server::{CredentialProtectionPolicy, HMACGetSecretOutput, RpIdHash};
Expand Down Expand Up @@ -119,11 +120,16 @@ pub struct Extension {
pub hmac_secret: Option<HmacSecretResponse>,
#[serde(rename = "minPinLength", skip_serializing_if = "Option::is_none")]
pub min_pin_length: Option<u64>,
#[serde(rename = "credBlob", skip_serializing_if = "Option::is_none")]
pub cred_blob: Option<AuthenticatorExtensionsCredBlob>,
}

impl Extension {
pub fn has_some(&self) -> bool {
self.min_pin_length.is_some() || self.hmac_secret.is_some() || self.cred_protect.is_some()
self.min_pin_length.is_some()
|| self.hmac_secret.is_some()
|| self.cred_protect.is_some()
|| self.cred_blob.is_some()
}
}

Expand Down
Loading