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

Support prf on makeCredential (PRF#5) #37

Merged
merged 2 commits into from
Jul 25, 2024
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: 2 additions & 2 deletions 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/testable"]

[dependencies]
async-trait = "0.1"
Expand All @@ -32,7 +32,7 @@ tokio = { version = "1", features = ["sync"], optional = true }
[dev-dependencies]
mockall = { version = "0.11" }
passkey-types = { path = "../passkey-types", version = "0.2", features = [
"testable",
"testable",
] }
tokio = { version = "1", features = ["sync", "macros", "rt"] }
generic-array = { version = "0.14", default-features = false }
Expand Down
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(doc)]
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
Loading