Skip to content

Commit

Permalink
Add signed/unsigned extension output types to authenticator response
Browse files Browse the repository at this point in the history
  • Loading branch information
Vogeltak authored and Progdrasil committed Jul 11, 2024
1 parent c102f74 commit a7a1ef9
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions passkey-authenticator/src/authenticator/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ where
name: "".into(),
}),
number_of_credentials: None,
unsigned_extension_outputs: None,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions passkey-authenticator/src/authenticator/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 54 additions & 1 deletion passkey-types/src/ctap2/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`],
Expand Down Expand Up @@ -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<u8>,

/// 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<UnsignedExtensionOutputs>,
}
}

/// 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.
///
/// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-hmac-secret-extension>
#[serde(
rename = "hmac-secret",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret: Option<Bytes>,
}

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<Self> {
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<AuthenticatorPrfGetOutputs>,
}

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<Self> {
let Self { prf } = &self;
prf.is_some().then_some(self)
}
}
58 changes: 57 additions & 1 deletion passkey-types/src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`],
Expand Down Expand Up @@ -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<UnsignedExtensionOutputs>,
}
}

/// 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
///
/// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-hmac-secret-extension>
#[serde(
rename = "hmac-secret",
default,
skip_serializing_if = "Option::is_none"
)]
pub hmac_secret: Option<bool>,
}

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<Self> {
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<AuthenticatorPrfMakeOutputs>,
}

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<Self> {
let Self { prf } = &self;

prf.is_some().then_some(self)
}
}

0 comments on commit a7a1ef9

Please sign in to comment.