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
14 changes: 9 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

## Unreleased

- ⚠ BREAKING: The `UserValidationMethod` trait has been updated to use `UiHint`
to give the implementation more information about the request, which can be used
to decide whether additional validations are needed. To reflect this, the
`UserValidationMethod` trait now also returns which validations were performed.

## Passkey v0.5.0

- Migrate project to Rust 2024 edition
Expand All @@ -20,6 +15,14 @@
This allows for updating/replacing credentials should the user so wish. (#67)
- Fix hmac-secret logic around the second salt (#67)
- ⚠ BREAKING: Fix Ctap2Api trait to correctly call the concrete method to prevent recursion (#67)
- ⚠ BREAKING: The `UserValidationMethod` trait has been updated to use `UiHint`
to give the implementation more information about the request, which can be used
to decide whether additional validations are needed. To reflect this, the
`UserValidationMethod` trait now also returns which validations were performed. (#76)
- ⚠ BREAKING: Change the `CredentialStore` and `UserValidationMethod` associated type constraint
to a new `PasskeyAccessor` trait instead of the `TryInto<Passkey>`, making it possible to use a
custom passkey representation type that goes throughout the entire flow without losing any
additional information through a conversion. (#87)


### passkey-client v0.5.0
Expand All @@ -31,6 +34,7 @@
- Make output types Hashable in Swift code gen (#67)
- Support stringified booleans in webauthn requests (#67)
- Be more tolerant to failed deserialization of optional vectors (#67)
- ⚠ BREAKING: Add `username` and `user_display_name` to the `Passkey` type and its mock builder. (#87)

## Passkey v0.4.0
### passkey-authenticator v0.4.0
Expand Down
20 changes: 10 additions & 10 deletions passkey-authenticator/src/authenticator/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
//! [webauthn]: https://w3c.github.io/webauthn/#sctn-defined-extensions
//! [AuthenticatorDisplayName]: https://w3c.github.io/webauthn/#dom-credentialpropertiesoutput-authenticatordisplayname

use passkey_types::{
Passkey,
ctap2::{StatusCode, get_assertion, get_info, make_credential},
};
use passkey_types::ctap2::{StatusCode, get_assertion, get_info, make_credential};

mod hmac_secret;
pub use hmac_secret::{HmacSecretConfig, HmacSecretCredentialSupport};
Expand All @@ -23,7 +20,7 @@ pub(crate) use hmac_secret::tests::prf_eval_request;
#[cfg(doc)]
use passkey_types::webauthn;

use crate::Authenticator;
use crate::{Authenticator, passkey::PasskeyAccessor};

#[derive(Debug, Default)]
#[non_exhaustive]
Expand Down Expand Up @@ -93,12 +90,15 @@ impl<S, U> Authenticator<S, U> {
passkey_types::CredentialExtensions { hmac_secret }
}

pub(super) fn get_extensions(
pub(super) fn get_extensions<P>(
&self,
passkey: &Passkey,
passkey: &P,
request: Option<get_assertion::ExtensionInputs>,
uv: bool,
) -> Result<GetExtensionOutputs, StatusCode> {
) -> Result<GetExtensionOutputs, StatusCode>
where
P: PasskeyAccessor,
{
let Some(ext) = request.and_then(get_assertion::ExtensionInputs::zip_contents) else {
return Ok(Default::default());
};
Expand All @@ -107,8 +107,8 @@ impl<S, U> Authenticator<S, U> {
.prf
.and_then(|salts| {
self.get_prf(
&passkey.credential_id,
passkey.extensions.hmac_secret.as_ref(),
passkey.credential_id(),
passkey.extensions().hmac_secret.as_ref(),
salts,
uv,
)
Expand Down
26 changes: 12 additions & 14 deletions passkey-authenticator/src/authenticator/get_assertion.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use p256::ecdsa::{SigningKey, signature::SignerMut};
use passkey_types::{
Bytes,
ctap2::{
AuthenticatorData, Ctap2Error, Flags, StatusCode,
get_assertion::{Request, Response},
Expand All @@ -8,7 +9,9 @@ use passkey_types::{
};

use crate::{
Authenticator, CredentialStore, UserValidationMethod, private_key_from_cose_key,
Authenticator, CredentialStore, UserValidationMethod,
passkey::{AsCredentialDescriptor, PasskeyAccessor},
private_key_from_cose_key,
user_validation::UiHint,
};

Expand Down Expand Up @@ -80,10 +83,7 @@ where
let flags = self.check_user(hint, &input.options).await?;

// 8. If no credentials were located in step 1, return CTAP2_ERR_NO_CREDENTIALS.
let mut credential = maybe_credential?
.try_into()
.ok()
.ok_or(Ctap2Error::NoCredentials)?;
let mut credential = maybe_credential?;

// 9. If more than one credential was located in step 1 and allowList is present and not
// empty, select any applicable credential and proceed to step 12. Otherwise, order the
Expand Down Expand Up @@ -116,11 +116,9 @@ where
// counter value, depending on which approach is implemented by the authenticator,
// by some positive value. If the authenticator does not implement a signature
// counter, let the signature counter value remain constant at zero.
if let Some(counter) = credential.counter {
credential.counter = Some(counter + 1);
self.store_mut()
.update_credential(credential.clone())
.await?;
if let Some(counter) = credential.counter() {
credential.set_counter(counter + 1);
self.store_mut().update_credential(&credential).await?;
}

let extensions =
Expand All @@ -131,24 +129,24 @@ where
// concatenation is safe to use here because the authenticator data describes its own
// length. The hash of the serialized client data (which potentially has a variable
// length) is always the last element.
let auth_data = AuthenticatorData::new(&input.rp_id, credential.counter)
let auth_data = AuthenticatorData::new(&input.rp_id, credential.counter())
.set_flags(flags)
.set_assertion_extensions(extensions.signed)?;

let mut signature_target = auth_data.to_vec();
signature_target.extend(input.client_data_hash);

let secret_key = private_key_from_cose_key(&credential.key)?;
let secret_key = private_key_from_cose_key(&credential.key())?;

let mut private_key = SigningKey::from(secret_key);

let signature: p256::ecdsa::Signature = private_key.sign(&signature_target);
let signature_bytes = signature.to_der().to_bytes().to_vec().into();

let user_handle = credential.user_handle.clone();
let user_handle = credential.user_handle().map(Bytes::from);

Ok(Response {
credential: Some(credential.into()),
credential: Some(credential.as_credential_descriptor(None)),
auth_data,
signature: signature_bytes,
user: user_handle.map(|id| PublicKeyCredentialUserEntity {
Expand Down
4 changes: 3 additions & 1 deletion passkey-authenticator/src/authenticator/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ where
key: private,
rp_id: input.rp.id.clone(),
credential_id: credential_id.into(),
user_handle: is_passkey_rk.then_some(input.user.id.clone()),
user_handle: is_passkey_rk.then(|| input.user.id.clone()),
username: is_passkey_rk.then(|| input.user.name.clone()),
user_display_name: is_passkey_rk.then(|| input.user.display_name.clone()),
counter: self.make_credentials_with_signature_counter.then_some(0),
extensions: extensions.credential,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ async fn assert_excluded_credentials() {
rp_id: "".into(),
credential_id: cred_id.clone(),
user_handle: Some(response.user.id.clone()),
username: Some("Appleseed".into()),
user_display_name: Some("wendy".into()),
counter: None,
extensions: Default::default(),
};
Expand Down Expand Up @@ -410,7 +412,7 @@ async fn make_credential_returns_err_when_rk_is_requested_but_not_supported() {
unimplemented!("The test should not call save_credential")
}

async fn update_credential(&mut self, _cred: Passkey) -> Result<(), StatusCode> {
async fn update_credential(&mut self, _cred: &Passkey) -> Result<(), StatusCode> {
#![allow(clippy::unimplemented)]
unimplemented!("The test should not call update_credential")
}
Expand Down Expand Up @@ -514,6 +516,8 @@ async fn store_with_credentials_not_in_exclude_list_succeeds() {
rp_id: "future.1password.com".into(),
credential_id: stored_cred_id.clone(),
user_handle: Some(random_vec(16).into()),
username: Some("Appleseed".into()),
user_display_name: Some("wendy".into()),
counter: None,
extensions: Default::default(),
};
Expand Down
22 changes: 12 additions & 10 deletions passkey-authenticator/src/credential_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use passkey_types::{
webauthn::PublicKeyCredentialDescriptor,
};

use crate::passkey::PasskeyAccessor;

/// A struct that defines the capabilities of a store.
pub struct StoreInfo {
/// How the store handles discoverability.
Expand Down Expand Up @@ -48,7 +50,7 @@ impl DiscoverabilitySupport {
#[async_trait::async_trait]
pub trait CredentialStore {
/// Defines the return type of find_credentials(...)
type PasskeyItem: TryInto<Passkey> + Send + Sync;
type PasskeyItem: PasskeyAccessor + Send + Sync;

/// Find all credentials matching the given `ids` and `rp_id`.
///
Expand All @@ -71,7 +73,7 @@ pub trait CredentialStore {
) -> Result<(), StatusCode>;

/// Update the credential in your store
async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode>;
async fn update_credential(&mut self, cred: &Self::PasskeyItem) -> Result<(), StatusCode>;

/// Get information about the store
async fn get_info(&self) -> StoreInfo;
Expand Down Expand Up @@ -116,8 +118,8 @@ impl CredentialStore for MemoryStore {
Ok(())
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
self.insert(cred.credential_id.clone().into(), cred);
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.insert(cred.credential_id.clone().into(), cred.clone());
Ok(())
}

Expand Down Expand Up @@ -161,8 +163,8 @@ impl CredentialStore for Option<Passkey> {
Ok(())
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
self.replace(cred);
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.replace(cred.clone());
Ok(())
}

Expand Down Expand Up @@ -205,7 +207,7 @@ impl<S: CredentialStore<PasskeyItem = Passkey> + Send + Sync> CredentialStore
.await
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.lock().await.update_credential(cred).await
}

Expand Down Expand Up @@ -246,7 +248,7 @@ impl<S: CredentialStore<PasskeyItem = Passkey> + Send + Sync> CredentialStore
.await
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.write().await.update_credential(cred).await
}

Expand Down Expand Up @@ -287,7 +289,7 @@ impl<S: CredentialStore<PasskeyItem = Passkey> + Send + Sync> CredentialStore
.await
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.lock().await.update_credential(cred).await
}

Expand Down Expand Up @@ -328,7 +330,7 @@ impl<S: CredentialStore<PasskeyItem = Passkey> + Send + Sync> CredentialStore
.await
}

async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
async fn update_credential(&mut self, cred: &Passkey) -> Result<(), StatusCode> {
self.write().await.update_credential(cred).await
}

Expand Down
2 changes: 2 additions & 0 deletions passkey-authenticator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
mod authenticator;
mod credential_store;
mod ctap2;
mod passkey;
mod u2f;
mod user_validation;

Expand All @@ -45,6 +46,7 @@ pub use self::{
authenticator::{Authenticator, CredentialIdLength, extensions},
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
ctap2::Ctap2Api,
passkey::PasskeyAccessor,
u2f::U2fApi,
user_validation::{UiHint, UserCheck, UserValidationMethod},
};
Expand Down
Loading
Loading