use soft_fido2_ctap::SecBytes;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelyingParty<'a> {
#[serde(borrow)]
pub id: std::borrow::Cow<'a, str>,
#[serde(borrow)]
pub name: Option<std::borrow::Cow<'a, str>>,
}
impl<'a> RelyingParty<'a> {
#[inline]
pub fn from_soft_fido2(rp: &'a soft_fido2_ctap::types::RelyingParty) -> Self {
Self {
id: std::borrow::Cow::Borrowed(&rp.id),
name: rp
.name
.as_ref()
.map(|n| std::borrow::Cow::Borrowed(n.as_str())),
}
}
pub fn to_soft_fido2(&self) -> soft_fido2_ctap::types::RelyingParty {
soft_fido2_ctap::types::RelyingParty {
id: self.id.to_string(),
name: self.name.as_ref().map(|n| n.to_string()),
}
}
#[allow(dead_code)]
pub fn into_owned(self) -> RelyingParty<'static> {
RelyingParty {
id: std::borrow::Cow::Owned(self.id.into_owned()),
name: self.name.map(|n| std::borrow::Cow::Owned(n.into_owned())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct User<'a> {
#[serde(borrow)]
pub id: std::borrow::Cow<'a, [u8]>,
#[serde(borrow)]
pub name: Option<std::borrow::Cow<'a, str>>,
#[serde(borrow)]
pub display_name: Option<std::borrow::Cow<'a, str>>,
}
impl<'a> User<'a> {
#[inline]
pub fn from_soft_fido2(user: &'a soft_fido2_ctap::types::User) -> Self {
Self {
id: std::borrow::Cow::Borrowed(&user.id),
name: user
.name
.as_ref()
.map(|n| std::borrow::Cow::Borrowed(n.as_str())),
display_name: user
.display_name
.as_ref()
.map(|n| std::borrow::Cow::Borrowed(n.as_str())),
}
}
pub fn to_soft_fido2(&self) -> soft_fido2_ctap::types::User {
soft_fido2_ctap::types::User {
id: self.id.to_vec(),
name: self.name.as_ref().map(|n| n.to_string()),
display_name: self.display_name.as_ref().map(|n| n.to_string()),
}
}
#[allow(dead_code)]
pub fn into_owned(self) -> User<'static> {
User {
id: std::borrow::Cow::Owned(self.id.into_owned()),
name: self.name.map(|n| std::borrow::Cow::Owned(n.into_owned())),
display_name: self
.display_name
.map(|n| std::borrow::Cow::Owned(n.into_owned())),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Extensions {
pub cred_protect: Option<u8>,
pub hmac_secret: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cred_random: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Credential<'a> {
#[serde(borrow)]
pub id: std::borrow::Cow<'a, [u8]>,
#[serde(borrow)]
pub rp: RelyingParty<'a>,
#[serde(borrow)]
pub user: User<'a>,
pub sign_count: u32,
pub alg: i32,
#[serde(with = "sec_bytes_serde")]
pub private_key: SecBytes,
#[serde(default)]
pub key_provider: Option<Vec<u8>>,
#[serde(default)]
pub key_format_version: Option<u16>,
pub created: i64,
pub discoverable: bool,
#[serde(default)]
pub backup_state: soft_fido2::CredentialBackupState,
#[serde(default)]
pub extensions: Extensions,
}
impl<'a> Credential<'a> {
#[inline]
pub fn from_soft_fido2(cred: &'a soft_fido2::Credential) -> Self {
let is_software = cred.key.provider.is_software();
Self {
id: std::borrow::Cow::Borrowed(&cred.id),
rp: RelyingParty::from_soft_fido2(&cred.rp),
user: User::from_soft_fido2(&cred.user),
sign_count: cred.sign_count,
alg: cred.alg,
private_key: cred.key.material.clone(),
key_provider: if is_software {
None
} else {
Some(cred.key.provider.as_bytes().to_vec())
},
key_format_version: if is_software {
None
} else {
Some(cred.key.format_version)
},
created: cred.created,
discoverable: cred.discoverable,
backup_state: cred.backup_state,
extensions: Extensions {
cred_protect: cred.extensions.cred_protect,
hmac_secret: cred.extensions.hmac_secret,
cred_random: cred.extensions.cred_random.as_ref().map(|cr| cr.to_vec()),
},
}
}
#[inline]
#[allow(dead_code)]
pub fn from_soft_fido2_ref(cred_ref: soft_fido2::CredentialRef<'a>) -> Self {
let is_software = cred_ref.key.provider.is_software();
Self {
id: std::borrow::Cow::Borrowed(cred_ref.id),
rp: RelyingParty {
id: std::borrow::Cow::Borrowed(cred_ref.rp_id),
name: cred_ref.rp_name.map(std::borrow::Cow::Borrowed),
},
user: User {
id: std::borrow::Cow::Borrowed(cred_ref.user_id),
name: cred_ref.user_name.map(std::borrow::Cow::Borrowed),
display_name: cred_ref.user_display_name.map(std::borrow::Cow::Borrowed),
},
sign_count: *cred_ref.sign_count,
alg: *cred_ref.alg,
private_key: cred_ref.key.material.clone(),
key_provider: if is_software {
None
} else {
Some(cred_ref.key.provider.as_bytes().to_vec())
},
key_format_version: if is_software {
None
} else {
Some(cred_ref.key.format_version)
},
created: *cred_ref.created,
discoverable: *cred_ref.discoverable,
backup_state: *cred_ref.backup_state,
extensions: Extensions {
cred_protect: cred_ref.cred_protect.copied(),
hmac_secret: None,
cred_random: cred_ref.cred_random.map(|cr| cr.to_vec()),
},
}
}
pub fn to_soft_fido2(&self) -> soft_fido2::Credential {
let key = match (&self.key_provider, self.key_format_version) {
(Some(provider_bytes), Some(version)) => soft_fido2_ctap::CredentialKey::new(
soft_fido2_ctap::CredentialKeyProviderId::new(provider_bytes),
version,
self.private_key.clone(),
),
_ => soft_fido2_ctap::CredentialKey::software(self.private_key.clone()),
};
soft_fido2::Credential {
id: self.id.to_vec(),
rp: self.rp.to_soft_fido2(),
user: self.user.to_soft_fido2(),
sign_count: self.sign_count,
alg: self.alg,
key,
created: self.created,
discoverable: self.discoverable,
backup_state: self.backup_state,
extensions: soft_fido2::Extensions {
cred_protect: self.extensions.cred_protect,
hmac_secret: self.extensions.hmac_secret,
cred_random: self
.extensions
.cred_random
.as_ref()
.map(|cr| soft_fido2_ctap::SecBytes::from_slice(cr)),
},
}
}
#[allow(dead_code)]
pub fn into_owned(self) -> Credential<'static> {
Credential {
id: std::borrow::Cow::Owned(self.id.into_owned()),
rp: self.rp.into_owned(),
user: self.user.into_owned(),
sign_count: self.sign_count,
alg: self.alg,
private_key: self.private_key,
key_provider: self.key_provider,
key_format_version: self.key_format_version,
created: self.created,
discoverable: self.discoverable,
backup_state: self.backup_state,
extensions: self.extensions,
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, soft_fido2::Error> {
let mut buf = Vec::new();
soft_fido2_ctap::cbor::into_writer(self, &mut buf).map_err(|_| soft_fido2::Error::Other)?;
Ok(buf)
}
pub fn from_bytes(data: &[u8]) -> Result<Credential<'static>, soft_fido2::Error> {
mod flexible_backup_state {
use serde::de::{self, Visitor};
pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<soft_fido2::CredentialBackupState, D::Error>
where
D: de::Deserializer<'de>,
{
struct BackupStateVisitor;
impl<'de> Visitor<'de> for BackupStateVisitor {
type Value = soft_fido2::CredentialBackupState;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a boolean or camelCase string")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
Ok(if v {
soft_fido2::CredentialBackupState::BackedUp
} else {
soft_fido2::CredentialBackupState::NotEligible
})
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v {
"notEligible" => Ok(soft_fido2::CredentialBackupState::NotEligible),
"eligible" => Ok(soft_fido2::CredentialBackupState::Eligible),
"backedUp" => Ok(soft_fido2::CredentialBackupState::BackedUp),
_ => Err(de::Error::unknown_variant(
v,
&["notEligible", "eligible", "backedUp"],
)),
}
}
}
deserializer.deserialize_any(BackupStateVisitor)
}
}
#[derive(serde::Deserialize)]
struct OwnedCredential {
#[serde(deserialize_with = "flexible_bytes::deserialize")]
id: Vec<u8>,
rp: OwnedRelyingParty,
user: OwnedUser,
sign_count: u32,
alg: i32,
#[serde(deserialize_with = "flexible_bytes::deserialize")]
private_key: Vec<u8>,
#[serde(default)]
key_provider: Option<Vec<u8>>,
#[serde(default)]
key_format_version: Option<u16>,
created: i64,
discoverable: bool,
#[serde(default, deserialize_with = "flexible_backup_state::deserialize")]
backup_state: soft_fido2::CredentialBackupState,
#[serde(default)]
extensions: Extensions,
}
#[derive(serde::Deserialize)]
struct OwnedRelyingParty {
id: String,
name: Option<String>,
}
#[derive(serde::Deserialize)]
struct OwnedUser {
#[serde(deserialize_with = "flexible_bytes::deserialize")]
id: Vec<u8>,
name: Option<String>,
display_name: Option<String>,
}
let owned: OwnedCredential =
soft_fido2_ctap::cbor::decode(data).map_err(|_| soft_fido2::Error::Other)?;
Ok(Credential {
id: std::borrow::Cow::Owned(owned.id),
rp: RelyingParty {
id: std::borrow::Cow::Owned(owned.rp.id),
name: owned.rp.name.map(std::borrow::Cow::Owned),
},
user: User {
id: std::borrow::Cow::Owned(owned.user.id),
name: owned.user.name.map(std::borrow::Cow::Owned),
display_name: owned.user.display_name.map(std::borrow::Cow::Owned),
},
sign_count: owned.sign_count,
alg: owned.alg,
private_key: SecBytes::new(owned.private_key),
key_provider: owned.key_provider,
key_format_version: owned.key_format_version,
created: owned.created,
discoverable: owned.discoverable,
backup_state: owned.backup_state,
extensions: owned.extensions,
})
}
}
mod flexible_bytes {
use serde::de::{Deserializer, SeqAccess, Visitor};
struct BytesVisitor;
impl<'de> Visitor<'de> for BytesVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("byte string or array of integers")
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.to_vec())
}
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut bytes = Vec::new();
while let Some(value) = seq.next_element::<u8>()? {
bytes.push(value);
}
Ok(bytes)
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(BytesVisitor)
}
}
mod sec_bytes_serde {
use soft_fido2_ctap::SecBytes;
use serde::{Deserializer, Serialize, Serializer};
pub fn serialize<S>(sec_bytes: &SecBytes, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let bytes: Vec<u8> = sec_bytes.as_slice().to_vec();
bytes.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<SecBytes, D::Error>
where
D: Deserializer<'de>,
{
let bytes = super::flexible_bytes::deserialize(deserializer)?;
Ok(SecBytes::new(bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relying_party_zero_copy() {
let soft_rp = soft_fido2_ctap::types::RelyingParty {
id: "example.com".to_string(),
name: Some("Example".to_string()),
};
let our_rp = RelyingParty::from_soft_fido2(&soft_rp);
assert_eq!(our_rp.id, "example.com");
assert_eq!(our_rp.name.as_ref().map(|s| s.as_ref()), Some("Example"));
match our_rp.id {
std::borrow::Cow::Borrowed(s) => assert_eq!(s, soft_rp.id.as_str()),
std::borrow::Cow::Owned(_) => panic!("Expected borrowed data"),
}
}
#[test]
fn test_user_zero_copy() {
let soft_user = soft_fido2_ctap::types::User {
id: vec![1, 2, 3, 4],
name: Some("user@example.com".to_string()),
display_name: Some("User Name".to_string()),
};
let our_user = User::from_soft_fido2(&soft_user);
assert_eq!(our_user.id.as_ref(), &[1, 2, 3, 4]);
assert_eq!(
our_user.name.as_ref().map(|s| s.as_ref()),
Some("user@example.com")
);
match our_user.id {
std::borrow::Cow::Borrowed(s) => assert_eq!(s, soft_user.id.as_slice()),
std::borrow::Cow::Owned(_) => panic!("Expected borrowed data"),
}
}
#[test]
fn test_credential_serialization_roundtrip() {
let soft_cred = soft_fido2::Credential {
id: vec![1, 2, 3, 4],
rp: soft_fido2_ctap::types::RelyingParty {
id: "example.com".to_string(),
name: Some("Example".to_string()),
},
user: soft_fido2_ctap::types::User {
id: vec![5, 6, 7, 8],
name: Some("user@example.com".to_string()),
display_name: Some("User Name".to_string()),
},
sign_count: 42,
alg: -7,
key: soft_fido2_ctap::CredentialKey::software(SecBytes::new(vec![0u8; 32])),
created: 1234567890,
discoverable: true,
backup_state: soft_fido2::CredentialBackupState::NotEligible,
extensions: soft_fido2::Extensions {
cred_protect: Some(1),
hmac_secret: None,
cred_random: None,
},
};
let our_cred = Credential::from_soft_fido2(&soft_cred);
let our_bytes = our_cred.to_bytes().expect("Serialization should succeed");
let deserialized =
Credential::from_bytes(&our_bytes).expect("Should deserialize our format");
assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
assert_eq!(deserialized.rp.id, "example.com");
assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
assert_eq!(deserialized.sign_count, 42);
assert_eq!(deserialized.alg, -7);
assert_eq!(deserialized.created, 1234567890);
assert!(deserialized.discoverable);
assert_eq!(
deserialized.backup_state,
soft_fido2::CredentialBackupState::NotEligible
);
}
#[test]
fn test_minimal_credential_roundtrip() {
let our_cred = Credential {
id: std::borrow::Cow::Owned(vec![1, 2, 3, 4]),
rp: RelyingParty {
id: std::borrow::Cow::Owned("example.com".to_string()),
name: None,
},
user: User {
id: std::borrow::Cow::Owned(vec![5, 6, 7, 8]),
name: Some(std::borrow::Cow::Owned("user".to_string())),
display_name: None,
},
sign_count: 0,
alg: -7,
private_key: SecBytes::new(vec![0u8; 32]),
key_provider: None,
key_format_version: None,
created: 0,
discoverable: true,
backup_state: soft_fido2::CredentialBackupState::NotEligible,
extensions: Extensions::default(),
};
let our_bytes = our_cred.to_bytes().expect("Should serialize our format");
let deserialized =
Credential::from_bytes(&our_bytes).expect("Should deserialize our format");
assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
assert_eq!(deserialized.rp.id, "example.com");
assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
}
#[test]
fn test_serialization_roundtrip() {
let soft_cred = soft_fido2::Credential {
id: vec![1, 2, 3, 4],
rp: soft_fido2_ctap::types::RelyingParty {
id: "example.com".to_string(),
name: Some("Example".to_string()),
},
user: soft_fido2_ctap::types::User {
id: vec![5, 6, 7, 8],
name: Some("user@example.com".to_string()),
display_name: Some("User Name".to_string()),
},
sign_count: 42,
alg: -7,
key: soft_fido2_ctap::CredentialKey::software(SecBytes::new(vec![0u8; 32])),
created: 1234567890,
discoverable: true,
backup_state: soft_fido2::CredentialBackupState::NotEligible,
extensions: soft_fido2::Extensions {
cred_protect: Some(1),
hmac_secret: None,
cred_random: None,
},
};
let our_cred = Credential::from_soft_fido2(&soft_cred);
let our_bytes = our_cred
.to_bytes()
.expect("Should be able to serialize our credential format");
let deserialized =
Credential::from_bytes(&our_bytes).expect("Should deserialize our own format");
assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
assert_eq!(deserialized.rp.id, "example.com");
assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
assert_eq!(
deserialized.user.name.as_ref().map(|s| s.as_ref()),
Some("user@example.com")
);
assert_eq!(
deserialized.user.display_name.as_ref().map(|s| s.as_ref()),
Some("User Name")
);
assert_eq!(deserialized.sign_count, 42);
assert_eq!(deserialized.alg, -7);
assert_eq!(deserialized.created, 1234567890);
assert!(deserialized.discoverable);
}
}