Skip to content

Commit

Permalink
Merge pull request #33 from 1Password/refactor-extension-types
Browse files Browse the repository at this point in the history
Refactor extension types (PRF#1)
  • Loading branch information
Progdrasil authored Jul 25, 2024
2 parents 94db2aa + 6fce95c commit 9576585
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 71 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ handles client data and its hash.

## passkey-types

- `CollectedClientData` is now generic and supports additional strongly typed fields.
- `CollectedClientData` is now generic and supports additional strongly typed fields. ([#28](https://github.com/1Password/passkey-rs/pull/28))
- Changed: `CollectedClientData` has changed to `CollectedClientData<E = ()>`
- The `Client` now returns `CredProps::rk` depending on the authenticator's capabilities.
- The `Client` now returns `CredProps::rk` depending on the authenticator's capabilities. ([#29](https://github.com/1Password/passkey-rs/pull/29))
- ⚠ BREAKING: Rename webauthn extension outputs to be consistent with inputs. ([#33](https://github.com/1Password/passkey-rs/pull/33))
- ⚠ BREAKING: Create new extension inputs for the CTAP authenticator inputs. ([#33](https://github.com/1Password/passkey-rs/pull/33))

## Passkey v0.2.0
### passkey-types v0.2.0
Expand Down
2 changes: 1 addition & 1 deletion passkey-authenticator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use passkey_types::{ctap2::Ctap2Error, Bytes};

pub use self::{
authenticator::Authenticator,
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore},
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
ctap2::Ctap2Api,
u2f::U2fApi,
user_validation::{UserCheck, UserValidationMethod},
Expand Down
78 changes: 78 additions & 0 deletions passkey-client/src/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! WebAuthn extensions as defined in [WebAuthn Defined Extensions][webauthn]
//! and [CTAP2 Defined Extensions][ctap2].
//!
//! The currently supported extensions are:
//! * [`Credential Properties`][credprops]
//!
//! [ctap2]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-defined-extensions
//! [webauthn]: https://w3c.github.io/webauthn/#sctn-defined-extensions
//! [credprops]: https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension

use passkey_authenticator::{
CredentialStore, DiscoverabilitySupport, StoreInfo, UserValidationMethod,
};
use passkey_types::{
ctap2::{get_assertion, make_credential},
webauthn::{
AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs,
CredentialPropertiesOutput,
},
Passkey,
};

use crate::Client;

impl<S, U, P> Client<S, U, P>
where
S: CredentialStore + Sync,
U: UserValidationMethod + Sync,
P: public_suffix::EffectiveTLDProvider + Sync + 'static,
Passkey: TryFrom<<S as CredentialStore>::PasskeyItem>,
{
/// Create the extension inputs to be passed to an authenticator over CTAP2
/// during a registration request.
pub(super) fn registration_extension_ctap2_input(
&self,
request: Option<&AuthenticationExtensionsClientInputs>,
) -> Option<make_credential::ExtensionInputs> {
request.map(|_| make_credential::ExtensionInputs {})
}

/// Build the extension outputs for the WebAuthn client in a registration request.
pub(super) fn registration_extension_outputs(
&self,
request: Option<&AuthenticationExtensionsClientInputs>,
store_info: StoreInfo,
rk: bool,
) -> AuthenticationExtensionsClientOutputs {
let cred_props_requested = request.and_then(|ext| ext.cred_props) == Some(true);
let cred_props = if cred_props_requested {
let discoverable = match store_info.discoverability {
DiscoverabilitySupport::Full => rk,
DiscoverabilitySupport::OnlyNonDiscoverable => false,
DiscoverabilitySupport::ForcedDiscoverable => true,
};

Some(CredentialPropertiesOutput {
discoverable: Some(discoverable),
authenticator_display_name: self.authenticator.display_name().cloned(),
})
} else {
None
};

AuthenticationExtensionsClientOutputs {
cred_props,
prf: None,
}
}

/// Create the extension inputs to be passed to an authenticator over CTAP2
/// during an authentication request.
pub(super) fn auth_extension_ctap2_input(
&self,
request: Option<&AuthenticationExtensionsClientInputs>,
) -> Option<get_assertion::ExtensionInputs> {
request.map(|_| get_assertion::ExtensionInputs {})
}
}
41 changes: 15 additions & 26 deletions passkey-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,21 @@ use std::{borrow::Cow, fmt::Display};

use ciborium::{cbor, value::Value};
use coset::{iana::EnumI64, Algorithm};
use passkey_authenticator::{
Authenticator, CredentialStore, DiscoverabilitySupport, UserValidationMethod,
};
use passkey_authenticator::{Authenticator, CredentialStore, UserValidationMethod};
use passkey_types::{
crypto::sha256,
ctap2, encoding,
webauthn::{
self, AuthenticatorExtensionsClientOutputs, AuthenticatorSelectionCriteria,
CredentialPropertiesOutput, ResidentKeyRequirement, UserVerificationRequirement,
self, AuthenticationExtensionsClientOutputs, AuthenticatorSelectionCriteria,
ResidentKeyRequirement, UserVerificationRequirement,
},
Passkey,
};
use serde::Serialize;
use typeshare::typeshare;
use url::Url;

mod extensions;
mod quirks;
use quirks::QuirkyRp;

Expand Down Expand Up @@ -259,8 +258,9 @@ where
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());

let cred_props_requested =
request.extensions.as_ref().and_then(|ext| ext.cred_props) == Some(true);
let extension_request = request.extensions.and_then(|e| e.zip_contents());

let ctap_extensions = self.registration_extension_ctap2_input(extension_request.as_ref());

let rk = self.map_rk(&request.authenticator_selection, &auth_info);
let uv = request.authenticator_selection.map(|s| s.user_verification)
Expand All @@ -277,7 +277,7 @@ where
user: request.user,
pub_key_cred_params,
exclude_list: request.exclude_credentials,
extensions: request.extensions,
extensions: ctap_extensions,
options: ctap2::make_credential::Options { rk, up: true, uv },
pin_auth: None,
pin_protocol: None,
Expand Down Expand Up @@ -321,21 +321,9 @@ where
.map_err(|e| WebauthnError::AuthenticatorError(e.into()))?,
);

let cred_props = if cred_props_requested {
let auth_discoverability = self.authenticator.store().get_info().await.discoverability;
let discoverable = match auth_discoverability {
DiscoverabilitySupport::Full => rk,
DiscoverabilitySupport::OnlyNonDiscoverable => false,
DiscoverabilitySupport::ForcedDiscoverable => true,
};

Some(CredentialPropertiesOutput {
discoverable: Some(discoverable),
authenticator_display_name: self.authenticator.display_name().cloned(),
})
} else {
None
};
let store_info = self.authenticator.store().get_info().await;
let client_extension_results =
self.registration_extension_outputs(extension_request.as_ref(), store_info, rk);

let response = webauthn::CreatedPublicKeyCredential {
id: encoding::base64url(credential_id.credential_id()),
Expand All @@ -350,7 +338,7 @@ where
transports: auth_info.transports,
},
authenticator_attachment: Some(self.authenticator().attachment_type()),
client_extension_results: AuthenticatorExtensionsClientOutputs { cred_props },
client_extension_results,
};

// Sanitize output before sending it back to the RP
Expand Down Expand Up @@ -398,6 +386,7 @@ where
.client_data_hash()
.unwrap_or_else(|| sha256(client_data_json.as_bytes()).to_vec());

let ctap_extensions = self.auth_extension_ctap2_input(request.extensions.as_ref());
let rk = false;
let uv = request.user_verification != UserVerificationRequirement::Discouraged;

Expand All @@ -407,7 +396,7 @@ where
rp_id: rp_id.to_owned(),
client_data_hash: client_data_json_hash.into(),
allow_list: request.allow_credentials,
extensions: request.extensions,
extensions: ctap_extensions,
options: ctap2::get_assertion::Options { rk, up: true, uv },
pin_auth: None,
pin_protocol: None,
Expand All @@ -432,7 +421,7 @@ where
attestation_object: None,
},
authenticator_attachment: Some(self.authenticator().attachment_type()),
client_extension_results: AuthenticatorExtensionsClientOutputs::default(),
client_extension_results: AuthenticationExtensionsClientOutputs::default(),
})
}

