diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a7b57..09e6307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - ⚠ BREAKING: Rename webauthn extension outputs to be consistent with inputs. - ⚠ BREAKING: Create new extension inputs for the CTAP authenticator inputs. +- ⚠ BREAKING: Add unsigned extension outputs for the CTAP authenticator outputs. ## Passkey v0.2.0 ### passkey-types v0.2.0 diff --git a/passkey-authenticator/src/authenticator/get_assertion.rs b/passkey-authenticator/src/authenticator/get_assertion.rs index 3109e85..2fc9456 100644 --- a/passkey-authenticator/src/authenticator/get_assertion.rs +++ b/passkey-authenticator/src/authenticator/get_assertion.rs @@ -149,6 +149,7 @@ where name: "".into(), }), number_of_credentials: None, + unsigned_extension_outputs: None, }) } } diff --git a/passkey-authenticator/src/authenticator/make_credential.rs b/passkey-authenticator/src/authenticator/make_credential.rs index 6ccf8a8..68afc09 100644 --- a/passkey-authenticator/src/authenticator/make_credential.rs +++ b/passkey-authenticator/src/authenticator/make_credential.rs @@ -137,6 +137,7 @@ where auth_data, fmt: "None".into(), att_stmt: vec![0xa0].into(), // CBOR exquivalent to empty map + unsigned_extension_outputs: None, }; // 10 diff --git a/passkey-types/src/ctap2/get_assertion.rs b/passkey-types/src/ctap2/get_assertion.rs index e247c4d..ba631f3 100644 --- a/passkey-types/src/ctap2/get_assertion.rs +++ b/passkey-types/src/ctap2/get_assertion.rs @@ -12,7 +12,7 @@ pub use crate::ctap2::make_credential::Options; #[cfg(doc)] use crate::webauthn::{CollectedClientData, PublicKeyCredentialRequestOptions}; -use super::extensions::{AuthenticatorPrfInputs, HmacGetSecretInput}; +use super::extensions::{AuthenticatorPrfGetOutputs, AuthenticatorPrfInputs, HmacGetSecretInput}; serde_workaround! { /// While similar in structure to [`PublicKeyCredentialRequestOptions`], @@ -142,5 +142,58 @@ serde_workaround! { /// file an enhancement request if this limit impacts your application. #[serde(rename = 0x05, default, skip_serializing_if = Option::is_none)] pub number_of_credentials: Option, + + /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. + /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. + /// Clients MUST treat an empty map the same as an omitted field. + #[serde(rename = 0x08, default, skip_serializing_if = Option::is_none)] + pub unsigned_extension_outputs: Option, + } +} + +/// All supported Authenticator extensions outputs during credential assertion +/// +/// This is to be serialized to [`Value`] in [`AuthenticatorData::extensions`] +#[derive(Debug, Serialize, Deserialize)] +pub struct SignedExtensionOutputs { + /// Outputs the symmetric secrets after successfull processing. The output MUST be encrypted. + /// + /// + #[serde( + rename = "hmac-secret", + default, + skip_serializing_if = "Option::is_none" + )] + pub hmac_secret: Option, +} + +impl SignedExtensionOutputs { + /// Validates that there is at least one extension field that is `Some`. + /// If all fields are `None` then this returns `None` as well. + pub fn zip_contents(self) -> Option { + let Self { hmac_secret } = &self; + hmac_secret.is_some().then_some(self) + } +} + +/// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. +/// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. +/// Clients MUST treat an empty map the same as an omitted field. +#[derive(Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct UnsignedExtensionOutputs { + /// This output is supported in the Webauthn specification and will be used when the authenticator + /// and the client are in memory or communicating through an internal channel. + /// + /// If you are using transports where this needs to pass through a wire, use hmac-secret instead. + pub prf: Option, +} + +impl UnsignedExtensionOutputs { + /// Validates that there is at least one extension field that is `Some`. + /// If all fields are `None` then this returns `None` as well. + pub fn zip_contents(self) -> Option { + let Self { prf } = &self; + prf.is_some().then_some(self) } } diff --git a/passkey-types/src/ctap2/make_credential.rs b/passkey-types/src/ctap2/make_credential.rs index 0fedc11..9dbbfd4 100644 --- a/passkey-types/src/ctap2/make_credential.rs +++ b/passkey-types/src/ctap2/make_credential.rs @@ -9,7 +9,7 @@ use crate::webauthn::{ CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, }; -use super::extensions::{AuthenticatorPrfInputs, HmacGetSecretInput}; +use super::extensions::{AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, HmacGetSecretInput}; serde_workaround! { /// While similar in structure to [`PublicKeyCredentialCreationOptions`], @@ -270,5 +270,61 @@ serde_workaround! { // the keys #[serde(rename = 0x03)] pub att_stmt: ciborium::value::Value, + + /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. + /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. + /// Clients MUST treat an empty map the same as an omitted field. + #[serde(rename = 0x06, default, skip_serializing_if = Option::is_none)] + pub unsigned_extension_outputs: Option, + } +} + +/// All supported Authenticator extensions outputs during credential creation +/// +/// This is to be serialized to [`Value`] in [`AuthenticatorData::extensions`] +#[derive(Debug, Serialize, Deserialize)] +pub struct SignedExtensionOutputs { + /// A boolean value to indicate that this extension was successfully processed by the extension + /// + /// + #[serde( + rename = "hmac-secret", + default, + skip_serializing_if = "Option::is_none" + )] + pub hmac_secret: Option, +} + +impl SignedExtensionOutputs { + /// Validates that there is at least one extension field that is `Some`. + /// If all fields are `None` then this returns `None` as well. + pub fn zip_contents(self) -> Option { + let Self { hmac_secret } = &self; + let has_hmac_secret = hmac_secret.is_some(); + + (has_hmac_secret).then_some(self) + } +} + +/// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any. +/// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs. +/// Clients MUST treat an empty map the same as an omitted field. +#[derive(Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct UnsignedExtensionOutputs { + /// This output is supported in the Webauthn specification and will be used when the authenticator + /// and the client are in memory or communicating through an internal channel. + /// + /// If you are using transports where this needs to pass through a wire, use hmac-secret instead. + pub prf: Option, +} + +impl UnsignedExtensionOutputs { + /// Validates that there is at least one extension field that is `Some`. + /// If all fields are `None` then this returns `None` as well. + pub fn zip_contents(self) -> Option { + let Self { prf } = &self; + + prf.is_some().then_some(self) } }