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

Support CTAP 2.2 #51

Merged
merged 5 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use enums instead of string constants
- Introduce `Version`, `Extension` and `Transport` enums and use them in `ctap2::get_info`
- Fix serialization of the `AttestationStatementFormat` enum and use it in `ctap2::make_credential`
- Remove `Deserialize` implementation for `ctap2::get_assertion::Response`
- Remove `Serialize` implementation for `ctap2::{get_assertion, make_credential}::Request`
- Move `AttestationStatement`, `AttestationStatementFormat`, `NoneAttestationStatement`, `PackedAttestationStatement` from `ctap2::make_credential` into the `ctap2` module

### Added

- Add a `std` feature (disabled by default)
- Add `arbitrary::Arbitrary` implementations for all requests behind an `arbitrary` feature (disabled by default)
- Add support for CTAP 2.2 ([#38](https://github.com/trussed-dev/ctap-types/issues/38))
- Add support for the `thirdPartyPayment` extension behind a `third-party-payment` feature (disabled by default)
- Add new fields to `get_info`
- Add unsigned extension outputs to `make_credential` and `get_assertion`
- Add enterprise attestation support to `get_assertion`
- Add support for attestation statements in `get_assertion`
- Add support for attestation format preferences
- Derive `Copy` for `ctap2::AttestationStatementFormat`

## [0.2.0] - 2024-06-21

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ arbitrary = ["dep:arbitrary", "std"]
get-info-full = []
# enables support for implementing the large-blobs extension, see src/sizes.rs
large-blobs = []
third-party-payment = []

log-all = []
log-none = []
Expand Down
18 changes: 18 additions & 0 deletions src/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ impl<'a> Arbitrary<'a> for ctap1::register::Request<'a> {
}
}

// cannot be derived because of missing impl for Vec<_>
impl<'a> Arbitrary<'a> for ctap2::AttestationFormatsPreference {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
let known_formats = arbitrary_vec(u)?;
let unknown = u.arbitrary()?;
Ok(Self {
known_formats,
unknown,
})
}
}

// cannot be derived because of missing impl for serde_bytes::Bytes, EcdhEsHkdf256PublicKey
impl<'a> Arbitrary<'a> for ctap2::client_pin::Request<'a> {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
Expand Down Expand Up @@ -137,6 +149,8 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> {
None
};
let pin_protocol = u.arbitrary()?;
let enterprise_attestation = u.arbitrary()?;
let attestation_formats_preference = u.arbitrary()?;
Ok(Self {
rp_id,
client_data_hash,
Expand All @@ -145,6 +159,8 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> {
options,
pin_auth,
pin_protocol,
enterprise_attestation,
attestation_formats_preference,
})
}
}
Expand Down Expand Up @@ -194,6 +210,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::Request<'a> {
};
let pin_protocol = u.arbitrary()?;
let enterprise_attestation = u.arbitrary()?;
let attestation_formats_preference = u.arbitrary()?;
Ok(Self {
client_data_hash,
rp,
Expand All @@ -205,6 +222,7 @@ impl<'a> Arbitrary<'a> for ctap2::make_credential::Request<'a> {
pin_auth,
pin_protocol,
enterprise_attestation,
attestation_formats_preference,
})
}
}
Expand Down
107 changes: 106 additions & 1 deletion src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use bitflags::bitflags;
use cbor_smol::cbor_deserialize;
use serde::{Deserialize, Serialize};

use crate::{sizes::*, Bytes, Vec};
use crate::{sizes::*, Bytes, TryFromStrError, Vec};

pub use crate::operation::{Operation, VendorOperation};

