Skip to content

Commit

Permalink
Use enums instead of string constants
Browse files Browse the repository at this point in the history
Previously, we used String<_> to represent string constants in some
responses.  This has multiple drawbacks:
- It is error-prone because the value is not validated.
- Construction is fallible because of the fixed length of the string.
- The length needs to be bumped if longer values are added.

This patch introduces enums to replace these constants.  As cbor_smol
serializes enums using the variant index instead of the string, we need
to manually implement the string conversion.
  • Loading branch information
robin-nitrokey committed Jun 22, 2024
1 parent be9589d commit f495443
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 10 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

[Unreleased]: https://github.com/trussed-dev/ctap-types/compare/0.2.0...HEAD

-
### Breaking Changes

- 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`

## [0.2.0] - 2024-06-21

Expand Down
124 changes: 119 additions & 5 deletions src/ctap2/get_info.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::webauthn::FilteredPublicKeyCredentialParameters;
use crate::{Bytes, String, Vec};
use crate::{Bytes, TryFromStrError, Vec};
use serde::{Deserialize, Serialize};
use serde_indexed::{DeserializeIndexed, SerializeIndexed};

Expand All @@ -10,11 +10,11 @@ pub type AuthenticatorInfo = Response;
#[serde_indexed(offset = 1)]
pub struct Response {
// 0x01
pub versions: Vec<String<12>, 4>,
pub versions: Vec<Version, 4>,

// 0x02
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<String<13>, 4>>,
pub extensions: Option<Vec<Extension, 4>>,

// 0x03
pub aaguid: Bytes<16>,
Expand Down Expand Up @@ -44,7 +44,7 @@ pub struct Response {
// 0x09
// FIDO_2_1
#[serde(skip_serializing_if = "Option::is_none")]
pub transports: Option<Vec<String<8>, 4>>,
pub transports: Option<Vec<Transport, 4>>,

// 0x0A
// FIDO_2_1
Expand Down Expand Up @@ -135,7 +135,7 @@ impl Default for Response {

#[derive(Debug)]
pub struct ResponseBuilder {
pub versions: Vec<String<12>, 4>,
pub versions: Vec<Version, 4>,
pub aaguid: Bytes<16>,
}

Expand Down Expand Up @@ -178,6 +178,120 @@ impl ResponseBuilder {
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(into = "&str", try_from = "&str")]
pub enum Version {
Fido2_0,
Fido2_1,
Fido2_1Pre,
U2fV2,
}

impl Version {
const FIDO_2_0: &'static str = "FIDO_2_0";
const FIDO_2_1: &'static str = "FIDO_2_1";
const FIDO_2_1_PRE: &'static str = "FIDO_2_1_PRE";
const U2F_V2: &'static str = "U2F_V2";
}

impl From<Version> for &str {
fn from(version: Version) -> Self {
match version {
Version::Fido2_0 => Version::FIDO_2_0,
Version::Fido2_1 => Version::FIDO_2_1,
Version::Fido2_1Pre => Version::FIDO_2_1_PRE,
Version::U2fV2 => Version::U2F_V2,
}
}
}

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

fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
Self::FIDO_2_0 => Ok(Self::Fido2_0),
Self::FIDO_2_1 => Ok(Self::Fido2_1),
Self::FIDO_2_1_PRE => Ok(Self::Fido2_1Pre),
Self::U2F_V2 => Ok(Self::U2fV2),
_ => Err(TryFromStrError),
}
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(into = "&str", try_from = "&str")]
pub enum Extension {
CredProtect,
HmacSecret,
LargeBlobKey,
}

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

impl From<Extension> for &str {
fn from(extension: Extension) -> Self {
match extension {
Extension::CredProtect => Extension::CRED_PROTECT,
Extension::HmacSecret => Extension::HMAC_SECRET,
Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY,
}
}
}

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

fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
Self::CRED_PROTECT => Ok(Self::CredProtect),
Self::HMAC_SECRET => Ok(Self::HmacSecret),
Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey),
_ => Err(TryFromStrError),
}
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(into = "&str", try_from = "&str")]
pub enum Transport {
Nfc,
Usb,
}

impl Transport {
const NFC: &'static str = "nfc";
const USB: &'static str = "usb";
}

impl From<Transport> for &str {
fn from(transport: Transport) -> Self {
match transport {
Transport::Nfc => Transport::NFC,
Transport::Usb => Transport::USB,
}
}
}

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

fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
Self::NFC => Ok(Self::Nfc),
Self::USB => Ok(Self::Usb),
_ => Err(TryFromStrError),
}
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
Expand Down
34 changes: 30 additions & 4 deletions src/ctap2/make_credential.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Bytes, String, Vec};
use crate::{Bytes, TryFromStrError, Vec};

use serde::{Deserialize, Serialize};
use serde_bytes::ByteArray;
Expand Down Expand Up @@ -103,7 +103,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 @@ -115,7 +115,7 @@ pub struct Response {

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

Expand Down Expand Up @@ -143,12 +143,38 @@ pub enum AttestationStatement {

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(untagged)]
#[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(version: AttestationStatementFormat) -> Self {
match version {
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 {}

Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ pub mod webauthn;

pub use ctap2::{Error, Result};

use core::fmt::{self, Display, Formatter};

/// An error returned by the `TryFrom<&str>` implementation for enums if an invalid value is
/// provided.
#[derive(Debug)]
pub struct TryFromStrError;

impl Display for TryFromStrError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
"invalid enum value".fmt(f)
}
}

#[cfg(test)]
mod tests {}

Expand Down

0 comments on commit f495443

Please sign in to comment.