Skip to content

Commit

Permalink
Handle PRF evaluation on makeCredential
Browse files Browse the repository at this point in the history
  • Loading branch information
Vogeltak authored and Progdrasil committed Jul 11, 2024
1 parent 8cb664d commit 105d90f
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 37 deletions.
3 changes: 2 additions & 1 deletion passkey-authenticator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ workspace = true
[features]
default = []
tokio = ["dep:tokio"]
testable = ["dep:mockall"]
testable = ["dep:mockall", "passkey-types/mocks"]

[dependencies]
async-trait = "0.1"
Expand All @@ -34,3 +34,4 @@ mockall = { version = "0.11" }
tokio = { version = "1", features = ["sync", "macros", "rt"] }
generic-array = { version = "0.14", default-features = false }
signature = { version = "2", features = ["rand_core"] }
passkey-types = { path = "../passkey-types", version = "0.2", features = ["mocks"]}
8 changes: 6 additions & 2 deletions passkey-authenticator/src/authenticator/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use passkey_types::{
mod hmac_secret;
pub use hmac_secret::{HmacSecretConfig, HmacSecretCredentialSupport};

#[cfg(test)]
pub(crate) use hmac_secret::tests::prf_eval_request;

#[cfg(docs)]
use passkey_types::webauthn;

Expand Down Expand Up @@ -61,14 +64,15 @@ impl<S, U> Authenticator<S, U> {
pub(super) fn make_extensions(
&self,
request: Option<make_credential::ExtensionInputs>,
uv: bool,
) -> Result<MakeExtensionOutputs, StatusCode> {
let request = request.and_then(|r| r.zip_contents());
let pk_extensions = self.make_passkey_extensions(request.as_ref());

let prf = request
.and_then(|ext| {
ext.prf.and_then(|_input| {
self.make_prf(pk_extensions.hmac_secret.as_ref())
ext.prf.and_then(|input| {
self.make_prf(pk_extensions.hmac_secret.as_ref(), input, uv)
.transpose()
})
})
Expand Down
199 changes: 196 additions & 3 deletions passkey-authenticator/src/authenticator/extensions/hmac_secret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use crate::Authenticator;
#[derive(Debug)]
pub struct HmacSecretConfig {
credentials: HmacSecretCredentialSupport,
/// Extension to retrieve a symmetric secret from the authenticator during registration.
///
/// In the spec this is support for `hmac-secret-mc`
on_make_credential_support: bool,
}

impl HmacSecretConfig {
Expand All @@ -28,6 +32,7 @@ impl HmacSecretConfig {
pub fn new_with_uv_only() -> Self {
Self {
credentials: HmacSecretCredentialSupport::WithUvOnly,
on_make_credential_support: false,
}
}

Expand All @@ -37,9 +42,22 @@ impl HmacSecretConfig {
pub fn new_without_uv() -> Self {
Self {
credentials: HmacSecretCredentialSupport::WithoutUv,
on_make_credential_support: false,
}
}

/// Enable support for returning the hmac-secret values on credential creation
pub fn enable_on_make_credential(mut self) -> Self {
self.on_make_credential_support = true;
self
}

/// Check whether this configuration supports `hmac-secret-mc`,
/// meaning it supports retrieving the symmetric secret on credential creation.
pub fn hmac_secret_mc(&self) -> bool {
self.on_make_credential_support
}

fn supports_no_uv(&self) -> bool {
self.credentials.without_uv()
}
Expand Down Expand Up @@ -86,21 +104,38 @@ impl<S, U> Authenticator<S, U> {
pub(super) fn make_prf(
&self,
passkey_ext: Option<&passkey_types::StoredHmacSecret>,
request: AuthenticatorPrfInputs,
uv: bool,
) -> Result<Option<AuthenticatorPrfMakeOutputs>, StatusCode> {
if self.extensions.hmac_secret.is_none() {
let Some(ref config) = self.extensions.hmac_secret else {
return Ok(None);
};

if passkey_ext.is_none() {
let Some(creds) = passkey_ext else {
return Ok(Some(AuthenticatorPrfMakeOutputs {
enabled: false,
results: None,
}));
};

let results = config
.on_make_credential_support
.then(|| {
request.eval.map(|eval| {
let request = HmacSecretSaltOrOutput::new(eval.first, eval.second);

calculate_hmac_secret(creds, request, config, uv)
})
})
.flatten()
.transpose()?;

Ok(Some(AuthenticatorPrfMakeOutputs {
enabled: true,
results: None,
results: results.map(|shared_secrets| AuthenticatorPrfValues {
first: shared_secrets.first().try_into().unwrap(),
second: shared_secrets.second().map(|b| b.try_into().unwrap()),
}),
}))
}

Expand Down Expand Up @@ -176,3 +211,161 @@ fn select_salts(

Some(HmacSecretSaltOrOutput::new(eval.first, eval.second))
}

#[cfg(test)]
pub mod tests {
use passkey_types::{ctap2::Aaguid, Passkey};

use crate::{Authenticator, MockUserValidationMethod};

use super::*;

pub(crate) fn prf_eval_request(eval: Option<Vec<u8>>) -> AuthenticatorPrfInputs {
let eval = eval
.and_then(|data| HmacSecretSaltOrOutput::try_from(data.as_slice()).ok())
.map(|salts| AuthenticatorPrfValues {
first: salts.first().try_into().unwrap(),
second: salts.second().map(|b| b.try_into().unwrap()),
});
AuthenticatorPrfInputs {
eval,
eval_by_credential: None,
}
}

#[test]
fn hmac_secret_cycle_works() {
let auth = Authenticator::new(Aaguid::new_empty(), None, MockUserValidationMethod::new())
.hmac_secret(HmacSecretConfig::new_without_uv());

let ext = auth
.make_hmac_secret(Some(true))
.expect("There should be passkey extensions");
assert!(ext.cred_without_uv.is_some());

let passkey = Passkey::mock("sneakernetsend.com".into())
.hmac_secret(ext)
.build();

let request = prf_eval_request(Some(random_vec(64)));

let res = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
request.clone(),
true,
)
.expect("did not succeed in creating hashes")
.expect("hmac-secret was not supported when creation was requested")
.results;
assert!(res.second.is_some());
assert_ne!(&res.first, res.second.as_ref().unwrap());

// Make sure that the same input gives the same output
let res2 = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
request.clone(),
true,
)
.expect("did not succeed in calling it twice with the same input")
.expect("hmac-secret was not supported when creation was requested")
.results;

assert_eq!(res.first, res2.first);
assert_eq!(res.second, res2.second);

// Ensure that a different input changes the output
let res3 = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
prf_eval_request(Some(random_vec(64))),
true,
)
.expect("Changing input should still succeed")
.expect("hmac-secret was not supported when creation was requested")
.results;

assert_ne!(res.first, res3.first);
assert_ne!(res.second, res3.second);
assert!(res3.second.is_some());
assert_ne!(res3.first, res3.second.unwrap());

// make sure that if the same input is given but without UV the output is different
let res4 = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
request,
false,
)
.expect("did not succeed in calling it twice with the same input")
.expect("hmac-secret was not supported when creation was requested")
.results;

assert_ne!(res.first, res4.first);
assert_ne!(res.second, res4.second);
assert!(res4.second.is_some());
assert_ne!(res4.first, res4.second.unwrap());
}

#[test]
fn hmac_secret_cycle_works_with_one_cred() {
let auth = Authenticator::new(Aaguid::new_empty(), None, MockUserValidationMethod::new())
.hmac_secret(HmacSecretConfig::new_with_uv_only());

let ext = auth
.make_hmac_secret(Some(true))
.expect("There should be passkey extensions");
assert!(ext.cred_without_uv.is_none());

let passkey = Passkey::mock("sneakernetsend.com".into())
.hmac_secret(ext)
.build();

let request = prf_eval_request(Some(random_vec(64)));

let res = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
request.clone(),
true,
)
.expect("did not succeed in creating hashes")
.expect("hmac-secret was not supported when creation was requested")
.results;
assert!(res.second.is_none());

let res2 = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
request,
true,
)
.expect("did not succeed in calling it twice with the same input")
.expect("hmac-secret was not supported when creation was requested")
.results;

assert_eq!(res.first, res2.first);
assert!(res2.second.is_none());

let res3 = auth
.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
prf_eval_request(Some(random_vec(64))),
true,
)
.expect("Changing input should still succeed")
.expect("hmac-secret was not supported when creation was requested")
.results;

assert_ne!(res.first, res3.first);
assert!(res3.second.is_none());
}
}
Loading

0 comments on commit 105d90f

Please sign in to comment.