Expand Down Expand Up @@ -250,6 +250,111 @@ impl<'a, A: SerializeAttestedCredentialData, E: serde::Serialize> AuthenticatorD
}
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum AttestationStatement {
None(NoneAttestationStatement),
Packed(PackedAttestationStatement),
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
#[serde(into = "&str", try_from = "&str")]
pub enum AttestationStatementFormat {
None,
Packed,
}

impl AttestationStatementFormat {
const NONE: &'static str = "none";
const PACKED: &'static str = "packed";
}

impl From<AttestationStatementFormat> for &str {
fn from(format: AttestationStatementFormat) -> Self {
match format {
AttestationStatementFormat::None => AttestationStatementFormat::NONE,
AttestationStatementFormat::Packed => AttestationStatementFormat::PACKED,
}
}
}

impl TryFrom<&str> for AttestationStatementFormat {
type Error = TryFromStrError;

fn try_from(s: &str) -> core::result::Result<Self, Self::Error> {
match s {
Self::NONE => Ok(Self::None),
Self::PACKED => Ok(Self::Packed),
_ => Err(TryFromStrError),
}
}
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct NoneAttestationStatement {}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PackedAttestationStatement {
pub alg: i32,
pub sig: Bytes<ASN1_SIGNATURE_LENGTH>,
#[serde(skip_serializing_if = "Option::is_none")]
pub x5c: Option<Vec<Bytes<1024>, 1>>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AttestationFormatsPreference {
pub(crate) known_formats: Vec<AttestationStatementFormat, 2>,
pub(crate) unknown: bool,
}

impl AttestationFormatsPreference {
pub fn known_formats(&self) -> &[AttestationStatementFormat] {
&self.known_formats
}

pub fn includes_unknown_formats(&self) -> bool {
self.unknown
}
}

impl<'de> Deserialize<'de> for AttestationFormatsPreference {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ValueVisitor;

impl<'de> serde::de::Visitor<'de> for ValueVisitor {
type Value = AttestationFormatsPreference;

fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("a sequence")
}

fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut preference = AttestationFormatsPreference::default();
while let Some(value) = seq.next_element::<&str>()? {
if let Ok(format) = AttestationStatementFormat::try_from(value) {
preference.known_formats.push(format).ok();
} else {
preference.unknown = true;
}
}
Ok(preference)
}
}

deserializer.deserialize_seq(ValueVisitor)
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
Expand Down
4 changes: 4 additions & 0 deletions src/ctap2/credential_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,8 @@ pub struct Response {
// 0x0B
#[serde(skip_serializing_if = "Option::is_none")]
pub large_blob_key: Option<ByteArray<32>>,
// 0x0C
#[cfg(feature = "third-party-payment")]
#[serde(skip_serializing_if = "Option::is_none")]
pub third_party_payment: Option<bool>,
}
53 changes: 50 additions & 3 deletions src/ctap2/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use serde_bytes::ByteArray;
use serde_indexed::{DeserializeIndexed, SerializeIndexed};

use super::{AuthenticatorOptions, Result};
use super::{AttestationFormatsPreference, AttestationStatement, AuthenticatorOptions, Result};
use crate::sizes::*;
use crate::webauthn::*;

Expand All @@ -27,10 +27,16 @@ pub struct ExtensionsInput {
#[serde(rename = "hmac-secret")]
#[serde(skip_serializing_if = "Option::is_none")]
pub hmac_secret: Option<HmacSecretInput>,

/// Whether a large blob key is requested.
#[serde(rename = "largeBlobKey")]
#[serde(skip_serializing_if = "Option::is_none")]
pub large_blob_key: Option<bool>,

#[cfg(feature = "third-party-payment")]
#[serde(rename = "thirdPartyPayment")]
#[serde(skip_serializing_if = "Option::is_none")]
pub third_party_payment: Option<bool>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
Expand All @@ -40,6 +46,30 @@ pub struct ExtensionsOutput {
#[serde(skip_serializing_if = "Option::is_none")]
// *either* enc(output1) *or* enc(output1 || output2)
pub hmac_secret: Option<Bytes<80>>,

#[cfg(feature = "third-party-payment")]
#[serde(rename = "thirdPartyPayment")]
#[serde(skip_serializing_if = "Option::is_none")]
pub third_party_payment: Option<bool>,
}

impl ExtensionsOutput {
#[inline]
pub fn is_set(&self) -> bool {
let Self {
hmac_secret,
#[cfg(feature = "third-party-payment")]
third_party_payment,
} = self;
if hmac_secret.is_some() {
return true;
}
#[cfg(feature = "third-party-payment")]
if third_party_payment.is_some() {
return true;
}
false
}
}

pub struct NoAttestedCredentialData;
Expand All @@ -55,7 +85,7 @@ pub type AuthenticatorData<'a> =

pub type AllowList<'a> = Vec<PublicKeyCredentialDescriptorRef<'a>, MAX_CREDENTIAL_COUNT_IN_LIST>;

#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)]
#[non_exhaustive]
#[serde_indexed(offset = 1)]
pub struct Request<'a> {
Expand All @@ -71,12 +101,16 @@ pub struct Request<'a> {
pub pin_auth: Option<&'a serde_bytes::Bytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pin_protocol: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enterprise_attestation: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attestation_formats_preference: Option<AttestationFormatsPreference>,
}

// NB: attn object definition / order at end of
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
// does not coincide with what python-fido2 expects in AttestationObject.__init__ *at all* :'-)
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)]
#[non_exhaustive]
#[serde_indexed(offset = 1)]
pub struct Response {
Expand All @@ -93,6 +127,12 @@ pub struct Response {
/// See https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-getAssert-authnr-alg
#[serde(skip_serializing_if = "Option::is_none")]
pub large_blob_key: Option<ByteArray<32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned_extension_outputs: Option<UnsignedExtensionOutputs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ep_att: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub att_stmt: Option<AttestationStatement>,
}

#[derive(Debug)]
Expand All @@ -113,6 +153,13 @@ impl ResponseBuilder {
number_of_credentials: None,
user_selected: None,
large_blob_key: None,
unsigned_extension_outputs: None,
ep_att: None,
att_stmt: None,
}
}
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct UnsignedExtensionOutputs {}
28 changes: 28 additions & 0 deletions src/ctap2/get_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ pub struct Response {
#[cfg(feature = "get-info-full")]
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_prototype_config_commands: Option<usize>,

// 0x16
// FIDO_2_2
#[cfg(feature = "get-info-full")]
#[serde(skip_serializing_if = "Option::is_none")]
pub attestation_formats: Option<Vec<super::AttestationStatementFormat, 2>>,

// 0x17
// FIDO_2_2
#[cfg(feature = "get-info-full")]
#[serde(skip_serializing_if = "Option::is_none")]
pub uv_count_since_last_pin_entry: Option<usize>,

// 0x18
// FIDO_2_2
#[cfg(feature = "get-info-full")]
#[serde(skip_serializing_if = "Option::is_none")]
pub long_touch_for_reset: Option<bool>,
}

