Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New struct members added in CTAP 2.2 #334

Open
wants to merge 4 commits into
base: ctap2-2021
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,5 @@ env_logger = "^0.6"
getopts = "^0.2"
assert_matches = "1.2"
rpassword = "5.0"
flate3 = "1"
aes-gcm = "0.10"
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
124 changes: 109 additions & 15 deletions examples/ctap2_discoverable_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@
* 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, Key,
};
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 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 +68,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 +89,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 +146,37 @@ 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.
let gcm_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(gcm_key);
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 +214,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 +249,30 @@ 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| {
let gcm_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(gcm_key);
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 +288,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 +319,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 +394,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 +412,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 +445,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
13 changes: 12 additions & 1 deletion src/ctap2/commands/credential_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ pub struct CredentialManagementResponse {
pub cred_protect: Option<u64>,
/// Large blob encryption key.
pub large_blob_key: Option<Vec<u8>>,

// CTAP 2.2
/// Whether the credential is third-party payment enabled, if supported by the authenticator.
pub third_party_payment: Option<bool>,
}

impl CtapResponse for CredentialManagementResponse {}
Expand Down Expand Up @@ -215,6 +219,7 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse {
let mut total_credentials = None; // (0x09) Unsigned Integer Total number of credentials present on the authenticator for the RP in question
let mut cred_protect = None; // (0x0A) Unsigned Integer Credential protection policy.
let mut large_blob_key = None; // (0x0B) Byte string Large blob encryption key.
let mut third_party_payment = None; // (0x0C) bool

while let Some(key) = map.next_key()? {
match key {
Expand Down Expand Up @@ -294,7 +299,12 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse {
// Using into_vec, to avoid any copy of large_blob_key
large_blob_key = Some(map.next_value::<ByteBuf>()?.into_vec());
}

0x0C => {
if third_party_payment.is_some() {
return Err(SerdeError::duplicate_field("third_party_payment"));
}
third_party_payment = Some(map.next_value()?);
}
k => {
warn!("ClientPinResponse: unexpected key: {:?}", k);
let _ = map.next_value::<IgnoredAny>()?;
Expand All @@ -315,6 +325,7 @@ impl<'de> Deserialize<'de> for CredentialManagementResponse {
total_credentials,
cred_protect,
large_blob_key,
third_party_payment,
})
}
}
Expand Down
Loading
Loading