Skip to content

Commit

Permalink
Add support for attestation format preferences
Browse files Browse the repository at this point in the history
  • Loading branch information
robin-nitrokey committed Jun 27, 2024
1 parent a62ef30 commit ff20dfb
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 63 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ 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

Expand All @@ -23,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
16 changes: 16 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 @@ -138,6 +150,7 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> {
};
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 @@ -147,6 +160,7 @@ impl<'a> Arbitrary<'a> for ctap2::get_assertion::Request<'a> {
pin_auth,
pin_protocol,
enterprise_attestation,
attestation_formats_preference,
})
}
}
Expand Down Expand Up @@ -196,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 @@ -207,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
11 changes: 8 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 Down Expand Up @@ -85,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 @@ -103,12 +103,14 @@ pub struct Request<'a> {
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 @@ -129,6 +131,8 @@ pub struct Response {
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 @@ -151,6 +155,7 @@ impl ResponseBuilder {
large_blob_key: None,
unsigned_extension_outputs: None,
ep_att: None,
att_stmt: None,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ctap2/get_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub struct Response {
// FIDO_2_2
#[cfg(feature = "get-info-full")]
#[serde(skip_serializing_if = "Option::is_none")]
pub attestation_formats: Option<Vec<super::make_credential::AttestationStatementFormat, 2>>,
pub attestation_formats: Option<Vec<super::AttestationStatementFormat, 2>>,

// 0x17
// FIDO_2_2
Expand Down
66 changes: 8 additions & 58 deletions src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::{Bytes, TryFromStrError, Vec};
use crate::Vec;

use serde::{Deserialize, Serialize};
use serde_bytes::ByteArray;
use serde_indexed::{DeserializeIndexed, SerializeIndexed};

use super::{AuthenticatorOptions, Error};
use super::{
AttestationFormatsPreference, AttestationStatement, AttestationStatementFormat,
AuthenticatorOptions, Error,
};
use crate::ctap2::credential_management::CredentialProtectionPolicy;
use crate::sizes::*;
use crate::webauthn::*;

impl TryFrom<u8> for CredentialProtectionPolicy {
Expand Down Expand Up @@ -45,7 +47,7 @@ pub struct Extensions {
pub third_party_payment: Option<bool>,
}

#[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 @@ -65,6 +67,8 @@ pub struct Request<'a> {
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>,
}

pub type AttestationObject = Response;
Expand Down Expand Up @@ -143,60 +147,6 @@ impl ResponseBuilder {
}
}

#[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)]
#[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) -> 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, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct UnsignedExtensionOutputs {}
Expand Down

0 comments on commit ff20dfb

Please sign in to comment.