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

Use enums instead of string constants #52

Merged
merged 2 commits into from
Jun 24, 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
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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ serde-indexed = "0.1.1"
serde_bytes = { version = "0.11.14", default-features = false }
serde_repr = "0.1"

[dev-dependencies]
serde_test = "1.0.176"

[features]
# enables all fields for ctap2::get_info
get-info-full = []
Expand Down
289 changes: 284 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 Expand Up @@ -299,3 +413,168 @@ pub struct Certifications {
#[serde(skip_serializing_if = "Option::is_none")]
pub fido: Option<u8>,
}

#[cfg(test)]
mod tests {
use super::*;
use serde_test::{assert_ser_tokens, assert_tokens, Token};

#[test]
fn test_serde_version() {
let versions = [
(Version::Fido2_0, "FIDO_2_0"),
(Version::Fido2_1, "FIDO_2_1"),
(Version::Fido2_1Pre, "FIDO_2_1_PRE"),
(Version::U2fV2, "U2F_V2"),
];
for (version, s) in versions {
assert_tokens(&version, &[Token::BorrowedStr(s)]);
}
}

#[test]
fn test_serde_extension() {
let extensions = [
(Extension::CredProtect, "credProtect"),
(Extension::HmacSecret, "hmac-secret"),
(Extension::LargeBlobKey, "largeBlobKey"),
];
for (extension, s) in extensions {
assert_tokens(&extension, &[Token::BorrowedStr(s)]);
}
}

#[test]
fn test_serde_transport() {
let transports = [(Transport::Nfc, "nfc"), (Transport::Usb, "usb")];
for (transport, s) in transports {
assert_tokens(&transport, &[Token::BorrowedStr(s)]);
}
}

#[test]
fn test_serde_get_info_minimal() {
let versions = Vec::from_slice(&[Version::Fido2_0, Version::Fido2_1]).unwrap();
let aaguid = Bytes::from_slice(&[0xff; 16]).unwrap();
let response = ResponseBuilder { versions, aaguid }.build();
assert_tokens(
&response,
&[
Token::Map { len: Some(2) },
Token::U64(1),
Token::Seq { len: Some(2) },
Token::BorrowedStr("FIDO_2_0"),
Token::BorrowedStr("FIDO_2_1"),
Token::SeqEnd,
Token::U64(3),
Token::BorrowedBytes(&[0xff; 16]),
Token::MapEnd,
],
);
}

#[test]
fn test_serde_get_info_default() {
// This corresponds to the response sent by the Nitrokey 3, see for example:
// https://github.com/Nitrokey/nitrokey-3-firmware/blob/0d7209f1f75354878c0cf3454055defe8372ed14/utils/fido2-mds/metadata/v4/metadata-nk3xn-v4.json
const AAGUID: &[u8] = &[
236, 153, 219, 25, 205, 31, 76, 6, 162, 169, 148, 15, 23, 166, 163, 11,
];
let versions =
Vec::from_slice(&[Version::U2fV2, Version::Fido2_0, Version::Fido2_1]).unwrap();
let aaguid = Bytes::from_slice(AAGUID).unwrap();
let mut options = CtapOptions::default();
options.rk = true;
options.plat = Some(false);
options.client_pin = Some(false);
options.cred_mgmt = Some(true);
options.large_blobs = Some(false);
options.pin_uv_auth_token = Some(true);
let mut response = ResponseBuilder { versions, aaguid }.build();
response.extensions =
Some(Vec::from_slice(&[Extension::CredProtect, Extension::HmacSecret]).unwrap());
response.options = Some(options);
response.max_msg_size = Some(3072);
response.pin_protocols = Some(Vec::from_slice(&[1, 0]).unwrap());
response.max_creds_in_list = Some(10);
response.max_cred_id_length = Some(255);
response.transports = Some(Vec::from_slice(&[Transport::Nfc, Transport::Usb]).unwrap());
assert_ser_tokens(
&response,
&[
Token::Map { len: Some(9) },
// 0x01: versions
Token::U64(0x01),
Token::Seq { len: Some(3) },
Token::BorrowedStr("U2F_V2"),
Token::BorrowedStr("FIDO_2_0"),
Token::BorrowedStr("FIDO_2_1"),
Token::SeqEnd,
// 0x02: extensions
Token::U64(0x02),
Token::Some,
Token::Seq { len: Some(2) },
Token::BorrowedStr("credProtect"),
Token::BorrowedStr("hmac-secret"),
Token::SeqEnd,
// 0x03: aaguid
Token::U64(0x03),
Token::BorrowedBytes(AAGUID),
// 0x04: options
Token::U64(0x04),
Token::Some,
Token::Struct {
name: "CtapOptions",
len: 7,
},
Token::BorrowedStr("rk"),
Token::Bool(true),
Token::BorrowedStr("up"),
Token::Bool(true),
Token::BorrowedStr("plat"),
Token::Some,
Token::Bool(false),
Token::BorrowedStr("credMgmt"),
Token::Some,
Token::Bool(true),
Token::BorrowedStr("clientPin"),
Token::Some,
Token::Bool(false),
Token::BorrowedStr("largeBlobs"),
Token::Some,
Token::Bool(false),
Token::BorrowedStr("pinUvAuthToken"),
Token::Some,
Token::Bool(true),
Token::StructEnd,
// 0x05: maxMsgSize
Token::U64(0x05),
Token::Some,
Token::U64(3072),
// 0x06: pinUvAuthProtocols
Token::U64(0x06),
Token::Some,
Token::Seq { len: Some(2) },
Token::U8(1),
Token::U8(0),
Token::SeqEnd,
// 0x07: maxCredentialCountInList
Token::U64(0x07),
Token::Some,
Token::U64(10),
// 0x08: maxCredentialIdLength
Token::U64(0x08),
Token::Some,
Token::U64(255),
// 0x09: transports
Token::U64(0x09),
Token::Some,
Token::Seq { len: Some(2) },
Token::BorrowedStr("nfc"),
Token::BorrowedStr("usb"),
Token::SeqEnd,
Token::MapEnd,
],
);
}
}
Loading
Loading