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 22, 2024
1 parent 2d9a091 commit 5db9497
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 38 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Breaking Changes

- Use `Extension` enum instead of `String<_>` for `extensions` in `ctap2::get_info::Response`
- Remove `Deserialize` implementation for `ctap2::get_assertion::Response`
- Remove `Serialize` implementation for `ctap2::{get_assertion, make_credential}::Request`
- Use `AttestationStatementFormat` enum for `fmt` in `ctap2::make_credential::Response`
- Move `AttestationStatement`, `AttestationStatementFormat`, `NoneAttestationStatement`, `PackedAttestationStatement` from `ctap2::make_credential` into the `ctap2` module

### Added

Expand All @@ -20,6 +24,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
88 changes: 88 additions & 0 deletions src/ctap2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,94 @@ 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, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum AttestationStatementFormat {
None,
Packed,
}

impl AttestationStatementFormat {
fn from_str(s: &str) -> Option<Self> {
match s {
"none" => Some(Self::None),
"packed" => Some(Self::Packed),
_ => None,
}
}
}

#[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 {
known_formats: Vec<AttestationStatementFormat, 2>,
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 Some(format) = AttestationStatementFormat::from_str(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 @@ -84,7 +84,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 @@ -102,12 +102,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 @@ -128,6 +130,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 @@ -150,6 +154,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
44 changes: 10 additions & 34 deletions src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::{Bytes, String, 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 @@ -44,7 +46,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 @@ -64,6 +66,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 @@ -110,7 +114,7 @@ impl<'a> super::SerializeAttestedCredentialData for AttestedCredentialData<'a> {
#[non_exhaustive]
#[serde_indexed(offset = 1)]
pub struct Response {
pub fmt: String<32>,
pub fmt: AttestationStatementFormat,
pub auth_data: super::SerializedAuthenticatorData,
#[serde(skip_serializing_if = "Option::is_none")]
pub att_stmt: Option<AttestationStatement>,
Expand All @@ -124,7 +128,7 @@ pub struct Response {

#[derive(Debug)]
pub struct ResponseBuilder {
pub fmt: String<32>,
pub fmt: AttestationStatementFormat,
pub auth_data: super::SerializedAuthenticatorData,
}

Expand All @@ -142,34 +146,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, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum AttestationStatementFormat {
None,
Packed,
}

#[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 5db9497

Please sign in to comment.