1use soft_fido2_ctap::SecBytes;
31
32use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct RelyingParty<'a> {
37 #[serde(borrow)]
39 pub id: std::borrow::Cow<'a, str>,
40
41 #[serde(borrow)]
43 pub name: Option<std::borrow::Cow<'a, str>>,
44}
45
46impl<'a> RelyingParty<'a> {
47 #[inline]
49 pub fn from_soft_fido2(rp: &'a soft_fido2_ctap::types::RelyingParty) -> Self {
50 Self {
51 id: std::borrow::Cow::Borrowed(&rp.id),
52 name: rp
53 .name
54 .as_ref()
55 .map(|n| std::borrow::Cow::Borrowed(n.as_str())),
56 }
57 }
58
59 pub fn to_soft_fido2(&self) -> soft_fido2_ctap::types::RelyingParty {
61 soft_fido2_ctap::types::RelyingParty {
62 id: self.id.to_string(),
63 name: self.name.as_ref().map(|n| n.to_string()),
64 }
65 }
66
67 #[allow(dead_code)]
69 pub fn into_owned(self) -> RelyingParty<'static> {
70 RelyingParty {
71 id: std::borrow::Cow::Owned(self.id.into_owned()),
72 name: self.name.map(|n| std::borrow::Cow::Owned(n.into_owned())),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct User<'a> {
80 #[serde(borrow)]
81 pub id: std::borrow::Cow<'a, [u8]>,
82
83 #[serde(borrow)]
84 pub name: Option<std::borrow::Cow<'a, str>>,
85
86 #[serde(borrow)]
87 pub display_name: Option<std::borrow::Cow<'a, str>>,
88}
89
90impl<'a> User<'a> {
91 #[inline]
93 pub fn from_soft_fido2(user: &'a soft_fido2_ctap::types::User) -> Self {
94 Self {
95 id: std::borrow::Cow::Borrowed(&user.id),
96 name: user
97 .name
98 .as_ref()
99 .map(|n| std::borrow::Cow::Borrowed(n.as_str())),
100 display_name: user
101 .display_name
102 .as_ref()
103 .map(|n| std::borrow::Cow::Borrowed(n.as_str())),
104 }
105 }
106
107 pub fn to_soft_fido2(&self) -> soft_fido2_ctap::types::User {
109 soft_fido2_ctap::types::User {
110 id: self.id.to_vec(),
111 name: self.name.as_ref().map(|n| n.to_string()),
112 display_name: self.display_name.as_ref().map(|n| n.to_string()),
113 }
114 }
115
116 #[allow(dead_code)]
118 pub fn into_owned(self) -> User<'static> {
119 User {
120 id: std::borrow::Cow::Owned(self.id.into_owned()),
121 name: self.name.map(|n| std::borrow::Cow::Owned(n.into_owned())),
122 display_name: self
123 .display_name
124 .map(|n| std::borrow::Cow::Owned(n.into_owned())),
125 }
126 }
127}
128
129#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
131pub struct Extensions {
132 pub cred_protect: Option<u8>,
134 pub hmac_secret: Option<bool>,
136 #[serde(skip_serializing_if = "Option::is_none", default)]
139 pub cred_random: Option<Vec<u8>>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct Credential<'a> {
147 #[serde(borrow)]
148 pub id: std::borrow::Cow<'a, [u8]>,
149
150 #[serde(borrow)]
151 pub rp: RelyingParty<'a>,
152
153 #[serde(borrow)]
154 pub user: User<'a>,
155
156 pub sign_count: u32,
157
158 pub alg: i32,
159
160 #[serde(with = "sec_bytes_serde")]
161 pub private_key: SecBytes,
162
163 #[serde(default)]
164 pub key_provider: Option<Vec<u8>>,
165
166 #[serde(default)]
167 pub key_format_version: Option<u16>,
168
169 pub created: i64,
170
171 pub discoverable: bool,
172
173 #[serde(default)]
175 pub backup_state: soft_fido2::CredentialBackupState,
176
177 #[serde(default)]
178 pub extensions: Extensions,
179}
180
181impl<'a> Credential<'a> {
182 #[inline]
184 pub fn from_soft_fido2(cred: &'a soft_fido2::Credential) -> Self {
185 let is_software = cred.key.provider.is_software();
186 Self {
187 id: std::borrow::Cow::Borrowed(&cred.id),
188 rp: RelyingParty::from_soft_fido2(&cred.rp),
189 user: User::from_soft_fido2(&cred.user),
190 sign_count: cred.sign_count,
191 alg: cred.alg,
192 private_key: cred.key.material.clone(),
193 key_provider: if is_software {
194 None
195 } else {
196 Some(cred.key.provider.as_bytes().to_vec())
197 },
198 key_format_version: if is_software {
199 None
200 } else {
201 Some(cred.key.format_version)
202 },
203 created: cred.created,
204 discoverable: cred.discoverable,
205 backup_state: cred.backup_state,
206 extensions: Extensions {
207 cred_protect: cred.extensions.cred_protect,
208 hmac_secret: cred.extensions.hmac_secret,
209 cred_random: cred.extensions.cred_random.as_ref().map(|cr| cr.to_vec()),
210 },
211 }
212 }
213
214 #[inline]
216 #[allow(dead_code)]
217 pub fn from_soft_fido2_ref(cred_ref: soft_fido2::CredentialRef<'a>) -> Self {
218 let is_software = cred_ref.key.provider.is_software();
219 Self {
220 id: std::borrow::Cow::Borrowed(cred_ref.id),
221 rp: RelyingParty {
222 id: std::borrow::Cow::Borrowed(cred_ref.rp_id),
223 name: cred_ref.rp_name.map(std::borrow::Cow::Borrowed),
224 },
225 user: User {
226 id: std::borrow::Cow::Borrowed(cred_ref.user_id),
227 name: cred_ref.user_name.map(std::borrow::Cow::Borrowed),
228 display_name: cred_ref.user_display_name.map(std::borrow::Cow::Borrowed),
229 },
230 sign_count: *cred_ref.sign_count,
231 alg: *cred_ref.alg,
232 private_key: cred_ref.key.material.clone(),
233 key_provider: if is_software {
234 None
235 } else {
236 Some(cred_ref.key.provider.as_bytes().to_vec())
237 },
238 key_format_version: if is_software {
239 None
240 } else {
241 Some(cred_ref.key.format_version)
242 },
243 created: *cred_ref.created,
244 discoverable: *cred_ref.discoverable,
245 backup_state: *cred_ref.backup_state,
246 extensions: Extensions {
247 cred_protect: cred_ref.cred_protect.copied(),
248 hmac_secret: None,
249 cred_random: cred_ref.cred_random.map(|cr| cr.to_vec()),
250 },
251 }
252 }
253
254 pub fn to_soft_fido2(&self) -> soft_fido2::Credential {
256 let key = match (&self.key_provider, self.key_format_version) {
257 (Some(provider_bytes), Some(version)) => soft_fido2_ctap::CredentialKey::new(
258 soft_fido2_ctap::CredentialKeyProviderId::new(provider_bytes),
259 version,
260 self.private_key.clone(),
261 ),
262 _ => soft_fido2_ctap::CredentialKey::software(self.private_key.clone()),
263 };
264
265 soft_fido2::Credential {
266 id: self.id.to_vec(),
267 rp: self.rp.to_soft_fido2(),
268 user: self.user.to_soft_fido2(),
269 sign_count: self.sign_count,
270 alg: self.alg,
271 key,
272 created: self.created,
273 discoverable: self.discoverable,
274 backup_state: self.backup_state,
275 extensions: soft_fido2::Extensions {
276 cred_protect: self.extensions.cred_protect,
277 hmac_secret: self.extensions.hmac_secret,
278 cred_random: self
279 .extensions
280 .cred_random
281 .as_ref()
282 .map(|cr| soft_fido2_ctap::SecBytes::from_slice(cr)),
283 },
284 }
285 }
286
287 #[allow(dead_code)]
289 pub fn into_owned(self) -> Credential<'static> {
290 Credential {
291 id: std::borrow::Cow::Owned(self.id.into_owned()),
292 rp: self.rp.into_owned(),
293 user: self.user.into_owned(),
294 sign_count: self.sign_count,
295 alg: self.alg,
296 private_key: self.private_key,
297 key_provider: self.key_provider,
298 key_format_version: self.key_format_version,
299 created: self.created,
300 discoverable: self.discoverable,
301 backup_state: self.backup_state,
302 extensions: self.extensions,
303 }
304 }
305
306 pub fn to_bytes(&self) -> Result<Vec<u8>, soft_fido2::Error> {
308 let mut buf = Vec::new();
309 soft_fido2_ctap::cbor::into_writer(self, &mut buf).map_err(|_| soft_fido2::Error::Other)?;
310 Ok(buf)
311 }
312
313 pub fn from_bytes(data: &[u8]) -> Result<Credential<'static>, soft_fido2::Error> {
317 mod flexible_backup_state {
318 use serde::de::{self, Visitor};
319
320 pub fn deserialize<'de, D>(
321 deserializer: D,
322 ) -> Result<soft_fido2::CredentialBackupState, D::Error>
323 where
324 D: de::Deserializer<'de>,
325 {
326 struct BackupStateVisitor;
327
328 impl<'de> Visitor<'de> for BackupStateVisitor {
329 type Value = soft_fido2::CredentialBackupState;
330
331 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
332 f.write_str("a boolean or camelCase string")
333 }
334
335 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
336 Ok(if v {
337 soft_fido2::CredentialBackupState::BackedUp
338 } else {
339 soft_fido2::CredentialBackupState::NotEligible
340 })
341 }
342
343 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
344 match v {
345 "notEligible" => Ok(soft_fido2::CredentialBackupState::NotEligible),
346 "eligible" => Ok(soft_fido2::CredentialBackupState::Eligible),
347 "backedUp" => Ok(soft_fido2::CredentialBackupState::BackedUp),
348 _ => Err(de::Error::unknown_variant(
349 v,
350 &["notEligible", "eligible", "backedUp"],
351 )),
352 }
353 }
354 }
355
356 deserializer.deserialize_any(BackupStateVisitor)
357 }
358 }
359
360 #[derive(serde::Deserialize)]
361 struct OwnedCredential {
362 #[serde(deserialize_with = "flexible_bytes::deserialize")]
363 id: Vec<u8>,
364 rp: OwnedRelyingParty,
365 user: OwnedUser,
366 sign_count: u32,
367 alg: i32,
368 #[serde(deserialize_with = "flexible_bytes::deserialize")]
369 private_key: Vec<u8>,
370 #[serde(default)]
371 key_provider: Option<Vec<u8>>,
372 #[serde(default)]
373 key_format_version: Option<u16>,
374 created: i64,
375 discoverable: bool,
376 #[serde(default, deserialize_with = "flexible_backup_state::deserialize")]
377 backup_state: soft_fido2::CredentialBackupState,
378 #[serde(default)]
379 extensions: Extensions,
380 }
381
382 #[derive(serde::Deserialize)]
383 struct OwnedRelyingParty {
384 id: String,
385 name: Option<String>,
386 }
387
388 #[derive(serde::Deserialize)]
389 struct OwnedUser {
390 #[serde(deserialize_with = "flexible_bytes::deserialize")]
391 id: Vec<u8>,
392 name: Option<String>,
393 display_name: Option<String>,
394 }
395
396 let owned: OwnedCredential =
397 soft_fido2_ctap::cbor::decode(data).map_err(|_| soft_fido2::Error::Other)?;
398
399 Ok(Credential {
400 id: std::borrow::Cow::Owned(owned.id),
401 rp: RelyingParty {
402 id: std::borrow::Cow::Owned(owned.rp.id),
403 name: owned.rp.name.map(std::borrow::Cow::Owned),
404 },
405 user: User {
406 id: std::borrow::Cow::Owned(owned.user.id),
407 name: owned.user.name.map(std::borrow::Cow::Owned),
408 display_name: owned.user.display_name.map(std::borrow::Cow::Owned),
409 },
410 sign_count: owned.sign_count,
411 alg: owned.alg,
412 private_key: SecBytes::new(owned.private_key),
413 key_provider: owned.key_provider,
414 key_format_version: owned.key_format_version,
415 created: owned.created,
416 discoverable: owned.discoverable,
417 backup_state: owned.backup_state,
418 extensions: owned.extensions,
419 })
420 }
421}
422
423mod flexible_bytes {
428 use serde::de::{Deserializer, SeqAccess, Visitor};
429
430 struct BytesVisitor;
431
432 impl<'de> Visitor<'de> for BytesVisitor {
433 type Value = Vec<u8>;
434
435 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
436 formatter.write_str("byte string or array of integers")
437 }
438
439 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
440 where
441 E: serde::de::Error,
442 {
443 Ok(v.to_vec())
444 }
445
446 fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
447 where
448 E: serde::de::Error,
449 {
450 Ok(v)
451 }
452
453 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
454 where
455 A: SeqAccess<'de>,
456 {
457 let mut bytes = Vec::new();
458 while let Some(value) = seq.next_element::<u8>()? {
459 bytes.push(value);
460 }
461 Ok(bytes)
462 }
463 }
464
465 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
466 where
467 D: Deserializer<'de>,
468 {
469 deserializer.deserialize_any(BytesVisitor)
470 }
471}
472
473mod sec_bytes_serde {
475 use soft_fido2_ctap::SecBytes;
476
477 use serde::{Deserializer, Serialize, Serializer};
478
479 pub fn serialize<S>(sec_bytes: &SecBytes, serializer: S) -> Result<S::Ok, S::Error>
480 where
481 S: Serializer,
482 {
483 let bytes: Vec<u8> = sec_bytes.as_slice().to_vec();
484 bytes.serialize(serializer)
485 }
486
487 pub fn deserialize<'de, D>(deserializer: D) -> Result<SecBytes, D::Error>
488 where
489 D: Deserializer<'de>,
490 {
491 let bytes = super::flexible_bytes::deserialize(deserializer)?;
492 Ok(SecBytes::new(bytes))
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_relying_party_zero_copy() {
502 let soft_rp = soft_fido2_ctap::types::RelyingParty {
503 id: "example.com".to_string(),
504 name: Some("Example".to_string()),
505 };
506
507 let our_rp = RelyingParty::from_soft_fido2(&soft_rp);
508
509 assert_eq!(our_rp.id, "example.com");
510 assert_eq!(our_rp.name.as_ref().map(|s| s.as_ref()), Some("Example"));
511
512 match our_rp.id {
514 std::borrow::Cow::Borrowed(s) => assert_eq!(s, soft_rp.id.as_str()),
515 std::borrow::Cow::Owned(_) => panic!("Expected borrowed data"),
516 }
517 }
518
519 #[test]
520 fn test_user_zero_copy() {
521 let soft_user = soft_fido2_ctap::types::User {
522 id: vec![1, 2, 3, 4],
523 name: Some("user@example.com".to_string()),
524 display_name: Some("User Name".to_string()),
525 };
526
527 let our_user = User::from_soft_fido2(&soft_user);
528
529 assert_eq!(our_user.id.as_ref(), &[1, 2, 3, 4]);
530 assert_eq!(
531 our_user.name.as_ref().map(|s| s.as_ref()),
532 Some("user@example.com")
533 );
534
535 match our_user.id {
537 std::borrow::Cow::Borrowed(s) => assert_eq!(s, soft_user.id.as_slice()),
538 std::borrow::Cow::Owned(_) => panic!("Expected borrowed data"),
539 }
540 }
541
542 #[test]
543 fn test_credential_serialization_roundtrip() {
544 let soft_cred = soft_fido2::Credential {
545 id: vec![1, 2, 3, 4],
546 rp: soft_fido2_ctap::types::RelyingParty {
547 id: "example.com".to_string(),
548 name: Some("Example".to_string()),
549 },
550 user: soft_fido2_ctap::types::User {
551 id: vec![5, 6, 7, 8],
552 name: Some("user@example.com".to_string()),
553 display_name: Some("User Name".to_string()),
554 },
555 sign_count: 42,
556 alg: -7,
557 key: soft_fido2_ctap::CredentialKey::software(SecBytes::new(vec![0u8; 32])),
558 created: 1234567890,
559 discoverable: true,
560 backup_state: soft_fido2::CredentialBackupState::NotEligible,
561 extensions: soft_fido2::Extensions {
562 cred_protect: Some(1),
563 hmac_secret: None,
564 cred_random: None,
565 },
566 };
567
568 let our_cred = Credential::from_soft_fido2(&soft_cred);
569
570 let our_bytes = our_cred.to_bytes().expect("Serialization should succeed");
571 let deserialized =
572 Credential::from_bytes(&our_bytes).expect("Should deserialize our format");
573
574 assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
576 assert_eq!(deserialized.rp.id, "example.com");
577 assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
578 assert_eq!(deserialized.sign_count, 42);
579 assert_eq!(deserialized.alg, -7);
580 assert_eq!(deserialized.created, 1234567890);
581 assert!(deserialized.discoverable);
582 assert_eq!(
583 deserialized.backup_state,
584 soft_fido2::CredentialBackupState::NotEligible
585 );
586 }
587
588 #[test]
589 fn test_minimal_credential_roundtrip() {
590 let our_cred = Credential {
591 id: std::borrow::Cow::Owned(vec![1, 2, 3, 4]),
592 rp: RelyingParty {
593 id: std::borrow::Cow::Owned("example.com".to_string()),
594 name: None,
595 },
596 user: User {
597 id: std::borrow::Cow::Owned(vec![5, 6, 7, 8]),
598 name: Some(std::borrow::Cow::Owned("user".to_string())),
599 display_name: None,
600 },
601 sign_count: 0,
602 alg: -7,
603 private_key: SecBytes::new(vec![0u8; 32]),
604 key_provider: None,
605 key_format_version: None,
606 created: 0,
607 discoverable: true,
608 backup_state: soft_fido2::CredentialBackupState::NotEligible,
609 extensions: Extensions::default(),
610 };
611
612 let our_bytes = our_cred.to_bytes().expect("Should serialize our format");
613 let deserialized =
614 Credential::from_bytes(&our_bytes).expect("Should deserialize our format");
615
616 assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
617 assert_eq!(deserialized.rp.id, "example.com");
618 assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
619 }
620
621 #[test]
622 fn test_serialization_roundtrip() {
623 let soft_cred = soft_fido2::Credential {
625 id: vec![1, 2, 3, 4],
626 rp: soft_fido2_ctap::types::RelyingParty {
627 id: "example.com".to_string(),
628 name: Some("Example".to_string()),
629 },
630 user: soft_fido2_ctap::types::User {
631 id: vec![5, 6, 7, 8],
632 name: Some("user@example.com".to_string()),
633 display_name: Some("User Name".to_string()),
634 },
635 sign_count: 42,
636 alg: -7,
637 key: soft_fido2_ctap::CredentialKey::software(SecBytes::new(vec![0u8; 32])),
638 created: 1234567890,
639 discoverable: true,
640 backup_state: soft_fido2::CredentialBackupState::NotEligible,
641 extensions: soft_fido2::Extensions {
642 cred_protect: Some(1),
643 hmac_secret: None,
644 cred_random: None,
645 },
646 };
647
648 let our_cred = Credential::from_soft_fido2(&soft_cred);
650 let our_bytes = our_cred
651 .to_bytes()
652 .expect("Should be able to serialize our credential format");
653
654 let deserialized =
656 Credential::from_bytes(&our_bytes).expect("Should deserialize our own format");
657
658 assert_eq!(deserialized.id.as_ref(), &[1, 2, 3, 4]);
659 assert_eq!(deserialized.rp.id, "example.com");
660 assert_eq!(deserialized.user.id.as_ref(), &[5, 6, 7, 8]);
661 assert_eq!(
662 deserialized.user.name.as_ref().map(|s| s.as_ref()),
663 Some("user@example.com")
664 );
665 assert_eq!(
666 deserialized.user.display_name.as_ref().map(|s| s.as_ref()),
667 Some("User Name")
668 );
669 assert_eq!(deserialized.sign_count, 42);
670 assert_eq!(deserialized.alg, -7);
671 assert_eq!(deserialized.created, 1234567890);
672 assert!(deserialized.discoverable);
673 }
674}