1use crate::error::WebauthnError;
16
17#[derive(Debug, Clone)]
19pub enum CosePublicKey {
20 Ec2 {
24 x: Vec<u8>,
26 y: Vec<u8>,
28 },
29 Rsa {
31 n: Vec<u8>,
33 e: Vec<u8>,
35 },
36 Okp {
38 id: Vec<u8>,
40 },
41}
42
43pub const COSE_KTY_OKP: i64 = 1;
45pub const COSE_KTY_EC2: i64 = 2;
47pub const COSE_KTY_RSA: i64 = 3;
49
50pub const COSE_ALG_ES256: i32 = -7;
52pub const COSE_ALG_EDDSA: i32 = -8;
54pub const COSE_ALG_ES384: i32 = -35;
56pub const COSE_ALG_RS256: i32 = -257;
58
59const COSE_KEY_KTY: i64 = 1;
61const COSE_KEY_ALG: i64 = 3;
63const COSE_KEY_CRV_N: i64 = -1;
65const COSE_KEY_X_E: i64 = -2;
67const COSE_KEY_Y: i64 = -3;
69
70const COSE_CRV_P256: i64 = 1;
72pub const COSE_CRV_P384: i64 = 2;
74pub const COSE_CRV_ED25519: i64 = 6;
76
77fn cbor_i64(val: &ciborium::Value) -> Option<i64> {
79 use ciborium::Value;
80 match val {
81 Value::Integer(i) => (*i).try_into().ok(),
82 _ => None,
83 }
84}
85
86pub fn cbor_bytes(val: &ciborium::Value) -> Option<Vec<u8>> {
95 use ciborium::Value;
96 match val {
97 Value::Bytes(b) => Some(b.clone()),
98 Value::Text(t) => Some(t.as_bytes().to_vec()),
99 _ => None,
100 }
101}
102
103pub fn cbor_map_entries(val: &ciborium::Value) -> Option<Vec<(i64, ciborium::Value)>> {
109 use ciborium::Value;
110 match val {
111 Value::Map(entries) => {
112 let mut result = Vec::with_capacity(entries.len());
113 let mut seen: Vec<i64> = Vec::with_capacity(entries.len());
119 for (k, v) in entries {
120 let key = cbor_i64(k)?;
121 if seen.contains(&key) {
122 return None;
123 }
124 seen.push(key);
125 result.push((key, v.clone()));
126 }
127 Some(result)
128 }
129 _ => None,
130 }
131}
132
133pub fn parse_cose_key(cose_bytes: &[u8]) -> Result<(i32, CosePublicKey), WebauthnError> {
160 use ciborium::Value;
161
162 let key_val: Value = ciborium::de::from_reader(cose_bytes).map_err(|e| {
163 WebauthnError::VerificationFailed(format!("COSE key CBOR parse error: {e}"))
164 })?;
165
166 let entries = cbor_map_entries(&key_val).ok_or_else(|| {
167 WebauthnError::VerificationFailed("COSE key is not a CBOR map".to_string())
168 })?;
169
170 let mut kty: Option<i64> = None;
171 let mut alg: Option<i32> = None;
172 let mut crv: Option<i64> = None;
173 let mut x: Option<Vec<u8>> = None;
174 let mut y: Option<Vec<u8>> = None;
175 let mut n: Option<Vec<u8>> = None;
176 let mut e: Option<Vec<u8>> = None;
177
178 for (label, val) in &entries {
179 match *label {
180 COSE_KEY_KTY => kty = cbor_i64(val),
181 COSE_KEY_ALG => alg = cbor_i64(val).map(|v| v as i32),
182 COSE_KEY_CRV_N => match kty {
183 Some(COSE_KTY_RSA) => n = cbor_bytes(val),
184 _ => crv = cbor_i64(val),
185 },
186 COSE_KEY_X_E => match kty {
187 Some(COSE_KTY_RSA) => e = cbor_bytes(val),
188 _ => x = cbor_bytes(val),
189 },
190 COSE_KEY_Y => y = cbor_bytes(val),
191 _ => {}
192 }
193 }
194
195 let kty =
196 kty.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'kty'".to_string()))?;
197 let alg =
198 alg.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'alg'".to_string()))?;
199
200 match kty {
201 COSE_KTY_EC2 => {
202 let crv = crv.ok_or_else(|| {
203 WebauthnError::VerificationFailed("EC2 key missing 'crv'".to_string())
204 })?;
205 let curve_ok = matches!(
209 (crv, alg),
210 (COSE_CRV_P256, COSE_ALG_ES256) | (COSE_CRV_P384, COSE_ALG_ES384)
211 );
212 if !curve_ok {
213 return Err(WebauthnError::UnsupportedAlgorithm(alg));
214 }
215 let x = x.ok_or_else(|| {
216 WebauthnError::VerificationFailed("EC2 key missing 'x'".to_string())
217 })?;
218 let y = y.ok_or_else(|| {
219 WebauthnError::VerificationFailed("EC2 key missing 'y'".to_string())
220 })?;
221 Ok((alg, CosePublicKey::Ec2 { x, y }))
222 }
223 COSE_KTY_RSA => {
224 let n = n.ok_or_else(|| {
225 WebauthnError::VerificationFailed("RSA key missing 'n'".to_string())
226 })?;
227 let e = e.ok_or_else(|| {
228 WebauthnError::VerificationFailed("RSA key missing 'e'".to_string())
229 })?;
230 Ok((alg, CosePublicKey::Rsa { n, e }))
231 }
232 COSE_KTY_OKP => {
233 if crv != Some(COSE_CRV_ED25519) || alg != COSE_ALG_EDDSA {
235 return Err(WebauthnError::UnsupportedAlgorithm(alg));
236 }
237 let id = x.ok_or_else(|| {
238 WebauthnError::VerificationFailed("OKP key missing public key bytes".to_string())
239 })?;
240 Ok((alg, CosePublicKey::Okp { id }))
241 }
242 other => Err(WebauthnError::VerificationFailed(format!(
243 "Unsupported COSE key type: {other}"
244 ))),
245 }
246}
247
248pub fn verify_cose_signature(
268 alg: i32,
269 public_key: &CosePublicKey,
270 signed_data: &[u8],
271 signature: &[u8],
272) -> Result<(), WebauthnError> {
273 use ring::signature;
274
275 match alg {
276 COSE_ALG_ES256 => {
277 let CosePublicKey::Ec2 { x, y } = public_key else {
278 return Err(WebauthnError::VerificationFailed(
279 "EC2 key expected for ES256".to_string(),
280 ));
281 };
282
283 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
284 public_key_bytes.push(0x04);
285 public_key_bytes.extend_from_slice(x);
286 public_key_bytes.extend_from_slice(y);
287
288 let public_key = signature::UnparsedPublicKey::new(
289 &signature::ECDSA_P256_SHA256_FIXED,
290 &public_key_bytes,
291 );
292 public_key
293 .verify(signed_data, signature)
294 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
295 Ok(())
296 }
297 COSE_ALG_ES384 => {
298 let CosePublicKey::Ec2 { x, y } = public_key else {
299 return Err(WebauthnError::VerificationFailed(
300 "EC2 key expected for ES384".to_string(),
301 ));
302 };
303
304 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
305 public_key_bytes.push(0x04);
306 public_key_bytes.extend_from_slice(x);
307 public_key_bytes.extend_from_slice(y);
308
309 let public_key = signature::UnparsedPublicKey::new(
310 &signature::ECDSA_P384_SHA384_FIXED,
311 &public_key_bytes,
312 );
313 public_key
314 .verify(signed_data, signature)
315 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
316 Ok(())
317 }
318 COSE_ALG_EDDSA => {
319 let CosePublicKey::Okp { id } = public_key else {
320 return Err(WebauthnError::VerificationFailed(
321 "OKP key expected for EdDSA".to_string(),
322 ));
323 };
324
325 let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, id);
326 public_key
327 .verify(signed_data, signature)
328 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
329 Ok(())
330 }
331 COSE_ALG_RS256 => {
332 let CosePublicKey::Rsa { n, e } = public_key else {
333 return Err(WebauthnError::VerificationFailed(
334 "RSA key expected for RS256".to_string(),
335 ));
336 };
337
338 let rsa_public_key = RsaPublicKeyDer { n, e };
339 let der_bytes = rsa_public_key.to_der()?;
340
341 let public_key = signature::UnparsedPublicKey::new(
342 &signature::RSA_PKCS1_2048_8192_SHA256,
343 &der_bytes,
344 );
345 public_key
346 .verify(signed_data, signature)
347 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
348 Ok(())
349 }
350 other => Err(WebauthnError::UnsupportedAlgorithm(other)),
351 }
352}
353
354struct RsaPublicKeyDer<'a> {
357 n: &'a [u8],
358 e: &'a [u8],
359}
360
361impl RsaPublicKeyDer<'_> {
362 fn to_der(&self) -> Result<Vec<u8>, WebauthnError> {
369 let mut der = Vec::new();
370 Self::encode_integer(&mut der, self.n);
371 Self::encode_integer(&mut der, self.e);
372 Self::encode_sequence_in_place(&mut der);
373 Ok(der)
374 }
375
376 fn encode_integer(buf: &mut Vec<u8>, value: &[u8]) {
382 let start = value
383 .iter()
384 .position(|&b| b != 0)
385 .unwrap_or(value.len().saturating_sub(1));
386 let v = value.get(start..).unwrap_or(&[]);
387 if v.first().is_some_and(|&b| b & 0x80 != 0) {
388 buf.push(0x02);
389 Self::encode_length(buf, v.len() + 1);
390 buf.push(0x00);
391 buf.extend_from_slice(v);
392 } else {
393 buf.push(0x02);
394 Self::encode_length(buf, v.len());
395 buf.extend_from_slice(v);
396 }
397 }
398
399 fn encode_sequence_in_place(buf: &mut Vec<u8>) {
401 let content_len = buf.len();
402 let header_len = if content_len < 0x80 {
403 2
404 } else if content_len < 0x100 {
405 3
406 } else {
407 4
408 };
409 buf.resize(content_len + header_len, 0);
410 buf.copy_within(..content_len, header_len);
411 if let Some(tag) = buf.first_mut() {
412 *tag = 0x30;
413 }
414 Self::encode_length_at(buf, 1, content_len);
415 }
416
417 fn encode_length(buf: &mut Vec<u8>, len: usize) {
419 if len < 0x80 {
420 buf.push(len as u8);
421 } else if len < 0x100 {
422 buf.push(0x81);
423 buf.push(len as u8);
424 } else {
425 buf.push(0x82);
426 buf.push((len >> 8) as u8);
427 buf.push((len & 0xFF) as u8);
428 }
429 }
430
431 #[allow(clippy::indexing_slicing)]
438 fn encode_length_at(buf: &mut [u8], offset: usize, len: usize) {
439 if len < 0x80 {
440 buf[offset] = len as u8;
441 } else if len < 0x100 {
442 buf[offset] = 0x81;
443 buf[offset + 1] = len as u8;
444 } else {
445 buf[offset] = 0x82;
446 buf[offset + 1] = (len >> 8) as u8;
447 buf[offset + 2] = (len & 0xFF) as u8;
448 }
449 }
450}
451
452pub fn alg_to_name(alg: i32) -> &'static str {
454 match alg {
455 COSE_ALG_ES256 => "ES256",
456 COSE_ALG_ES384 => "ES384",
457 COSE_ALG_EDDSA => "EdDSA",
458 COSE_ALG_RS256 => "RS256",
459 _ => "unknown",
460 }
461}
462
463pub fn generate_challenge_bytes() -> Vec<u8> {
476 use ring::rand::SecureRandom;
477 let mut bytes = [0u8; 32];
478 #[allow(clippy::expect_used)]
481 ring::rand::SystemRandom::new()
482 .fill(&mut bytes)
483 .expect("OS CSPRNG unavailable; cannot generate secure challenge");
484 bytes.to_vec()
485}
486
487pub fn base64_encode_urlsafe(data: &[u8]) -> String {
490 use base64::Engine;
491 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
492}
493
494pub fn base64_decode_urlsafe(data: &str) -> Result<Vec<u8>, WebauthnError> {
502 use base64::Engine;
503 base64::engine::general_purpose::URL_SAFE_NO_PAD
504 .decode(data)
505 .map_err(|e| WebauthnError::VerificationFailed(format!("base64 decode error: {e}")))
506}
507
508#[allow(
512 clippy::unwrap_used,
513 clippy::expect_used,
514 clippy::indexing_slicing,
515 clippy::panic
516)]
517#[cfg(test)]
518pub(crate) mod tests {
519 use super::*;
520
521 pub(crate) fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
523 build_cose_ec2_key_alg(COSE_CRV_P256, COSE_ALG_ES256, x, y)
524 }
525
526 pub(crate) fn build_cose_ec2_key_alg(crv: i64, alg: i32, x: &[u8], y: &[u8]) -> Vec<u8> {
528 use ciborium::Value;
529 let map = vec![
530 (Value::Integer(1.into()), Value::Integer(2.into())),
531 (Value::Integer(3.into()), Value::Integer(alg.into())),
532 (Value::Integer((-1).into()), Value::Integer(crv.into())),
533 (Value::Integer((-2).into()), Value::Bytes(x.to_vec())),
534 (Value::Integer((-3).into()), Value::Bytes(y.to_vec())),
535 ];
536 let mut buf = Vec::new();
537 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
538 buf
539 }
540
541 pub(crate) fn build_cose_okp_key(id: &[u8]) -> Vec<u8> {
543 use ciborium::Value;
544 let map = vec![
545 (Value::Integer(1.into()), Value::Integer(1.into())),
546 (Value::Integer(3.into()), Value::Integer((-8).into())),
547 (Value::Integer((-1).into()), Value::Integer(6.into())),
548 (Value::Integer((-2).into()), Value::Bytes(id.to_vec())),
549 ];
550 let mut buf = Vec::new();
551 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
552 buf
553 }
554
555 pub(crate) fn build_cose_rsa_key(n: &[u8], e: &[u8]) -> Vec<u8> {
557 use ciborium::Value;
558 let map = vec![
559 (Value::Integer(1.into()), Value::Integer(3.into())),
560 (Value::Integer(3.into()), Value::Integer((-257).into())),
561 (Value::Integer((-1).into()), Value::Bytes(n.to_vec())),
562 (Value::Integer((-2).into()), Value::Bytes(e.to_vec())),
563 ];
564 let mut buf = Vec::new();
565 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
566 buf
567 }
568
569 #[test]
570 fn test_parse_cose_ec2_key() {
571 let x = vec![0xAA; 32];
572 let y = vec![0xBB; 32];
573 let cose_key = build_cose_ec2_key(&x, &y);
574
575 let (alg, key) = parse_cose_key(&cose_key).unwrap();
576 assert_eq!(alg, -7);
577 match key {
578 CosePublicKey::Ec2 { x: kx, y: ky } => {
579 assert_eq!(kx, x);
580 assert_eq!(ky, y);
581 }
582 _ => panic!("Expected EC2 key"),
583 }
584 }
585
586 #[test]
587 fn test_parse_cose_rsa_key() {
588 let n = vec![0xAA; 256];
589 let e = vec![0x01, 0x00, 0x01];
590 let cose_key = build_cose_rsa_key(&n, &e);
591
592 let (alg, key) = parse_cose_key(&cose_key).unwrap();
593 assert_eq!(alg, -257);
594 match key {
595 CosePublicKey::Rsa { n: kn, e: ke } => {
596 assert_eq!(kn, n);
597 assert_eq!(ke, e);
598 }
599 _ => panic!("Expected RSA key"),
600 }
601 }
602
603 #[test]
604 fn test_parse_cose_key_not_a_map() {
605 let buf = [0x00u8]; let result = parse_cose_key(&buf);
607 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
608 }
609
610 #[test]
611 fn test_parse_cose_key_missing_kty() {
612 use ciborium::Value;
613 let map = vec![(Value::Integer(3.into()), Value::Integer((-7).into()))];
614 let mut buf = Vec::new();
615 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
616 let result = parse_cose_key(&buf);
617 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
618 }
619
620 #[test]
621 fn test_parse_cose_key_okp_unsupported() {
622 use ciborium::Value;
623 let map = vec![
624 (Value::Integer(1.into()), Value::Integer(1.into())),
625 (Value::Integer(3.into()), Value::Integer((-8).into())),
626 ];
627 let mut buf = Vec::new();
628 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
629 let result = parse_cose_key(&buf);
630 assert!(matches!(
631 result,
632 Err(WebauthnError::UnsupportedAlgorithm(-8))
633 ));
634 }
635
636 #[test]
637 fn test_verify_cose_signature_es256() {
638 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P256_SHA256_FIXED_SIGNING};
639
640 let rng = ring::rand::SystemRandom::new();
641 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
642 let key_pair =
643 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
644 .unwrap();
645
646 let public_key_bytes = key_pair.public_key().as_ref().to_vec();
647 let x = public_key_bytes[1..33].to_vec();
648 let y = public_key_bytes[33..65].to_vec();
649
650 let cose_key = CosePublicKey::Ec2 { x, y };
651 let message = b"test message for WebAuthn";
652 let signature = key_pair.sign(&rng, message).unwrap();
653
654 let result = verify_cose_signature(COSE_ALG_ES256, &cose_key, message, signature.as_ref());
655 assert!(result.is_ok());
656
657 let wrong_result = verify_cose_signature(
658 COSE_ALG_ES256,
659 &cose_key,
660 b"wrong message",
661 signature.as_ref(),
662 );
663 assert!(matches!(
664 wrong_result,
665 Err(WebauthnError::SignatureVerificationFailed)
666 ));
667 }
668
669 #[test]
670 fn test_verify_cose_signature_alg_key_type_mismatch() {
671 let n = vec![0xAA; 256];
673 let e = vec![0x01, 0x00, 0x01];
674 let cose_key = build_cose_rsa_key(&n, &e);
675 let (alg, key) = parse_cose_key(&cose_key).unwrap();
676 assert_eq!(alg, -257);
677
678 let mismatch = verify_cose_signature(COSE_ALG_ES256, &key, b"data", &[0u8; 64]);
679 assert!(matches!(
680 mismatch,
681 Err(WebauthnError::VerificationFailed(_))
682 ));
683 }
684
685 #[test]
686 fn test_verify_cose_signature_rs256() {
687 let n = vec![
688 0xC0, 0xE9, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
689 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
690 0x00, 0x00, 0x00, 0x00,
691 ];
692 let e = vec![0x01, 0x00, 0x01];
693 let cose_key = build_cose_rsa_key(&n, &e);
694
695 let (alg, key) = parse_cose_key(&cose_key).unwrap();
696 assert_eq!(alg, -257);
697 match key {
698 CosePublicKey::Rsa { n: kn, e: ke } => {
699 assert_eq!(kn, n);
700 assert_eq!(ke, e);
701 }
702 _ => panic!("Expected RSA key"),
703 }
704
705 let rsa_key = RsaPublicKeyDer { n: &n, e: &e };
706 let der = rsa_key.to_der().unwrap();
707 assert_eq!(der[0], 0x30);
708 assert!(der.len() > 40);
709 }
710
711 #[test]
712 fn test_generate_challenge_bytes_length() {
713 let bytes = generate_challenge_bytes();
714 assert_eq!(bytes.len(), 32);
715 }
716
717 #[test]
718 fn test_generate_challenge_bytes_not_all_zero() {
719 let bytes = generate_challenge_bytes();
721 assert!(bytes.iter().any(|&b| b != 0));
722 }
723
724 #[test]
725 fn test_base64_roundtrip() {
726 let original = b"hello world";
727 let encoded = base64_encode_urlsafe(original);
728 let decoded = base64_decode_urlsafe(&encoded).unwrap();
729 assert_eq!(decoded, original);
730 }
731
732 #[test]
733 fn test_base64_decode_urlsafe_invalid() {
734 let result = base64_decode_urlsafe("NOT_VALID_BASE64!!!");
735 assert!(result.is_err());
736 }
737
738 #[test]
739 fn test_base64_encode_decode_empty() {
740 let encoded = base64_encode_urlsafe(b"");
741 let decoded = base64_decode_urlsafe(&encoded).unwrap();
742 assert!(decoded.is_empty());
743 }
744
745 #[test]
746 fn test_cbor_bytes_from_text_is_lenient() {
747 use ciborium::Value;
748 let v = Value::Text("abc".to_string());
749 assert_eq!(cbor_bytes(&v), Some(b"abc".to_vec()));
750 let v = Value::Null;
751 assert_eq!(cbor_bytes(&v), None);
752 }
753
754 #[test]
760 fn der_sequence_header_forms_at_length_boundaries() {
761 for (content_len, header_len) in [(0x7Fusize, 2usize), (0x80, 3), (0xFF, 3), (0x100, 4)] {
762 let mut buf = vec![0xCBu8; content_len];
763 RsaPublicKeyDer::encode_sequence_in_place(&mut buf);
764 assert_eq!(
765 buf.len(),
766 content_len + header_len,
767 "content_len {content_len:#x}: wrong total length"
768 );
769 assert_eq!(buf[0], 0x30, "content_len {content_len:#x}: SEQUENCE tag");
770 match header_len {
771 2 => assert_eq!(buf[1] as usize, content_len, "short-form length"),
772 3 => {
773 assert_eq!(buf[1], 0x81, "content_len {content_len:#x}: 0x81 form");
774 assert_eq!(buf[2] as usize, content_len);
775 }
776 4 => {
777 assert_eq!(buf[1], 0x82, "content_len {content_len:#x}: 0x82 form");
778 assert_eq!(((buf[2] as usize) << 8) | buf[3] as usize, content_len);
779 }
780 _ => unreachable!(),
781 }
782 assert!(buf[header_len..].iter().all(|&b| b == 0xCB));
784 }
785 }
786
787 #[test]
791 fn der_integer_encoding_edge_cases() {
792 let mut buf = Vec::new();
793 RsaPublicKeyDer::encode_integer(&mut buf, &[0u8; 8]);
794 assert_eq!(buf, vec![0x02, 0x01, 0x00]);
795
796 buf.clear();
797 buf.clear();
798 RsaPublicKeyDer::encode_integer(&mut buf, &[0x80, 0x00]);
800 assert_eq!(buf, vec![0x02, 0x03, 0x00, 0x80, 0x00]);
801
802 buf.clear();
803 RsaPublicKeyDer::encode_integer(&mut buf, &[0x00, 0x00, 0x7F]);
804 assert_eq!(buf, vec![0x02, 0x01, 0x7F]);
805
806 buf.clear();
807 RsaPublicKeyDer::encode_integer(&mut buf, &[]);
810 assert_eq!(buf, vec![0x02, 0x00]);
811
812 buf.clear();
813 RsaPublicKeyDer::encode_integer(&mut buf, &[0x01u8; 0x80]);
816 assert_eq!(&buf[..3], &[0x02, 0x81, 0x80]);
817 assert_eq!(buf.len(), 3 + 0x80);
818 assert!(buf[3..].iter().all(|&b| b == 0x01));
819 }
820
821 pub(crate) const RSA_PKCS8_B64: &str = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWaabvRsESS9rP70iKb473SMUwLEeKvSBGd2DcB4CgcjL2b69pqy7Mgp8ZXZlbJ96cK8y0OFZFKzFs5tLGVcG3JbkYSnFAgCHtW+k29N9B7XJe5k5K1p0piKQoeth8GX2QhYTNECABAU+FeKxAwjojTqVPH2oXNs7c7rQe5y03PYFqyrr0IyMt8QIXsjH7VVnTps/ojUQFqCPSqSi8Yb8BxokjtVd1K5IOlwYsfHLYBFefg3YTigQ5J/ZHZoL1oPRihRzPIEgDu4LmIT2Xp9Hzs9QV1DdLK2DxBcaMDyR/JEL1+LhnxD/HkaNdEDA/FA28JVl9GxHkHMEw3oyvEPDRAgMBAAECggEABS5GbbTGLwHQhkZQ/VOyXO9zZfa10CA2Mwx8Qu055PeKAdWpo74dsorykpvCuH0QZy4AXa9tvpwqQPytAzUuTZyjBM8rmh56YgPpwywpXx/1RzIvktb+5XtWDCmPDgHiwqOWsL7UG19sLxtk83tn0pIsPM7G3LMqlOQz7WHmZiH5UZmNKHcs5Gu5LxGoyMTN1weXk5Nx1OG63sUaXwFbaRv10Z+w6KBSpY9+fQDOXuet/5HwOl1cLXOcQeCXBsMDVUIU78+fsyqzZzIl7WL+rD1ulwCHcEFou3JxsFS+AuGXGAo6rIaFEOXR8g5vIF3g20zLCWYEzAByTUJa1hvnMQKBgQDS6gd8pbKi1cbg75s8JrNEFmMp3C2pAA42FvFQuigVy5p+92Lv3dLFM3a+UOz3l2DFwTQEUzm6tDKCCsMr5J2W8OZCQJ+QvvmI4ErN3X4h/HF6zcM9cZScMQN6/qe/jk6UMuzBFkenajbSvvvuKqlzV9zFmSV6+qh88CDTwiwCcwKBgQC2kMSYDFKPDsHqGgHOsaytHsi4lyUUGtIOVo+kvGWKYycr3V6q/sliMJ6v/E1SdVSJCDPyZbWc0fL9+PNhHrF5PGKS+HAjXsvQ+xOzXweByDiAZVmVNKxHmf9LFiAPPzUGIMxzwT9TU823E62aXybfgz7wCCFXx3Eh4sPCjN36qwKBgDKr2QqgQG+QjoxB5HiqD41/F2naJPoiMkfacTVk0/aQiNiSFKnuEBIikBefF59QNgasqROU7xyk6DGH5mXoMdgunhMytWMwDoFM6YvV99Swco7/WjWr0PlJaT2maqTByq0eIvUspiBZizxMd/g7NaSpajfq2C9YgxwpEKnvT2VzAoGAT6SL/wCxK3N2qNe7nh3ohIV/bveQ11pz9IlSlL0TVvG2bu5dlB8eX1VyhLd+S9CflkAb2U0Bk24LoTvvgJjRN2BeaFs1IFkEdSBzEbcNIVLlQy3zjKGz3nCR7IG0brJWQVwhlQXiyEkw3wMYotWLscohtLj3QsHg2rWATOkDFY0CgYEAuc5W0GMzucY940ok8VoIBrsD/FVtpfflsx/FDGUafBjvQ/2kLH7iyfZLX0jFTACiazLpxZESVcxEWYEr2Rz4KG7GGKYkppwMoH43Xk1L+zkR2ZeEHnzTzwLdVhczqGTs90V4HCYwLKzXqgJPZeH/42ngQuA/8dUrQ/9ofV68k4A=";
826
827 fn der_tlv(i: &[u8]) -> (&[u8], &[u8]) {
829 let l0 = *i.get(1).unwrap();
830 let (header, len) = if l0 < 0x80 {
831 (2usize, l0 as usize)
832 } else {
833 let n = (l0 & 0x7F) as usize;
834 let mut len = 0usize;
835 for b in &i[2..2 + n] {
836 len = (len << 8) | *b as usize;
837 }
838 (2 + n, len)
839 };
840 (&i[header..header + len], &i[header + len..])
841 }
842
843 pub(crate) fn rsa_test_material() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
845 use base64::Engine as _;
846 let pkcs8 = base64::engine::general_purpose::STANDARD
847 .decode(RSA_PKCS8_B64)
848 .unwrap();
849 let (info, _) = der_tlv(&pkcs8);
852 let (_version, rest) = der_tlv(info);
853 let (_algorithm, rest) = der_tlv(rest);
854 let (octet, _) = der_tlv(rest);
855 let (rsa_key, _) = der_tlv(octet);
856 let (_version, rest) = der_tlv(rsa_key);
857 let (n, rest) = der_tlv(rest);
858 let (e, _) = der_tlv(rest);
859 (n.to_vec(), e.to_vec(), pkcs8)
860 }
861
862 pub(crate) fn rsa_test_spki() -> Vec<u8> {
865 let (n, e, _) = rsa_test_material();
866 RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap()
867 }
868
869 fn cose_key_from(entries: Vec<(i64, ciborium::Value)>) -> Vec<u8> {
870 use ciborium::Value;
871 let map: Vec<(Value, Value)> = entries
872 .into_iter()
873 .map(|(k, v)| (Value::Integer(k.into()), v))
874 .collect();
875 let mut buf = Vec::new();
876 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
877 buf
878 }
879
880 #[test]
881 fn parse_cose_key_garbage_cbor_is_error() {
882 let result = parse_cose_key(&[0xFF, 0xFF, 0xFF, 0xFF]);
884 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
885 }
886
887 #[test]
888 fn parse_cose_key_ec2_missing_crv() {
889 let buf = cose_key_from(vec![
890 (1, ciborium::Value::Integer(2.into())),
891 (3, ciborium::Value::Integer((-7).into())),
892 (-2, ciborium::Value::Bytes(vec![0xAA; 32])),
893 (-3, ciborium::Value::Bytes(vec![0xBB; 32])),
894 ]);
895 assert!(matches!(
896 parse_cose_key(&buf),
897 Err(WebauthnError::VerificationFailed(_))
898 ));
899 }
900
901 #[test]
902 fn parse_cose_key_ec2_wrong_curve_is_unsupported() {
903 let buf = cose_key_from(vec![
904 (1, ciborium::Value::Integer(2.into())),
905 (3, ciborium::Value::Integer((-7).into())),
906 (-1, ciborium::Value::Integer(3.into())), (-2, ciborium::Value::Bytes(vec![0xAA; 48])),
908 (-3, ciborium::Value::Bytes(vec![0xBB; 48])),
909 ]);
910 assert!(matches!(
911 parse_cose_key(&buf),
912 Err(WebauthnError::UnsupportedAlgorithm(-7))
913 ));
914 }
915
916 #[test]
917 fn parse_cose_key_ec2_missing_coordinates() {
918 let kty_alg = vec![
919 (1, ciborium::Value::Integer(2.into())),
920 (3, ciborium::Value::Integer((-7).into())),
921 (-1, ciborium::Value::Integer(1.into())),
922 ];
923 let missing_x = cose_key_from(
924 [
925 kty_alg.clone(),
926 vec![(-3, ciborium::Value::Bytes(vec![0xBB; 32]))],
927 ]
928 .concat(),
929 );
930 assert!(matches!(
931 parse_cose_key(&missing_x),
932 Err(WebauthnError::VerificationFailed(_))
933 ));
934
935 let missing_y =
936 cose_key_from([kty_alg, vec![(-2, ciborium::Value::Bytes(vec![0xAA; 32]))]].concat());
937 assert!(matches!(
938 parse_cose_key(&missing_y),
939 Err(WebauthnError::VerificationFailed(_))
940 ));
941 }
942
943 #[test]
944 fn parse_cose_key_rsa_missing_modulus_or_exponent() {
945 let missing_n = cose_key_from(vec![
946 (1, ciborium::Value::Integer(3.into())),
947 (3, ciborium::Value::Integer((-257).into())),
948 (-2, ciborium::Value::Bytes(vec![0x01, 0x00, 0x01])),
949 ]);
950 assert!(matches!(
951 parse_cose_key(&missing_n),
952 Err(WebauthnError::VerificationFailed(_))
953 ));
954
955 let missing_e = cose_key_from(vec![
956 (1, ciborium::Value::Integer(3.into())),
957 (3, ciborium::Value::Integer((-257).into())),
958 (-1, ciborium::Value::Bytes(vec![0xAA; 256])),
959 ]);
960 assert!(matches!(
961 parse_cose_key(&missing_e),
962 Err(WebauthnError::VerificationFailed(_))
963 ));
964 }
965
966 #[test]
967 fn parse_cose_key_unknown_kty_is_error() {
968 let buf = cose_key_from(vec![
969 (1, ciborium::Value::Integer(99.into())),
970 (3, ciborium::Value::Integer((-7).into())),
971 ]);
972 assert!(matches!(
973 parse_cose_key(&buf),
974 Err(WebauthnError::VerificationFailed(_))
975 ));
976 }
977
978 #[test]
979 fn cbor_map_entries_rejects_duplicate_labels() {
980 use ciborium::Value;
986 let map = Value::Map(vec![
987 (Value::Integer(1.into()), Value::Integer(2.into())),
988 (Value::Integer(3.into()), Value::Integer((-7).into())),
989 (Value::Integer(3.into()), Value::Integer((-257).into())),
990 ]);
991 assert!(cbor_map_entries(&map).is_none());
992
993 let benign = Value::Map(vec![
995 (Value::Integer(1.into()), Value::Integer(2.into())),
996 (Value::Integer(1.into()), Value::Integer(2.into())),
997 ]);
998 assert!(cbor_map_entries(&benign).is_none());
999 }
1000
1001 #[test]
1002 fn cbor_map_entries_rejects_non_integer_keys() {
1003 use ciborium::Value;
1004 let map = Value::Map(vec![(
1005 Value::Text("kty".to_string()),
1006 Value::Integer(2.into()),
1007 )]);
1008 assert!(cbor_map_entries(&map).is_none());
1009 }
1010
1011 #[test]
1012 fn verify_rs256_with_ec_key_is_rejected() {
1013 let key = CosePublicKey::Ec2 {
1014 x: vec![0xAA; 32],
1015 y: vec![0xBB; 32],
1016 };
1017 assert!(matches!(
1018 verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]),
1019 Err(WebauthnError::VerificationFailed(_))
1020 ));
1021 }
1022
1023 #[test]
1024 fn verify_cose_signature_unknown_alg_is_rejected() {
1025 let key = CosePublicKey::Ec2 {
1026 x: vec![0xAA; 32],
1027 y: vec![0xBB; 32],
1028 };
1029 assert!(matches!(
1030 verify_cose_signature(-47, &key, b"data", &[0u8; 64]),
1031 Err(WebauthnError::UnsupportedAlgorithm(-47))
1032 ));
1033 }
1034
1035 #[test]
1036 fn alg_to_name_covers_known_and_unknown() {
1037 assert_eq!(alg_to_name(COSE_ALG_ES256), "ES256");
1038 assert_eq!(alg_to_name(COSE_ALG_ES384), "ES384");
1039 assert_eq!(alg_to_name(COSE_ALG_EDDSA), "EdDSA");
1040 assert_eq!(alg_to_name(COSE_ALG_RS256), "RS256");
1041 assert_eq!(alg_to_name(0), "unknown");
1042 }
1043
1044 #[test]
1049 fn verify_es384_real_signature_roundtrip() {
1050 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1051
1052 let rng = ring::rand::SystemRandom::new();
1053 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1054 let key_pair =
1055 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1056 .unwrap();
1057
1058 let pub_bytes = key_pair.public_key().as_ref();
1059 assert_eq!(pub_bytes.len(), 97); let x = pub_bytes[1..49].to_vec();
1061 let y = pub_bytes[49..97].to_vec();
1062 let key = CosePublicKey::Ec2 { x, y };
1063
1064 let message = b"webauthn-kit es384 vector";
1065 let signature = key_pair.sign(&rng, message).unwrap();
1066 assert_eq!(signature.as_ref().len(), 96); assert!(verify_cose_signature(COSE_ALG_ES384, &key, message, signature.as_ref()).is_ok());
1069
1070 let mut tampered = *message;
1071 tampered[0] ^= 0x01;
1072 assert!(matches!(
1073 verify_cose_signature(COSE_ALG_ES384, &key, &tampered, signature.as_ref()),
1074 Err(WebauthnError::SignatureVerificationFailed)
1075 ));
1076
1077 assert!(matches!(
1079 verify_cose_signature(COSE_ALG_ES256, &key, message, signature.as_ref()),
1080 Err(WebauthnError::SignatureVerificationFailed)
1081 ));
1082 }
1083
1084 #[test]
1087 fn verify_eddsa_real_signature_roundtrip() {
1088 use ring::signature::{Ed25519KeyPair, KeyPair as _};
1089
1090 let rng = ring::rand::SystemRandom::new();
1091 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1092 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1093
1094 let public = key_pair.public_key().as_ref().to_vec();
1095 assert_eq!(public.len(), 32);
1096 let key = CosePublicKey::Okp { id: public };
1097
1098 let message = b"webauthn-kit ed25519 vector";
1099 let signature = key_pair.sign(message);
1100 assert_eq!(signature.as_ref().len(), 64);
1101
1102 assert!(verify_cose_signature(COSE_ALG_EDDSA, &key, message, signature.as_ref()).is_ok());
1103
1104 let mut tampered = *message;
1105 tampered[0] ^= 0x01;
1106 assert!(matches!(
1107 verify_cose_signature(COSE_ALG_EDDSA, &key, &tampered, signature.as_ref()),
1108 Err(WebauthnError::SignatureVerificationFailed)
1109 ));
1110 }
1111
1112 #[test]
1115 fn parse_cose_p384_key() {
1116 let x = vec![0xCC; 48];
1117 let y = vec![0xDD; 48];
1118 let cose_key = build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES384, &x, &y);
1119
1120 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1121 assert_eq!(alg, COSE_ALG_ES384);
1122 match key {
1123 CosePublicKey::Ec2 { x: kx, y: ky } => {
1124 assert_eq!(kx, x);
1125 assert_eq!(ky, y);
1126 }
1127 _ => panic!("Expected EC2 key"),
1128 }
1129 }
1130
1131 #[test]
1133 fn parse_cose_okp_ed25519_key() {
1134 let id = vec![0x42u8; 32];
1135 let cose_key = build_cose_okp_key(&id);
1136
1137 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1138 assert_eq!(alg, COSE_ALG_EDDSA);
1139 match key {
1140 CosePublicKey::Okp { id: kid } => assert_eq!(kid, id),
1141 _ => panic!("Expected OKP key"),
1142 }
1143 }
1144
1145 #[test]
1151 fn parse_p256_key_claiming_es384_rejected() {
1152 let cose_key =
1153 build_cose_ec2_key_alg(COSE_CRV_P256, COSE_ALG_ES384, &[0xAA; 32], &[0xBB; 32]);
1154 assert!(matches!(
1155 parse_cose_key(&cose_key),
1156 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1157 ));
1158 }
1159
1160 #[test]
1162 fn parse_p384_key_claiming_es256_rejected() {
1163 let cose_key =
1164 build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES256, &[0xAA; 48], &[0xBB; 48]);
1165 assert!(matches!(
1166 parse_cose_key(&cose_key),
1167 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1168 ));
1169 }
1170
1171 #[test]
1174 fn parse_okp_claiming_other_algorithms_rejected() {
1175 let bad_alg = cose_key_from(vec![
1177 (1, ciborium::Value::Integer(1.into())),
1178 (3, ciborium::Value::Integer((-7).into())),
1179 (-1, ciborium::Value::Integer(6.into())),
1180 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1181 ]);
1182 assert!(matches!(
1183 parse_cose_key(&bad_alg),
1184 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1185 ));
1186
1187 let bad_crv = cose_key_from(vec![
1189 (1, ciborium::Value::Integer(1.into())),
1190 (3, ciborium::Value::Integer((-8).into())),
1191 (-1, ciborium::Value::Integer(4.into())),
1192 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1193 ]);
1194 assert!(matches!(
1195 parse_cose_key(&bad_crv),
1196 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_EDDSA))
1197 ));
1198 }
1199
1200 #[test]
1203 fn verify_okp_ec2_type_mismatches_rejected() {
1204 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1205 assert!(matches!(
1206 verify_cose_signature(COSE_ALG_ES256, &okp, b"data", &[0u8; 64]),
1207 Err(WebauthnError::VerificationFailed(_))
1208 ));
1209
1210 let ec2 = CosePublicKey::Ec2 {
1211 x: vec![0xAA; 32],
1212 y: vec![0xBB; 32],
1213 };
1214 assert!(matches!(
1215 verify_cose_signature(COSE_ALG_EDDSA, &ec2, b"data", &[0u8; 64]),
1216 Err(WebauthnError::VerificationFailed(_))
1217 ));
1218 }
1219
1220 #[test]
1223 fn verify_es384_short_point_fails_closed() {
1224 let short = CosePublicKey::Ec2 {
1225 x: vec![0xAA; 32], y: vec![0xBB; 32],
1227 };
1228 assert!(matches!(
1229 verify_cose_signature(COSE_ALG_ES384, &short, b"data", &[0u8; 96]),
1230 Err(WebauthnError::SignatureVerificationFailed)
1231 ));
1232 }
1233
1234 #[test]
1237 fn verify_eddsa_wrong_key_length_fails_closed() {
1238 let long = CosePublicKey::Okp { id: vec![0x42; 31] };
1239 assert!(matches!(
1240 verify_cose_signature(COSE_ALG_EDDSA, &long, b"data", &[0u8; 64]),
1241 Err(WebauthnError::SignatureVerificationFailed)
1242 ));
1243 let short_sig = CosePublicKey::Okp { id: vec![0x42; 32] };
1244 assert!(matches!(
1245 verify_cose_signature(COSE_ALG_EDDSA, &short_sig, b"data", &[0u8; 63]),
1246 Err(WebauthnError::SignatureVerificationFailed)
1247 ));
1248 }
1249
1250 #[test]
1254 fn cose_key_unknown_labels_and_okp_error_paths() {
1255 let with_extra = cose_key_from(vec![
1257 (1, ciborium::Value::Integer(1.into())),
1258 (3, ciborium::Value::Integer((-8).into())),
1259 (-1, ciborium::Value::Integer(6.into())),
1260 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1261 (99, ciborium::Value::Text("ignored".to_string())),
1262 ]);
1263 let (alg, _) = parse_cose_key(&with_extra).unwrap();
1264 assert_eq!(alg, COSE_ALG_EDDSA);
1265
1266 let no_bytes = cose_key_from(vec![
1268 (1, ciborium::Value::Integer(1.into())),
1269 (3, ciborium::Value::Integer((-8).into())),
1270 (-1, ciborium::Value::Integer(6.into())),
1271 ]);
1272 assert!(matches!(
1273 parse_cose_key(&no_bytes),
1274 Err(WebauthnError::VerificationFailed(_))
1275 ));
1276
1277 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1279 assert!(matches!(
1280 verify_cose_signature(COSE_ALG_ES384, &okp, b"data", &[0u8; 96]),
1281 Err(WebauthnError::VerificationFailed(_))
1282 ));
1283 }
1284
1285 #[test]
1288 fn verify_rs256_real_signature_roundtrip() {
1289 use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
1290
1291 let (n, e, pkcs8) = rsa_test_material();
1292 let rsa = RsaKeyPair::from_pkcs8(&pkcs8).unwrap();
1293 let modulus_len = rsa.public().modulus_len();
1294 assert_eq!(modulus_len, 256);
1295
1296 let rng = ring::rand::SystemRandom::new();
1298 let message = b"webauthn-kit rs256 vector";
1299 let mut signature = vec![0u8; modulus_len];
1300 rsa.sign(&RSA_PKCS1_SHA256, &rng, message, &mut signature)
1301 .unwrap();
1302
1303 let key = CosePublicKey::Rsa { n, e };
1304 assert!(verify_cose_signature(COSE_ALG_RS256, &key, message, &signature).is_ok());
1305
1306 let mut tampered = *message;
1308 tampered[0] ^= 0x01;
1309 assert!(matches!(
1310 verify_cose_signature(COSE_ALG_RS256, &key, &tampered, &signature),
1311 Err(WebauthnError::SignatureVerificationFailed)
1312 ));
1313 }
1314
1315 #[test]
1319 fn der_long_form_length_encoding() {
1320 let n = [0x77u8; 128];
1321 let e = [0x01, 0x00, 0x01];
1322 let der = RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap();
1323
1324 assert_eq!(der[0], 0x30);
1326 assert_eq!(der[1], 0x81);
1327 assert_eq!(der[2] as usize + 3, der.len());
1328 assert!(der.windows(3).any(|w| w == [0x02, 0x81, 0x80]));
1330 }
1331}