impl Default for Response {
Expand Down Expand Up @@ -174,6 +192,12 @@ impl ResponseBuilder {
remaining_discoverable_credentials: None,
#[cfg(feature = "get-info-full")]
vendor_prototype_config_commands: None,
#[cfg(feature = "get-info-full")]
attestation_formats: None,
#[cfg(feature = "get-info-full")]
uv_count_since_last_pin_entry: None,
#[cfg(feature = "get-info-full")]
long_touch_for_reset: None,
}
}
}
Expand Down Expand Up @@ -227,12 +251,14 @@ pub enum Extension {
CredProtect,
HmacSecret,
LargeBlobKey,
ThirdPartyPayment,
}

impl Extension {
const CRED_PROTECT: &'static str = "credProtect";
const HMAC_SECRET: &'static str = "hmac-secret";
const LARGE_BLOB_KEY: &'static str = "largeBlobKey";
const THIRD_PARTY_PAYMENT: &'static str = "thirdPartyPayment";
}

impl From<Extension> for &str {
Expand All @@ -241,6 +267,7 @@ impl From<Extension> for &str {
Extension::CredProtect => Extension::CRED_PROTECT,
Extension::HmacSecret => Extension::HMAC_SECRET,
Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY,
Extension::ThirdPartyPayment => Extension::THIRD_PARTY_PAYMENT,
}
}
}
Expand All @@ -253,6 +280,7 @@ impl TryFrom<&str> for Extension {
Self::CRED_PROTECT => Ok(Self::CredProtect),
Self::HMAC_SECRET => Ok(Self::HmacSecret),
Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey),
Self::THIRD_PARTY_PAYMENT => Ok(Self::ThirdPartyPayment),
_ => Err(TryFromStrError),
}
}
Expand Down
Loading
Loading