Skip to content

Commit

Permalink
Merge pull request #28 from bitwarden/PM-8579-CONTRIB-PM-7720-android…
Browse files Browse the repository at this point in the history
…-package-name-support

Add android package name support
  • Loading branch information
Progdrasil authored Jul 22, 2024
2 parents 7868809 + 551e522 commit 7a349f0
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 29 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

- 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)`.
- Added: The `Client` now has the ability to adjust the response for quirky relying parties
when a fully featured response would break their server side validation. ([#31](https://github.com/1Password/passkey-rs/pull/31))
- ⚠ BREAKING: Added the `Origin` enum which is now the origin parameter for the following methods ([#32](https://github.com/1Password/passkey-rs/pull/27)):
Expand All @@ -25,6 +31,11 @@
- `RpIdValidator::assert_domain` takes an `&Origin` instead of a `&Url`
- ⚠ BREAKING: The collected client data will now have the android app signature as the origin when a request comes from an app directly. ([#32](https://github.com/1Password/passkey-rs/pull/27))

## passkey-types

- `CollectedClientData` is now generic and supports additional strongly typed fields.
- Changed: `CollectedClientData` has changed to `CollectedClientData<E = ()>`

## 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>>;
}

/// 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()
}
}
29 changes: 19 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, fmt::Display};

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 @@ -124,6 +128,7 @@ impl Display for Origin<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Origin::Web(url) => write!(f, "{}", url.as_str().trim_end_matches('/')),
#[cfg(feature = "android-asset-validation")]
Origin::Android(target_link) => {
write!(
f,
Expand Down Expand Up @@ -209,11 +214,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: impl Into<Origin<'_>>,
request: webauthn::CredentialCreationOptions,
client_data_hash: Option<Vec<u8>>,
client_data: D,
) -> Result<webauthn::CreatedPublicKeyCredential, WebauthnError> {
let origin = origin.into();

Expand All @@ -237,18 +242,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.to_string(),
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 @@ -343,11 +350,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: impl Into<Origin<'_>>,
request: webauthn::CredentialRequestOptions,
client_data_hash: Option<Vec<u8>>,
client_data: D,
) -> Result<webauthn::AuthenticatedPublicKeyCredential, WebauthnError> {
let origin = origin.into();

Expand All @@ -365,18 +372,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.to_string(),
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 @@ -87,7 +90,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 @@ -97,11 +100,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),
)
.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 @@ -116,7 +187,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 @@ -132,7 +203,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 @@ -160,7 +231,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 @@ -179,7 +250,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 @@ -204,7 +275,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 @@ -214,7 +285,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 @@ -351,7 +422,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 @@ -378,7 +449,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

0 comments on commit 7a349f0

Please sign in to comment.