1use std::collections::BTreeSet;
15use std::fmt;
16
17use base64::Engine as _;
18use base64::engine::general_purpose::URL_SAFE_NO_PAD;
19use p256::ecdsa::signature::Verifier as _;
20use p256::ecdsa::{Signature, VerifyingKey};
21use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
22use sha2::{Digest, Sha256};
23
24use super::GrantError;
25use super::config::is_loopback_origin;
26use crate::eth::{self, Address, EthError};
27
28const MIN_AUTHENTICATOR_DATA: usize = 37;
30const FLAG_USER_PRESENT: u8 = 0x01;
32const CLIENT_DATA_TYPE_GET: &str = "webauthn.get";
34const MAX_RP_ID_LEN: usize = 253;
36pub const MAX_CLIENT_DATA_DEPTH: usize = 64;
40
41#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct RelyingParty {
49 id: String,
50 id_hash: [u8; 32],
51 origins: Vec<String>,
52}
53
54impl RelyingParty {
55 pub fn new<'a>(
69 id: &str,
70 origins: impl IntoIterator<Item = &'a str>,
71 ) -> Result<Self, GrantError> {
72 if !is_dns_name(id) {
73 return Err(GrantError::RelyingParty);
74 }
75 let mut list: Vec<String> = Vec::new();
76 for origin in origins {
77 let printable =
78 !origin.is_empty() && origin.bytes().all(|b| (0x21..=0x7E).contains(&b));
79 if !printable || list.iter().any(|o| o == origin) {
80 return Err(GrantError::RelyingParty);
81 }
82 list.push(origin.to_owned());
83 }
84 if list.is_empty() {
85 return Err(GrantError::RelyingParty);
86 }
87 Ok(Self {
88 id: id.to_owned(),
89 id_hash: Sha256::digest(id.as_bytes()).into(),
90 origins: list,
91 })
92 }
93
94 #[must_use]
96 pub fn id(&self) -> &str {
97 &self.id
98 }
99
100 #[must_use]
102 pub fn origins(&self) -> &[String] {
103 &self.origins
104 }
105
106 pub(crate) fn is_loopback(&self) -> bool {
109 self.id == "localhost"
110 || self.id.ends_with(".localhost")
111 || self.origins.iter().any(|o| is_loopback_origin(o))
112 }
113
114 pub(crate) fn allows(&self, origin: &str) -> bool {
116 self.origins.iter().any(|o| o == origin)
117 }
118}
119
120fn is_dns_name(id: &str) -> bool {
121 if id.is_empty() || id.len() > MAX_RP_ID_LEN {
122 return false;
123 }
124 let labels_ok = id.split('.').all(|label| {
125 !label.is_empty()
126 && label.len() <= 63
127 && !label.starts_with('-')
128 && !label.ends_with('-')
129 && label
130 .bytes()
131 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
132 });
133 let last_is_number = id
134 .rsplit('.')
135 .next()
136 .is_some_and(|l| l.bytes().all(|b| b.is_ascii_digit()));
137 labels_ok && !last_is_number
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct WebAuthnAssertion {
150 pub public_key: [u8; 64],
152 pub authenticator_data: Vec<u8>,
154 pub client_data_json: Vec<u8>,
156 pub signature: [u8; 64],
159}
160
161impl WebAuthnAssertion {
162 pub fn parse(blob: &[u8]) -> Result<Self, GrantError> {
169 let mut rest = blob;
170 let public_key = take_field(&mut rest)?;
171 let authenticator_data = take_field(&mut rest)?;
172 let client_data_json = take_field(&mut rest)?;
173 let signature = take_field(&mut rest)?;
174 if !rest.is_empty() {
175 return Err(GrantError::WebAuthnBlob);
176 }
177 Ok(Self {
178 public_key: public_key
179 .try_into()
180 .map_err(|_| GrantError::WebAuthnBlob)?,
181 authenticator_data: authenticator_data.to_vec(),
182 client_data_json: client_data_json.to_vec(),
183 signature: signature.try_into().map_err(|_| GrantError::WebAuthnBlob)?,
184 })
185 }
186
187 pub fn encode(&self) -> Result<Vec<u8>, GrantError> {
192 let fields: [&[u8]; 4] = [
193 &self.public_key,
194 &self.authenticator_data,
195 &self.client_data_json,
196 &self.signature,
197 ];
198 let mut out = Vec::with_capacity(16 + fields.iter().map(|f| f.len()).sum::<usize>());
199 for field in fields {
200 let len = u32::try_from(field.len()).map_err(|_| GrantError::WebAuthnBlob)?;
201 out.extend_from_slice(&len.to_le_bytes());
202 out.extend_from_slice(field);
203 }
204 Ok(out)
205 }
206}
207
208fn take_field<'a>(rest: &mut &'a [u8]) -> Result<&'a [u8], GrantError> {
209 let (len, tail) = rest
210 .split_first_chunk::<4>()
211 .ok_or(GrantError::WebAuthnBlob)?;
212 let len = usize::try_from(u32::from_le_bytes(*len)).map_err(|_| GrantError::WebAuthnBlob)?;
213 if tail.len() < len {
214 return Err(GrantError::WebAuthnBlob);
215 }
216 let (field, tail) = tail.split_at(len);
217 *rest = tail;
218 Ok(field)
219}
220
221#[must_use]
225pub fn webauthn_challenge(statement: &[u8]) -> String {
226 URL_SAFE_NO_PAD.encode(mkit_core::hash::hash(statement))
227}
228
229#[derive(Clone, Debug, PartialEq, Eq)]
233enum Member {
234 Str(String),
236 False,
238 Other,
240}
241
242#[derive(Debug, Default)]
244struct ClientData {
245 ty: Option<Member>,
246 challenge: Option<Member>,
247 origin: Option<Member>,
248 cross_origin: Option<Member>,
249 top_origin: bool,
250}
251
252#[derive(Clone, Copy)]
256struct Walk {
257 depth: usize,
258}
259
260impl Walk {
261 fn enter<E: de::Error>(self) -> Result<Self, E> {
262 if self.depth > MAX_CLIENT_DATA_DEPTH {
263 return Err(E::custom("nesting too deep"));
264 }
265 Ok(Self {
266 depth: self.depth + 1,
267 })
268 }
269}
270
271impl<'de> DeserializeSeed<'de> for Walk {
272 type Value = Member;
273
274 fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<Member, D::Error> {
275 d.deserialize_any(self)
276 }
277}
278
279impl<'de> Visitor<'de> for Walk {
280 type Value = Member;
281
282 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 f.write_str("a JSON value")
284 }
285
286 fn visit_bool<E>(self, v: bool) -> Result<Member, E> {
287 Ok(if v { Member::Other } else { Member::False })
288 }
289
290 fn visit_i64<E>(self, _: i64) -> Result<Member, E> {
291 Ok(Member::Other)
292 }
293
294 fn visit_u64<E>(self, _: u64) -> Result<Member, E> {
295 Ok(Member::Other)
296 }
297
298 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Member, E> {
299 if v.is_finite() {
302 Ok(Member::Other)
303 } else {
304 Err(E::custom("non-finite number"))
305 }
306 }
307
308 fn visit_str<E>(self, v: &str) -> Result<Member, E> {
309 Ok(Member::Str(v.to_owned()))
310 }
311
312 fn visit_unit<E>(self) -> Result<Member, E> {
313 Ok(Member::Other)
314 }
315
316 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Member, A::Error> {
317 let inner = self.enter()?;
318 while seq.next_element_seed(inner)?.is_some() {}
319 Ok(Member::Other)
320 }
321
322 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Member, A::Error> {
323 walk_object(&mut map, self.enter()?, |_, _| {})?;
324 Ok(Member::Other)
325 }
326}
327
328fn walk_object<'de, A: MapAccess<'de>>(
331 map: &mut A,
332 inner: Walk,
333 mut each: impl FnMut(&str, Member),
334) -> Result<(), A::Error> {
335 let mut seen = BTreeSet::new();
336 while let Some(name) = map.next_key::<String>()? {
337 let value = map.next_value_seed(inner)?;
338 each(&name, value);
339 if !seen.insert(name) {
340 return Err(de::Error::custom("duplicate member name"));
341 }
342 }
343 Ok(())
344}
345
346struct TopLevel;
348
349impl<'de> DeserializeSeed<'de> for TopLevel {
350 type Value = ClientData;
351
352 fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<ClientData, D::Error> {
353 d.deserialize_any(TopLevel)
354 }
355}
356
357impl<'de> Visitor<'de> for TopLevel {
358 type Value = ClientData;
359
360 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 f.write_str("a JSON object")
362 }
363
364 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<ClientData, A::Error> {
365 let mut out = ClientData::default();
366 let inner = Walk { depth: 1 }.enter()?;
367 walk_object(&mut map, inner, |name, value| match name {
368 "type" => out.ty = Some(value),
369 "challenge" => out.challenge = Some(value),
370 "origin" => out.origin = Some(value),
371 "crossOrigin" => out.cross_origin = Some(value),
372 "topOrigin" => out.top_origin = true,
373 _ => {}
374 })?;
375 Ok(out)
376 }
377}
378
379fn parse_client_data(bytes: &[u8]) -> Result<ClientData, GrantError> {
386 let mut de = serde_json::Deserializer::from_slice(bytes);
387 let data = TopLevel
388 .deserialize(&mut de)
389 .map_err(|_| GrantError::ClientData)?;
390 de.end().map_err(|_| GrantError::ClientData)?;
391 Ok(data)
392}
393
394#[derive(Clone, Debug, PartialEq, Eq)]
398pub(crate) struct WebAuthnBinding {
399 pub(crate) rp_id: String,
400 pub(crate) origin: String,
401}
402
403pub(crate) fn verify_webauthn(
417 relying_parties: &[RelyingParty],
418 statement: &[u8],
419 blob: &[u8],
420 namespace: &Address,
421) -> Result<WebAuthnBinding, GrantError> {
422 let a = WebAuthnAssertion::parse(blob)?;
423 eth::p256_check_raw_low_s(&a.signature).map_err(|e| match e {
424 EthError::HighS => GrantError::HighS,
425 _ => GrantError::SignatureScalar,
426 })?;
427 let address = eth::address_p256(&a.public_key).map_err(|_| GrantError::InvalidOwnerKey)?;
428 if address != *namespace {
429 return Err(GrantError::OwnerMismatch);
430 }
431
432 let auth = &a.authenticator_data;
433 if auth.len() < MIN_AUTHENTICATOR_DATA {
434 return Err(GrantError::AuthenticatorData);
435 }
436 if auth[32] & FLAG_USER_PRESENT == 0 {
437 return Err(GrantError::UserNotPresent);
438 }
439 let rp = relying_parties
440 .iter()
441 .find(|rp| rp.id_hash[..] == auth[..32])
442 .ok_or(GrantError::RelyingPartyMismatch)?;
443
444 let data = parse_client_data(&a.client_data_json)?;
445 if data.ty != Some(Member::Str(CLIENT_DATA_TYPE_GET.to_owned())) {
446 return Err(GrantError::ClientDataType);
447 }
448 if data.challenge != Some(Member::Str(webauthn_challenge(statement))) {
449 return Err(GrantError::Challenge);
450 }
451 if !matches!(data.cross_origin, None | Some(Member::False)) {
452 return Err(GrantError::CrossOrigin);
453 }
454 if data.top_origin {
455 return Err(GrantError::TopOrigin);
456 }
457 let origin = match data.origin {
458 Some(Member::Str(o)) if rp.allows(&o) => o,
459 _ => return Err(GrantError::OriginNotAllowed),
460 };
461
462 let mut sec1 = [0u8; 65];
463 sec1[0] = 0x04;
464 sec1[1..].copy_from_slice(&a.public_key);
465 let key = VerifyingKey::from_sec1_bytes(&sec1).map_err(|_| GrantError::InvalidOwnerKey)?;
466 let signature = Signature::from_slice(&a.signature).map_err(|_| GrantError::SignatureScalar)?;
467 let mut signed = Vec::with_capacity(auth.len() + 32);
468 signed.extend_from_slice(auth);
469 signed.extend_from_slice(&Sha256::digest(&a.client_data_json));
470 key.verify(&signed, &signature)
471 .map_err(|_| GrantError::BadSignature)?;
472 Ok(WebAuthnBinding {
473 rp_id: rp.id.clone(),
474 origin,
475 })
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn webauthn_relying_party_rules() {
484 let rp = RelyingParty::new("example.com", ["https://example.com"]).unwrap();
485 assert_eq!(rp.id(), "example.com");
486 assert_eq!(rp.origins(), ["https://example.com"]);
487 assert_eq!(rp.id_hash, <[u8; 32]>::from(Sha256::digest(b"example.com")));
488 assert!(
489 RelyingParty::new("a-1.b2.example", ["android:apk-key-hash:x", "https://a"]).is_ok()
490 );
491 assert!(RelyingParty::new("localhost", ["http://localhost:8080"]).is_ok());
492 let long_ok = format!(
493 "{}.{}.{}.{}",
494 "a".repeat(63),
495 "b".repeat(63),
496 "c".repeat(63),
497 "d".repeat(61)
498 );
499 assert_eq!(long_ok.len(), 253);
500 assert!(RelyingParty::new(&long_ok, ["https://x"]).is_ok());
501 for bad in [
502 "",
503 "Example.com",
504 "example..com",
505 ".example.com",
506 "example.com.",
507 "-a.example",
508 "a-.example",
509 "ex_ample.com",
510 "127.0.0.1",
511 "example.123",
512 &format!("{long_ok}x"),
513 &format!("{}.com", "a".repeat(64)),
514 ] {
515 assert_eq!(
516 RelyingParty::new(bad, ["https://example.com"]),
517 Err(GrantError::RelyingParty),
518 "{bad}"
519 );
520 }
521 for origins in [
522 &[][..],
523 &[""][..],
524 &["https://a b"][..],
525 &["https://é"][..],
526 &["https://a", "https://a"][..],
527 ] {
528 assert_eq!(
529 RelyingParty::new("example.com", origins.iter().copied()),
530 Err(GrantError::RelyingParty),
531 "{origins:?}"
532 );
533 }
534 }
535
536 #[test]
537 fn webauthn_relying_party_loopback() {
538 for (id, origin, loopback) in [
539 ("localhost", "https://example.com", true),
540 ("dev.localhost", "https://example.com", true),
541 ("example.com", "http://127.0.0.1:8080", true),
542 ("example.com", "http://[::1]", true),
543 ("example.com", "https://example.com", false),
544 ("localhost.example", "https://localhost.example", false),
545 ] {
546 assert_eq!(
547 RelyingParty::new(id, [origin]).unwrap().is_loopback(),
548 loopback,
549 "{id} {origin}"
550 );
551 }
552 }
553
554 const MAX_ROUNDING_INT: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791";
556 const TIE_INT: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792";
558
559 #[test]
560 fn webauthn_client_data_strict_json() {
561 let ok = |s: &str| parse_client_data(s.as_bytes());
562 let d = ok(r#" {"type":"webauthn.get","challenge":"c","origin":"o","crossOrigin":false,"x":[1,{"a":null}]} "#)
563 .unwrap();
564 assert_eq!(d.ty, Some(Member::Str("webauthn.get".into())));
565 assert_eq!(d.challenge, Some(Member::Str("c".into())));
566 assert_eq!(d.origin, Some(Member::Str("o".into())));
567 assert_eq!(d.cross_origin, Some(Member::False));
568 assert!(!d.top_origin);
569 let d = ok(r#"{"type":"webauthn.get","topOrigin":1}"#).unwrap();
571 assert_eq!(d.ty, Some(Member::Str("webauthn.get".into())));
572 assert!(d.top_origin);
573 assert_eq!(
574 ok(r#"{"crossOrigin":"false"}"#).unwrap().cross_origin,
575 Some(Member::Str("false".into()))
576 );
577 assert_eq!(
578 ok(r#"{"crossOrigin":true}"#).unwrap().cross_origin,
579 Some(Member::Other)
580 );
581 for bad in [
582 r#"{"type":"a","type":"a"}"#,
583 r#"{"type":"a","type":"a"}"#,
584 r#"{"x":{"a":1,"a":1}}"#,
585 r#"{"x":[{"a":1},{"b":[{"c":1,"c":2}]}]}"#,
586 r#"{"x":"\ud800"}"#,
587 r#"{"\udc00":1}"#,
588 r#"{"x":1,}"#,
589 r#"{"x":NaN}"#,
590 r#"{"x":1} x"#,
591 r#"{"x":1}{}"#,
592 "{\"x\":\"a\u{1}\"}",
593 "\u{feff}{}",
594 r#"["type"]"#,
595 r#""webauthn.get""#,
596 "",
597 "{",
598 ] {
599 assert_eq!(
600 parse_client_data(bad.as_bytes()).unwrap_err(),
601 GrantError::ClientData,
602 "{bad}"
603 );
604 }
605 assert_eq!(
606 parse_client_data(b"{\"x\":\"\xff\"}").unwrap_err(),
607 GrantError::ClientData
608 );
609 let nest = |depth: usize, open: &str, close: &str| {
612 format!(
613 "{{\"x\":{}1{}}}",
614 open.repeat(depth - 1),
615 close.repeat(depth - 1)
616 )
617 };
618 assert!(parse_client_data(nest(MAX_CLIENT_DATA_DEPTH, "[", "]").as_bytes()).is_ok());
619 assert!(parse_client_data(nest(MAX_CLIENT_DATA_DEPTH, "{\"a\":", "}").as_bytes()).is_ok());
620 assert_eq!(
621 parse_client_data(nest(MAX_CLIENT_DATA_DEPTH + 1, "[", "]").as_bytes()).unwrap_err(),
622 GrantError::ClientData
623 );
624 assert_eq!(
625 parse_client_data(nest(MAX_CLIENT_DATA_DEPTH + 1, "{\"a\":", "}").as_bytes())
626 .unwrap_err(),
627 GrantError::ClientData
628 );
629 for good in [
631 "1e308",
632 "-1.7976931348623157e308",
633 "1.7976931348623158e308",
635 "-1.7976931348623158e308",
636 MAX_ROUNDING_INT,
637 "1e-400",
638 "123456789012345678901234567890",
639 ] {
640 assert!(
641 parse_client_data(format!("{{\"x\":{good}}}").as_bytes()).is_ok(),
642 "{good}"
643 );
644 }
645 for bad in [
646 "1e400",
647 "-1e400",
648 &format!("1{}", "0".repeat(400)),
649 "1.7976931348623159e308",
651 "-1.7976931348623159e308",
652 TIE_INT,
653 ] {
654 assert_eq!(
655 parse_client_data(format!("{{\"x\":{bad}}}").as_bytes()).unwrap_err(),
656 GrantError::ClientData,
657 "{bad}"
658 );
659 }
660 let deep = format!("{}{{\"a\":1,\"a\":2}}{}", "[".repeat(50), "]".repeat(50));
662 assert!(parse_client_data(format!("{{\"x\":{deep}}}").as_bytes()).is_err());
663 }
664
665 #[test]
666 fn webauthn_blob_framing() {
667 let a = WebAuthnAssertion {
668 public_key: [1; 64],
669 authenticator_data: vec![2; 37],
670 client_data_json: b"{}".to_vec(),
671 signature: [3; 64],
672 };
673 let blob = a.encode().unwrap();
674 assert_eq!(blob.len(), 16 + 64 + 37 + 2 + 64);
675 assert_eq!(&blob[..4], &64u32.to_le_bytes());
676 assert_eq!(WebAuthnAssertion::parse(&blob).unwrap(), a);
677 let mut trailing = blob.clone();
678 trailing.push(0);
679 assert_eq!(
680 WebAuthnAssertion::parse(&trailing),
681 Err(GrantError::WebAuthnBlob)
682 );
683 for cut in 0..blob.len() {
684 assert_eq!(
685 WebAuthnAssertion::parse(&blob[..cut]),
686 Err(GrantError::WebAuthnBlob),
687 "{cut}"
688 );
689 }
690 let mut huge = blob.clone();
691 huge[68..72].copy_from_slice(&u32::MAX.to_le_bytes());
692 assert_eq!(
693 WebAuthnAssertion::parse(&huge),
694 Err(GrantError::WebAuthnBlob)
695 );
696 }
697
698 #[test]
699 fn webauthn_challenge_is_43_base64url_chars() {
700 let c = webauthn_challenge(b"statement");
701 assert_eq!(c.len(), 43);
702 assert_eq!(
703 URL_SAFE_NO_PAD.decode(&c).unwrap(),
704 blake3::hash(b"statement").as_bytes()
705 );
706 }
707
708 fn cases(default: u32) -> u32 {
711 std::env::var("PROPTEST_CASES")
712 .ok()
713 .and_then(|v| v.parse().ok())
714 .unwrap_or(default)
715 }
716
717 fn json_value() -> impl proptest::strategy::Strategy<Value = serde_json::Value> {
718 use proptest::prelude::*;
719 use serde_json::Value;
720 let leaf = prop_oneof![
721 Just(Value::Null),
722 any::<bool>().prop_map(Value::Bool),
723 any::<i64>().prop_map(Value::from),
724 (-1e300f64..1e300).prop_map(Value::from),
725 "\\PC{0,8}".prop_map(Value::String),
726 ];
727 leaf.prop_recursive(4, 48, 6, |inner| {
728 prop_oneof![
729 prop::collection::vec(inner.clone(), 0..5).prop_map(Value::Array),
730 prop::collection::btree_map("[a-z\\u{e9}\"\\\\]{0,4}", inner, 0..5)
731 .prop_map(|m| Value::Object(m.into_iter().collect())),
732 ]
733 })
734 }
735
736 proptest::proptest! {
737 #![proptest_config(proptest::prelude::ProptestConfig::with_cases(cases(256)))]
738
739 #[test]
744 fn webauthn_client_data_matches_serde_json(
745 members in proptest::collection::btree_map("[a-z]{0,3}", json_value(), 1..5),
746 pos in proptest::prelude::any::<usize>(),
747 byte in proptest::prelude::any::<u8>(),
748 ) {
749 let object = serde_json::Value::Object(members.clone().into_iter().collect());
750 let text = serde_json::to_vec(&object).unwrap();
751 proptest::prop_assert!(parse_client_data(&text).is_ok());
752 let (name, value) = members.iter().next().unwrap();
753 let member = format!("{}:{}", serde_json::to_string(name).unwrap(), value);
754 let duplicated = format!("{{{member},{}", &String::from_utf8(text.clone()).unwrap()[1..]);
755 proptest::prop_assert_eq!(
756 parse_client_data(duplicated.as_bytes()).unwrap_err(),
757 GrantError::ClientData
758 );
759 let mut mutated = text;
760 let i = pos % mutated.len();
761 mutated[i] = byte;
762 if parse_client_data(&mutated).is_ok() {
763 proptest::prop_assert!(serde_json::from_slice::<serde_json::Value>(&mutated).is_ok());
764 }
765 }
766 }
767}