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

Add android package name support #28

Merged
Show file tree
Hide file tree
Changes from 13 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@

- Changed: The `Client` no longer hardcodes the UV value sent to the `Authenticator` ([#22](https://github.com/1Password/passkey-rs/pull/22)).
- Changed: The `Client` no longer hardcodes the RK value sent to the `Authenticator` ([#27](https://github.com/1Password/passkey-rs/pull/27)).
- The client now supports additional user-defined properties in the client data, while also clarifying how the client
handles client data and its hash.
- ⚠ BREAKING: Changed: `register` and `authenticate` take `ClientData<E>` instead of `Option<Vec<u8>>`.
- ⚠ BREAKING: Changed: Custom client data hashes are now specified using `DefaultClientDataWithCustomHash(Vec<u8>)` instead of
`Some(Vec<u8>)`.
- Added: Additional fields can be added to the client data using `DefaultClientDataWithExtra(ExtraData)`.
- `CollectedClientData` is now generic and supports additional strongly typed fields.
- Changed: `CollectedClientData` has changed to `CollectedClientData<E = ()>`
Progdrasil marked this conversation as resolved.
Show resolved Hide resolved

## Passkey v0.2.0
### passkey-types v0.2.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ let request = CredentialCreationOptions {
};

// Now create the credential.
let my_webauthn_credential: CreatedPublicKeyCredential = my_client.register(origin, request).await?;
let my_webauthn_credential: CreatedPublicKeyCredential = my_client.register(origin, request, DefaultClientData).await?;

```

Expand Down
54 changes: 54 additions & 0 deletions passkey-client/src/client_data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use serde::Serialize;

/// A trait describing how client data should be generated during a WebAuthn operation.
pub trait ClientData<E: Serialize> {
/// Extra client data to be appended to the automatically generated client data.
fn extra_client_data(&self) -> E;

/// The hash of the client data to be used in the WebAuthn operation.
fn client_data_hash(&self) -> Option<Vec<u8>>;
}
Progdrasil marked this conversation as resolved.
Show resolved Hide resolved

/// The client data and its hash will be automatically generated from the request
/// according to the WebAuthn specification.
pub struct DefaultClientData;
impl ClientData<()> for DefaultClientData {
fn extra_client_data(&self) {}

fn client_data_hash(&self) -> Option<Vec<u8>> {
None
}
}

/// The extra client data will be appended to the automatically generated client data.
/// The hash will be automatically generated from the result client data according to the WebAuthn specification.
pub struct DefaultClientDataWithExtra<E: Serialize>(pub E);
impl<E: Serialize + Clone> ClientData<E> for DefaultClientDataWithExtra<E> {
fn extra_client_data(&self) -> E {
self.0.clone()
}
fn client_data_hash(&self) -> Option<Vec<u8>> {
None
}
}

/// The client data will be automatically generated from the request according to the WebAuthn specification
/// but it will not be used as a base for the hash. The client data hash will instead be provided by the caller.
pub struct DefaultClientDataWithCustomHash(pub Vec<u8>);
impl ClientData<()> for DefaultClientDataWithCustomHash {
fn extra_client_data(&self) {}

fn client_data_hash(&self) -> Option<Vec<u8>> {
Some(self.0.clone())
}
}

/// Backwards compatibility with the previous `register` and `authenticate` functions
/// which only took `Option<Vec<u8>>` as a client data hash.
impl ClientData<()> for Option<Vec<u8>> {
fn extra_client_data(&self) {}

fn client_data_hash(&self) -> Option<Vec<u8>> {
self.clone()
}
}
28 changes: 18 additions & 10 deletions passkey-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
//! [version]: https://img.shields.io/crates/v/passkey-client?logo=rust&style=flat
//! [documentation]: https://img.shields.io/docsrs/passkey-client/latest?logo=docs.rs&style=flat
//! [Webauthn]: https://w3c.github.io/webauthn/
mod client_data;
pub use client_data::*;

use std::borrow::Cow;

use ciborium::{cbor, value::Value};
Expand All @@ -28,6 +31,7 @@ use passkey_types::{
},
Passkey,
};
use serde::Serialize;
use typeshare::typeshare;
use url::Url;

Expand Down Expand Up @@ -163,11 +167,11 @@ where
/// Register a webauthn `request` from the given `origin`.
///
/// Returns either a [`webauthn::CreatedPublicKeyCredential`] on success or some [`WebauthnError`]
pub async fn register(
pub async fn register<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: &Url,
request: webauthn::CredentialCreationOptions,
client_data_hash: Option<Vec<u8>>,
client_data: D,
) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
// extract inner value of request as there is nothing else of value directly in CredentialCreationOptions
let request = request.public_key;
Expand All @@ -189,18 +193,20 @@ where
.rp_id_verifier
.assert_domain(origin, request.rp.id.as_deref())?;

let collected_client_data = webauthn::CollectedClientData {
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Create,
challenge: encoding::base64url(&request.challenge),
origin: origin.as_str().trim_end_matches('/').to_owned(),
cross_origin: None,
extra_data: client_data.extra_client_data(),
unknown_keys: Default::default(),
};

// SAFETY: it is a developer error if serializing this struct fails.
let client_data_json = serde_json::to_string(&collected_client_data).unwrap();
let client_data_json_hash =
client_data_hash.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
let client_data_json_hash = client_data
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());

let cred_props =
if let Some(true) = request.extensions.as_ref().and_then(|ext| ext.cred_props) {
Expand Down Expand Up @@ -293,11 +299,11 @@ where
/// Authenticate a Webauthn request.
///
/// Returns either an [`webauthn::AuthenticatedPublicKeyCredential`] on success or some [`WebauthnError`].
pub async fn authenticate(
pub async fn authenticate<D: ClientData<E>, E: Serialize + Clone>(
&mut self,
origin: &Url,
request: webauthn::CredentialRequestOptions,
client_data_hash: Option<Vec<u8>>,
client_data: D,
) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
// extract inner value of request as there is nothing else of value directly in CredentialRequestOptions
let request = request.public_key;
Expand All @@ -313,18 +319,20 @@ where
.rp_id_verifier
.assert_domain(origin, request.rp_id.as_deref())?;

let collected_client_data = webauthn::CollectedClientData {
let collected_client_data = webauthn::CollectedClientData::<E> {
ty: webauthn::ClientDataType::Get,
challenge: encoding::base64url(&request.challenge),
origin: origin.as_str().trim_end_matches('/').to_owned(),
cross_origin: None, //Some(false),
extra_data: client_data.extra_client_data(),
unknown_keys: Default::default(),
};

// SAFETY: it is a developer error if serializing this struct fails.
let client_data_json = serde_json::to_string(&collected_client_data).unwrap();
let client_data_json_hash =
client_data_hash.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());
let client_data_json_hash = client_data
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());

let ctap2_response = self
.authenticator
Expand Down
93 changes: 82 additions & 11 deletions passkey-client/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use super::*;
use coset::iana;
use passkey_authenticator::{MemoryStore, MockUserValidationMethod, UserCheck};
use passkey_types::{ctap2, rand::random_vec, Bytes};
use passkey_types::{
ctap2, encoding::try_from_base64url, rand::random_vec, webauthn::CollectedClientData, Bytes,
};
use serde::Deserialize;
use url::{ParseError, Url};

fn good_credential_creation_options() -> webauthn::PublicKeyCredentialCreationOptions {
Expand Down Expand Up @@ -91,7 +94,7 @@ async fn create_and_authenticate() {
public_key: good_credential_creation_options(),
};
let cred = client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");

Expand All @@ -101,11 +104,79 @@ async fn create_and_authenticate() {
public_key: good_credential_request_options(credential_id),
};
client
.authenticate(&origin, auth_options, None)
.authenticate(&origin, auth_options, DefaultClientData)
.await
.expect("failed to authenticate with freshly created credential");
}

#[tokio::test]
async fn create_and_authenticate_with_extra_client_data() {
#[derive(Clone, Serialize, Deserialize)]
struct AndroidClientData {
android_package_name: String,
}
let auth = Authenticator::new(
ctap2::Aaguid::new_empty(),
MemoryStore::new(),
uv_mock_with_creation(2),
);
let mut client = Client::new(auth);

let origin = Url::parse("https://future.1password.com").unwrap();
let options = webauthn::CredentialCreationOptions {
public_key: good_credential_creation_options(),
};
let extra_data = AndroidClientData {
android_package_name: "com.example.app".to_owned(),
};
let cred = client
.register(
&origin,
options,
DefaultClientDataWithExtra(extra_data.clone()),
)
.await
.expect("failed to register with options");

let returned_base64url_client_data_json: String = cred.response.client_data_json.into();
let returned_client_data_json =
try_from_base64url(returned_base64url_client_data_json.as_str())
.expect("could not base64url decode client data");
let returned_client_data: CollectedClientData<AndroidClientData> =
serde_json::from_slice(&returned_client_data_json)
.expect("could not json deserialize client data");
assert_eq!(
returned_client_data.extra_data.android_package_name,
"com.example.app"
);

let credential_id = cred.raw_id;

let auth_options = webauthn::CredentialRequestOptions {
public_key: good_credential_request_options(credential_id),
};
let result = client
.authenticate(
&origin,
auth_options,
DefaultClientDataWithExtra(extra_data.clone()),
Progdrasil marked this conversation as resolved.
Show resolved Hide resolved
)
.await
.expect("failed to authenticate with freshly created credential");

let returned_base64url_client_data_json: String = result.response.client_data_json.into();
let returned_client_data_json =
try_from_base64url(returned_base64url_client_data_json.as_str())
.expect("could not base64url decode client data");
let returned_client_data: CollectedClientData<AndroidClientData> =
serde_json::from_slice(&returned_client_data_json)
.expect("could not json deserialize client data");
assert_eq!(
returned_client_data.extra_data.android_package_name,
"com.example.app"
);
}

#[tokio::test]
async fn create_and_authenticate_with_origin_subdomain() {
let auth = Authenticator::new(
Expand All @@ -120,7 +191,7 @@ async fn create_and_authenticate_with_origin_subdomain() {
public_key: good_credential_creation_options(),
};
let cred = client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");

Expand All @@ -136,7 +207,7 @@ async fn create_and_authenticate_with_origin_subdomain() {
public_key: good_credential_request_options(cred.raw_id),
};
let res = client
.authenticate(&origin, auth_options, None)
.authenticate(&origin, auth_options, DefaultClientData)
.await
.expect("failed to authenticate with freshly created credential");
let att_obj = ctap2::AuthenticatorData::from_slice(&res.response.authenticator_data)
Expand Down Expand Up @@ -164,7 +235,7 @@ async fn create_and_authenticate_without_rp_id() {
},
};
let cred = client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");

Expand All @@ -183,7 +254,7 @@ async fn create_and_authenticate_without_rp_id() {
},
};
let res = client
.authenticate(&origin, auth_options, None)
.authenticate(&origin, auth_options, DefaultClientData)
.await
.expect("failed to authenticate with freshly created credential");
let att_obj = ctap2::AuthenticatorData::from_slice(&res.response.authenticator_data)
Expand All @@ -208,7 +279,7 @@ async fn create_and_authenticate_without_cred_params() {
},
};
let cred = client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");

Expand All @@ -218,7 +289,7 @@ async fn create_and_authenticate_without_cred_params() {
public_key: good_credential_request_options(credential_id),
};
client
.authenticate(&origin, auth_options, None)
.authenticate(&origin, auth_options, DefaultClientData)
.await
.expect("failed to authenticate with freshly created credential");
}
Expand Down Expand Up @@ -355,7 +426,7 @@ async fn client_register_triggers_uv_when_uv_is_required() {

// Act & Assert
client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");
}
Expand All @@ -382,7 +453,7 @@ async fn client_register_does_not_trigger_uv_when_uv_is_discouraged() {

// Act & Assert
client
.register(&origin, options, None)
.register(&origin, options, DefaultClientData)
.await
.expect("failed to register with options");
}
2 changes: 1 addition & 1 deletion passkey-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "passkey-types"
description = "Rust type definitions for the webauthn and CTAP specifications"
include = ["src/", "../LICENSE-APACHE", "../LICENSE-MIT"]
readme = "README.md"
version = "0.2.0"
version = "0.2.1"
authors.workspace = true
repository.workspace = true
edition.workspace = true
Expand Down
Loading
Loading