Expand Down
13 changes: 8 additions & 5 deletions passkey-types/src/ctap2/get_assertion.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
//! <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetAssertion>
use serde::{Deserialize, Serialize};

use crate::{
ctap2::AuthenticatorData,
webauthn::{
AuthenticationExtensionsClientInputs, PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity,
},
webauthn::{PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity},
Bytes,
};

Expand Down Expand Up @@ -36,7 +35,7 @@ serde_workaround! {
/// Parameters to influence authenticator operation. These parameters might be authenticator
/// specific.
#[serde(rename = 0x04, default, skip_serializing_if = Option::is_none)]
pub extensions: Option<AuthenticationExtensionsClientInputs>,
pub extensions: Option<ExtensionInputs>,

/// Parameters to influence authenticator operation, see [`Options`] for more details.
#[serde(rename = 0x05, default)]
Expand All @@ -53,6 +52,10 @@ serde_workaround! {
}
}

/// All supported Authenticator extensions inputs during credential assertion
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ExtensionInputs {}

serde_workaround! {
/// Type returned from `Authenticator::get_assertion` on success.
#[derive(Debug)]
Expand Down
6 changes: 5 additions & 1 deletion passkey-types/src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ serde_workaround! {
/// Parameters to influence authenticator operation, as specified in [`webauthn`].
/// These parameters might be authenticator specific.
#[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)]
pub extensions: Option<webauthn::AuthenticationExtensionsClientInputs>,
pub extensions: Option<ExtensionInputs>,

/// Parameters to influence authenticator operation, see [`Options`] for more details.
#[serde(rename = 0x07, default)]
Expand Down Expand Up @@ -199,6 +199,10 @@ const fn default_true() -> bool {
true
}

/// All supported Authenticator extensions inputs during credential creation
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ExtensionInputs {}

serde_workaround! {
/// Upon successful creation of a credential, the authenticator returns an attestation object.
#[derive(Debug)]
Expand Down
2 changes: 1 addition & 1 deletion passkey-types/src/webauthn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ pub struct PublicKeyCredential<R: AuthenticatorResponse> {
/// This object is a map containing extension identifier → client extension output entries
/// produced by the extension’s client extension processing.
#[serde(default)]
pub client_extension_results: AuthenticatorExtensionsClientOutputs,
pub client_extension_results: AuthenticationExtensionsClientOutputs,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,6 @@ use typeshare::typeshare;
#[cfg(doc)]
use crate::webauthn::PublicKeyCredential;

/// This is a dictionary containing the client extension input values for zero or more
/// [WebAuthn Extensions]. There are currently none supported.
///
/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientinputs>
///
/// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct AuthenticationExtensionsClientInputs {
/// Boolean to indicate that this extension is requested by the relying party.
///
/// See [`CredentialPropertiesOutput`] for more information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cred_props: Option<bool>,
}

/// This is a dictionary containing the client extension output values for zero or more
/// [WebAuthn Extensions].
///
/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs>
///
/// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct AuthenticatorExtensionsClientOutputs {
/// Contains properties of the given [`PublicKeyCredential`] when it is included.
///
/// See [`CredentialPropertiesOutput`] for more information
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cred_props: Option<CredentialPropertiesOutput>,
}

/// This client registration extension facilitates reporting certain credential properties known by
/// the client to the requesting WebAuthn [Relying Party] upon creation of a [`PublicKeyCredential`]
/// source as a result of a registration ceremony.
Expand All @@ -47,7 +13,7 @@ pub struct AuthenticatorExtensionsClientOutputs {
/// [Relying Party]: https://w3c.github.io/webauthn/#relying-party
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
#[typeshare(swift = "Equatable")]
pub struct CredentialPropertiesOutput {
/// This OPTIONAL property, known abstractly as the resident key credential property
/// (i.e., client-side [discoverable credential] property), is a Boolean value indicating whether
Expand Down
68 changes: 68 additions & 0 deletions passkey-types/src/webauthn/extensions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;

mod credential_properties;
mod pseudo_random_function;

pub use credential_properties::*;
pub use pseudo_random_function::*;

/// This is a dictionary containing the client extension input values for zero or more
/// [WebAuthn Extensions]. There are currently none supported.
///
/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientinputs>
///
/// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct AuthenticationExtensionsClientInputs {
/// Boolean to indicate that this extension is requested by the relying party.
///
/// See [`CredentialPropertiesOutput`] for more information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cred_props: Option<bool>,

/// Inputs for the pseudo-random function extensions.
///
/// See [`AuthenticationExtensionsPrfInputs`] for more information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prf: Option<AuthenticationExtensionsPrfInputs>,
}

impl AuthenticationExtensionsClientInputs {
/// Validates that there is at least one extension field that is `Some`
/// and that they are in turn not empty. If all fields are `None`
/// then this returns `None` as well.
pub fn zip_contents(self) -> Option<Self> {
let Self { cred_props, prf } = &self;

let has_cred_props = cred_props.is_some();
let has_prf = prf.is_some();

(has_cred_props || has_prf).then_some(self)
}
}

/// This is a dictionary containing the client extension output values for zero or more
/// [WebAuthn Extensions].
///
/// <https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs>
///
/// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#webauthn-extensions
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct AuthenticationExtensionsClientOutputs {
/// Contains properties of the given [`PublicKeyCredential`] when it is included.
///
/// See [`CredentialPropertiesOutput`] for more information
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cred_props: Option<CredentialPropertiesOutput>,

/// Contains the results of evaluating the PRF.
///
/// See [`AuthenticationExtensionsPrfOutputs`] for more information.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prf: Option<AuthenticationExtensionsPrfOutputs>,
}
Loading

0 comments on commit 9576585

Please sign in to comment.