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 for (k, v) in entries {
114 let key = cbor_i64(k)?;
115 result.push((key, v.clone()));
116 }
117 Some(result)
118 }
119 _ => None,
120 }
121}
122
123pub fn parse_cose_key(cose_bytes: &[u8]) -> Result<(i32, CosePublicKey), WebauthnError> {
150 use ciborium::Value;
151
152 let key_val: Value = ciborium::de::from_reader(cose_bytes).map_err(|e| {
153 WebauthnError::VerificationFailed(format!("COSE key CBOR parse error: {e}"))
154 })?;
155
156 let entries = cbor_map_entries(&key_val).ok_or_else(|| {
157 WebauthnError::VerificationFailed("COSE key is not a CBOR map".to_string())
158 })?;
159
160 let mut kty: Option<i64> = None;
161 let mut alg: Option<i32> = None;
162 let mut crv: Option<i64> = None;
163 let mut x: Option<Vec<u8>> = None;
164 let mut y: Option<Vec<u8>> = None;
165 let mut n: Option<Vec<u8>> = None;
166 let mut e: Option<Vec<u8>> = None;
167
168 for (label, val) in &entries {
169 match *label {
170 COSE_KEY_KTY => kty = cbor_i64(val),
171 COSE_KEY_ALG => alg = cbor_i64(val).map(|v| v as i32),
172 COSE_KEY_CRV_N => match kty {
173 Some(COSE_KTY_RSA) => n = cbor_bytes(val),
174 _ => crv = cbor_i64(val),
175 },
176 COSE_KEY_X_E => match kty {
177 Some(COSE_KTY_RSA) => e = cbor_bytes(val),
178 _ => x = cbor_bytes(val),
179 },
180 COSE_KEY_Y => y = cbor_bytes(val),
181 _ => {}
182 }
183 }
184
185 let kty =
186 kty.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'kty'".to_string()))?;
187 let alg =
188 alg.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'alg'".to_string()))?;
189
190 match kty {
191 COSE_KTY_EC2 => {
192 let crv = crv.ok_or_else(|| {
193 WebauthnError::VerificationFailed("EC2 key missing 'crv'".to_string())
194 })?;
195 let curve_ok = matches!(
199 (crv, alg),
200 (COSE_CRV_P256, COSE_ALG_ES256) | (COSE_CRV_P384, COSE_ALG_ES384)
201 );
202 if !curve_ok {
203 return Err(WebauthnError::UnsupportedAlgorithm(alg));
204 }
205 let x = x.ok_or_else(|| {
206 WebauthnError::VerificationFailed("EC2 key missing 'x'".to_string())
207 })?;
208 let y = y.ok_or_else(|| {
209 WebauthnError::VerificationFailed("EC2 key missing 'y'".to_string())
210 })?;
211 Ok((alg, CosePublicKey::Ec2 { x, y }))
212 }
213 COSE_KTY_RSA => {
214 let n = n.ok_or_else(|| {
215 WebauthnError::VerificationFailed("RSA key missing 'n'".to_string())
216 })?;
217 let e = e.ok_or_else(|| {
218 WebauthnError::VerificationFailed("RSA key missing 'e'".to_string())
219 })?;
220 Ok((alg, CosePublicKey::Rsa { n, e }))
221 }
222 COSE_KTY_OKP => {
223 if crv != Some(COSE_CRV_ED25519) || alg != COSE_ALG_EDDSA {
225 return Err(WebauthnError::UnsupportedAlgorithm(alg));
226 }
227 let id = x.ok_or_else(|| {
228 WebauthnError::VerificationFailed("OKP key missing public key bytes".to_string())
229 })?;
230 Ok((alg, CosePublicKey::Okp { id }))
231 }
232 other => Err(WebauthnError::VerificationFailed(format!(
233 "Unsupported COSE key type: {other}"
234 ))),
235 }
236}
237
238pub fn verify_cose_signature(
258 alg: i32,
259 public_key: &CosePublicKey,
260 signed_data: &[u8],
261 signature: &[u8],
262) -> Result<(), WebauthnError> {
263 use ring::signature;
264
265 match alg {
266 COSE_ALG_ES256 => {
267 let CosePublicKey::Ec2 { x, y } = public_key else {
268 return Err(WebauthnError::VerificationFailed(
269 "EC2 key expected for ES256".to_string(),
270 ));
271 };
272
273 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
274 public_key_bytes.push(0x04);
275 public_key_bytes.extend_from_slice(x);
276 public_key_bytes.extend_from_slice(y);
277
278 let public_key = signature::UnparsedPublicKey::new(
279 &signature::ECDSA_P256_SHA256_FIXED,
280 &public_key_bytes,
281 );
282 public_key
283 .verify(signed_data, signature)
284 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
285 Ok(())
286 }
287 COSE_ALG_ES384 => {
288 let CosePublicKey::Ec2 { x, y } = public_key else {
289 return Err(WebauthnError::VerificationFailed(
290 "EC2 key expected for ES384".to_string(),
291 ));
292 };
293
294 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
295 public_key_bytes.push(0x04);
296 public_key_bytes.extend_from_slice(x);
297 public_key_bytes.extend_from_slice(y);
298
299 let public_key = signature::UnparsedPublicKey::new(
300 &signature::ECDSA_P384_SHA384_FIXED,
301 &public_key_bytes,
302 );
303 public_key
304 .verify(signed_data, signature)
305 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
306 Ok(())
307 }
308 COSE_ALG_EDDSA => {
309 let CosePublicKey::Okp { id } = public_key else {
310 return Err(WebauthnError::VerificationFailed(
311 "OKP key expected for EdDSA".to_string(),
312 ));
313 };
314
315 let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, id);
316 public_key
317 .verify(signed_data, signature)
318 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
319 Ok(())
320 }
321 COSE_ALG_RS256 => {
322 let CosePublicKey::Rsa { n, e } = public_key else {
323 return Err(WebauthnError::VerificationFailed(
324 "RSA key expected for RS256".to_string(),
325 ));
326 };
327
328 let rsa_public_key = RsaPublicKeyDer { n, e };
329 let der_bytes = rsa_public_key.to_der()?;
330
331 let public_key = signature::UnparsedPublicKey::new(
332 &signature::RSA_PKCS1_2048_8192_SHA256,
333 &der_bytes,
334 );
335 public_key
336 .verify(signed_data, signature)
337 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
338 Ok(())
339 }
340 other => Err(WebauthnError::UnsupportedAlgorithm(other)),
341 }
342}
343
344struct RsaPublicKeyDer<'a> {
347 n: &'a [u8],
348 e: &'a [u8],
349}
350
351impl RsaPublicKeyDer<'_> {
352 fn to_der(&self) -> Result<Vec<u8>, WebauthnError> {
359 let mut der = Vec::new();
360 Self::encode_integer(&mut der, self.n);
361 Self::encode_integer(&mut der, self.e);
362 Self::encode_sequence_in_place(&mut der);
363 Ok(der)
364 }
365
366 fn encode_integer(buf: &mut Vec<u8>, value: &[u8]) {
372 let start = value
373 .iter()
374 .position(|&b| b != 0)
375 .unwrap_or(value.len().saturating_sub(1));
376 let v = value.get(start..).unwrap_or(&[]);
377 if v.first().is_some_and(|&b| b & 0x80 != 0) {
378 buf.push(0x02);
379 Self::encode_length(buf, v.len() + 1);
380 buf.push(0x00);
381 buf.extend_from_slice(v);
382 } else {
383 buf.push(0x02);
384 Self::encode_length(buf, v.len());
385 buf.extend_from_slice(v);
386 }
387 }
388
389 fn encode_sequence_in_place(buf: &mut Vec<u8>) {
391 let content_len = buf.len();
392 let header_len = if content_len < 0x80 {
393 2
394 } else if content_len < 0x100 {
395 3
396 } else {
397 4
398 };
399 buf.resize(content_len + header_len, 0);
400 buf.copy_within(..content_len, header_len);
401 if let Some(tag) = buf.first_mut() {
402 *tag = 0x30;
403 }
404 Self::encode_length_at(buf, 1, content_len);
405 }
406
407 fn encode_length(buf: &mut Vec<u8>, len: usize) {
409 if len < 0x80 {
410 buf.push(len as u8);
411 } else if len < 0x100 {
412 buf.push(0x81);
413 buf.push(len as u8);
414 } else {
415 buf.push(0x82);
416 buf.push((len >> 8) as u8);
417 buf.push((len & 0xFF) as u8);
418 }
419 }
420
421 #[allow(clippy::indexing_slicing)]
428 fn encode_length_at(buf: &mut [u8], offset: usize, len: usize) {
429 if len < 0x80 {
430 buf[offset] = len as u8;
431 } else if len < 0x100 {
432 buf[offset] = 0x81;
433 buf[offset + 1] = len as u8;
434 } else {
435 buf[offset] = 0x82;
436 buf[offset + 1] = (len >> 8) as u8;
437 buf[offset + 2] = (len & 0xFF) as u8;
438 }
439 }
440}
441
442pub fn alg_to_name(alg: i32) -> &'static str {
444 match alg {
445 COSE_ALG_ES256 => "ES256",
446 COSE_ALG_ES384 => "ES384",
447 COSE_ALG_EDDSA => "EdDSA",
448 COSE_ALG_RS256 => "RS256",
449 _ => "unknown",
450 }
451}
452
453pub fn generate_challenge_bytes() -> Vec<u8> {
466 use ring::rand::SecureRandom;
467 let mut bytes = [0u8; 32];
468 #[allow(clippy::expect_used)]
471 ring::rand::SystemRandom::new()
472 .fill(&mut bytes)
473 .expect("OS CSPRNG unavailable; cannot generate secure challenge");
474 bytes.to_vec()
475}
476
477pub fn base64_encode_urlsafe(data: &[u8]) -> String {
480 use base64::Engine;
481 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
482}
483
484pub fn base64_decode_urlsafe(data: &str) -> Result<Vec<u8>, WebauthnError> {
492 use base64::Engine;
493 base64::engine::general_purpose::URL_SAFE_NO_PAD
494 .decode(data)
495 .map_err(|e| WebauthnError::VerificationFailed(format!("base64 decode error: {e}")))
496}
497
498#[allow(
502 clippy::unwrap_used,
503 clippy::expect_used,
504 clippy::indexing_slicing,
505 clippy::panic
506)]
507#[cfg(test)]
508pub(crate) mod tests {
509 use super::*;
510
511 pub(crate) fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
513 build_cose_ec2_key_alg(COSE_CRV_P256, COSE_ALG_ES256, x, y)
514 }
515
516 pub(crate) fn build_cose_ec2_key_alg(crv: i64, alg: i32, x: &[u8], y: &[u8]) -> Vec<u8> {
518 use ciborium::Value;
519 let map = vec![
520 (Value::Integer(1.into()), Value::Integer(2.into())),
521 (Value::Integer(3.into()), Value::Integer(alg.into())),
522 (Value::Integer((-1).into()), Value::Integer(crv.into())),
523 (Value::Integer((-2).into()), Value::Bytes(x.to_vec())),
524 (Value::Integer((-3).into()), Value::Bytes(y.to_vec())),
525 ];
526 let mut buf = Vec::new();
527 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
528 buf
529 }
530
531 pub(crate) fn build_cose_okp_key(id: &[u8]) -> Vec<u8> {
533 use ciborium::Value;
534 let map = vec![
535 (Value::Integer(1.into()), Value::Integer(1.into())),
536 (Value::Integer(3.into()), Value::Integer((-8).into())),
537 (Value::Integer((-1).into()), Value::Integer(6.into())),
538 (Value::Integer((-2).into()), Value::Bytes(id.to_vec())),
539 ];
540 let mut buf = Vec::new();
541 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
542 buf
543 }
544
545 pub(crate) fn build_cose_rsa_key(n: &[u8], e: &[u8]) -> Vec<u8> {
547 use ciborium::Value;
548 let map = vec![
549 (Value::Integer(1.into()), Value::Integer(3.into())),
550 (Value::Integer(3.into()), Value::Integer((-257).into())),
551 (Value::Integer((-1).into()), Value::Bytes(n.to_vec())),
552 (Value::Integer((-2).into()), Value::Bytes(e.to_vec())),
553 ];
554 let mut buf = Vec::new();
555 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
556 buf
557 }
558
559 #[test]
560 fn test_parse_cose_ec2_key() {
561 let x = vec![0xAA; 32];
562 let y = vec![0xBB; 32];
563 let cose_key = build_cose_ec2_key(&x, &y);
564
565 let (alg, key) = parse_cose_key(&cose_key).unwrap();
566 assert_eq!(alg, -7);
567 match key {
568 CosePublicKey::Ec2 { x: kx, y: ky } => {
569 assert_eq!(kx, x);
570 assert_eq!(ky, y);
571 }
572 _ => panic!("Expected EC2 key"),
573 }
574 }
575
576 #[test]
577 fn test_parse_cose_rsa_key() {
578 let n = vec![0xAA; 256];
579 let e = vec![0x01, 0x00, 0x01];
580 let cose_key = build_cose_rsa_key(&n, &e);
581
582 let (alg, key) = parse_cose_key(&cose_key).unwrap();
583 assert_eq!(alg, -257);
584 match key {
585 CosePublicKey::Rsa { n: kn, e: ke } => {
586 assert_eq!(kn, n);
587 assert_eq!(ke, e);
588 }
589 _ => panic!("Expected RSA key"),
590 }
591 }
592
593 #[test]
594 fn test_parse_cose_key_not_a_map() {
595 let buf = [0x00u8]; let result = parse_cose_key(&buf);
597 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
598 }
599
600 #[test]
601 fn test_parse_cose_key_missing_kty() {
602 use ciborium::Value;
603 let map = vec![(Value::Integer(3.into()), Value::Integer((-7).into()))];
604 let mut buf = Vec::new();
605 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
606 let result = parse_cose_key(&buf);
607 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
608 }
609
610 #[test]
611 fn test_parse_cose_key_okp_unsupported() {
612 use ciborium::Value;
613 let map = vec![
614 (Value::Integer(1.into()), Value::Integer(1.into())),
615 (Value::Integer(3.into()), Value::Integer((-8).into())),
616 ];
617 let mut buf = Vec::new();
618 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
619 let result = parse_cose_key(&buf);
620 assert!(matches!(
621 result,
622 Err(WebauthnError::UnsupportedAlgorithm(-8))
623 ));
624 }
625
626 #[test]
627 fn test_verify_cose_signature_es256() {
628 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P256_SHA256_FIXED_SIGNING};
629
630 let rng = ring::rand::SystemRandom::new();
631 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
632 let key_pair =
633 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
634 .unwrap();
635
636 let public_key_bytes = key_pair.public_key().as_ref().to_vec();
637 let x = public_key_bytes[1..33].to_vec();
638 let y = public_key_bytes[33..65].to_vec();
639
640 let cose_key = CosePublicKey::Ec2 { x, y };
641 let message = b"test message for WebAuthn";
642 let signature = key_pair.sign(&rng, message).unwrap();
643
644 let result = verify_cose_signature(COSE_ALG_ES256, &cose_key, message, signature.as_ref());
645 assert!(result.is_ok());
646
647 let wrong_result = verify_cose_signature(
648 COSE_ALG_ES256,
649 &cose_key,
650 b"wrong message",
651 signature.as_ref(),
652 );
653 assert!(matches!(
654 wrong_result,
655 Err(WebauthnError::SignatureVerificationFailed)
656 ));
657 }
658
659 #[test]
660 fn test_verify_cose_signature_alg_key_type_mismatch() {
661 let n = vec![0xAA; 256];
663 let e = vec![0x01, 0x00, 0x01];
664 let cose_key = build_cose_rsa_key(&n, &e);
665 let (alg, key) = parse_cose_key(&cose_key).unwrap();
666 assert_eq!(alg, -257);
667
668 let mismatch = verify_cose_signature(COSE_ALG_ES256, &key, b"data", &[0u8; 64]);
669 assert!(matches!(
670 mismatch,
671 Err(WebauthnError::VerificationFailed(_))
672 ));
673 }
674
675 #[test]
676 fn test_verify_cose_signature_rs256() {
677 let n = vec![
678 0xC0, 0xE9, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
679 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
680 0x00, 0x00, 0x00, 0x00,
681 ];
682 let e = vec![0x01, 0x00, 0x01];
683 let cose_key = build_cose_rsa_key(&n, &e);
684
685 let (alg, key) = parse_cose_key(&cose_key).unwrap();
686 assert_eq!(alg, -257);
687 match key {
688 CosePublicKey::Rsa { n: kn, e: ke } => {
689 assert_eq!(kn, n);
690 assert_eq!(ke, e);
691 }
692 _ => panic!("Expected RSA key"),
693 }
694
695 let rsa_key = RsaPublicKeyDer { n: &n, e: &e };
696 let der = rsa_key.to_der().unwrap();
697 assert_eq!(der[0], 0x30);
698 assert!(der.len() > 40);
699 }
700
701 #[test]
702 fn test_generate_challenge_bytes_length() {
703 let bytes = generate_challenge_bytes();
704 assert_eq!(bytes.len(), 32);
705 }
706
707 #[test]
708 fn test_generate_challenge_bytes_not_all_zero() {
709 let bytes = generate_challenge_bytes();
711 assert!(bytes.iter().any(|&b| b != 0));
712 }
713
714 #[test]
715 fn test_base64_roundtrip() {
716 let original = b"hello world";
717 let encoded = base64_encode_urlsafe(original);
718 let decoded = base64_decode_urlsafe(&encoded).unwrap();
719 assert_eq!(decoded, original);
720 }
721
722 #[test]
723 fn test_base64_decode_urlsafe_invalid() {
724 let result = base64_decode_urlsafe("NOT_VALID_BASE64!!!");
725 assert!(result.is_err());
726 }
727
728 #[test]
729 fn test_base64_encode_decode_empty() {
730 let encoded = base64_encode_urlsafe(b"");
731 let decoded = base64_decode_urlsafe(&encoded).unwrap();
732 assert!(decoded.is_empty());
733 }
734
735 #[test]
736 fn test_cbor_bytes_from_text_is_lenient() {
737 use ciborium::Value;
738 let v = Value::Text("abc".to_string());
739 assert_eq!(cbor_bytes(&v), Some(b"abc".to_vec()));
740 let v = Value::Null;
741 assert_eq!(cbor_bytes(&v), None);
742 }
743
744 #[test]
750 fn der_sequence_header_forms_at_length_boundaries() {
751 for (content_len, header_len) in [(0x7Fusize, 2usize), (0x80, 3), (0xFF, 3), (0x100, 4)] {
752 let mut buf = vec![0xCBu8; content_len];
753 RsaPublicKeyDer::encode_sequence_in_place(&mut buf);
754 assert_eq!(
755 buf.len(),
756 content_len + header_len,
757 "content_len {content_len:#x}: wrong total length"
758 );
759 assert_eq!(buf[0], 0x30, "content_len {content_len:#x}: SEQUENCE tag");
760 match header_len {
761 2 => assert_eq!(buf[1] as usize, content_len, "short-form length"),
762 3 => {
763 assert_eq!(buf[1], 0x81, "content_len {content_len:#x}: 0x81 form");
764 assert_eq!(buf[2] as usize, content_len);
765 }
766 4 => {
767 assert_eq!(buf[1], 0x82, "content_len {content_len:#x}: 0x82 form");
768 assert_eq!(((buf[2] as usize) << 8) | buf[3] as usize, content_len);
769 }
770 _ => unreachable!(),
771 }
772 assert!(buf[header_len..].iter().all(|&b| b == 0xCB));
774 }
775 }
776
777 #[test]
781 fn der_integer_encoding_edge_cases() {
782 let mut buf = Vec::new();
783 RsaPublicKeyDer::encode_integer(&mut buf, &[0u8; 8]);
784 assert_eq!(buf, vec![0x02, 0x01, 0x00]);
785
786 buf.clear();
787 buf.clear();
788 RsaPublicKeyDer::encode_integer(&mut buf, &[0x80, 0x00]);
790 assert_eq!(buf, vec![0x02, 0x03, 0x00, 0x80, 0x00]);
791
792 buf.clear();
793 RsaPublicKeyDer::encode_integer(&mut buf, &[0x00, 0x00, 0x7F]);
794 assert_eq!(buf, vec![0x02, 0x01, 0x7F]);
795
796 buf.clear();
797 RsaPublicKeyDer::encode_integer(&mut buf, &[]);
800 assert_eq!(buf, vec![0x02, 0x00]);
801
802 buf.clear();
803 RsaPublicKeyDer::encode_integer(&mut buf, &[0x01u8; 0x80]);
806 assert_eq!(&buf[..3], &[0x02, 0x81, 0x80]);
807 assert_eq!(buf.len(), 3 + 0x80);
808 assert!(buf[3..].iter().all(|&b| b == 0x01));
809 }
810
811 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=";
816
817 fn der_tlv(i: &[u8]) -> (&[u8], &[u8]) {
819 let l0 = *i.get(1).unwrap();
820 let (header, len) = if l0 < 0x80 {
821 (2usize, l0 as usize)
822 } else {
823 let n = (l0 & 0x7F) as usize;
824 let mut len = 0usize;
825 for b in &i[2..2 + n] {
826 len = (len << 8) | *b as usize;
827 }
828 (2 + n, len)
829 };
830 (&i[header..header + len], &i[header + len..])
831 }
832
833 pub(crate) fn rsa_test_material() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
835 use base64::Engine as _;
836 let pkcs8 = base64::engine::general_purpose::STANDARD
837 .decode(RSA_PKCS8_B64)
838 .unwrap();
839 let (info, _) = der_tlv(&pkcs8);
842 let (_version, rest) = der_tlv(info);
843 let (_algorithm, rest) = der_tlv(rest);
844 let (octet, _) = der_tlv(rest);
845 let (rsa_key, _) = der_tlv(octet);
846 let (_version, rest) = der_tlv(rsa_key);
847 let (n, rest) = der_tlv(rest);
848 let (e, _) = der_tlv(rest);
849 (n.to_vec(), e.to_vec(), pkcs8)
850 }
851
852 pub(crate) fn rsa_test_spki() -> Vec<u8> {
855 let (n, e, _) = rsa_test_material();
856 RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap()
857 }
858
859 fn cose_key_from(entries: Vec<(i64, ciborium::Value)>) -> Vec<u8> {
860 use ciborium::Value;
861 let map: Vec<(Value, Value)> = entries
862 .into_iter()
863 .map(|(k, v)| (Value::Integer(k.into()), v))
864 .collect();
865 let mut buf = Vec::new();
866 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
867 buf
868 }
869
870 #[test]
871 fn parse_cose_key_garbage_cbor_is_error() {
872 let result = parse_cose_key(&[0xFF, 0xFF, 0xFF, 0xFF]);
874 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
875 }
876
877 #[test]
878 fn parse_cose_key_ec2_missing_crv() {
879 let buf = cose_key_from(vec![
880 (1, ciborium::Value::Integer(2.into())),
881 (3, ciborium::Value::Integer((-7).into())),
882 (-2, ciborium::Value::Bytes(vec![0xAA; 32])),
883 (-3, ciborium::Value::Bytes(vec![0xBB; 32])),
884 ]);
885 assert!(matches!(
886 parse_cose_key(&buf),
887 Err(WebauthnError::VerificationFailed(_))
888 ));
889 }
890
891 #[test]
892 fn parse_cose_key_ec2_wrong_curve_is_unsupported() {
893 let buf = cose_key_from(vec![
894 (1, ciborium::Value::Integer(2.into())),
895 (3, ciborium::Value::Integer((-7).into())),
896 (-1, ciborium::Value::Integer(3.into())), (-2, ciborium::Value::Bytes(vec![0xAA; 48])),
898 (-3, ciborium::Value::Bytes(vec![0xBB; 48])),
899 ]);
900 assert!(matches!(
901 parse_cose_key(&buf),
902 Err(WebauthnError::UnsupportedAlgorithm(-7))
903 ));
904 }
905
906 #[test]
907 fn parse_cose_key_ec2_missing_coordinates() {
908 let kty_alg = vec![
909 (1, ciborium::Value::Integer(2.into())),
910 (3, ciborium::Value::Integer((-7).into())),
911 (-1, ciborium::Value::Integer(1.into())),
912 ];
913 let missing_x = cose_key_from(
914 [
915 kty_alg.clone(),
916 vec![(-3, ciborium::Value::Bytes(vec![0xBB; 32]))],
917 ]
918 .concat(),
919 );
920 assert!(matches!(
921 parse_cose_key(&missing_x),
922 Err(WebauthnError::VerificationFailed(_))
923 ));
924
925 let missing_y =
926 cose_key_from([kty_alg, vec![(-2, ciborium::Value::Bytes(vec![0xAA; 32]))]].concat());
927 assert!(matches!(
928 parse_cose_key(&missing_y),
929 Err(WebauthnError::VerificationFailed(_))
930 ));
931 }
932
933 #[test]
934 fn parse_cose_key_rsa_missing_modulus_or_exponent() {
935 let missing_n = cose_key_from(vec![
936 (1, ciborium::Value::Integer(3.into())),
937 (3, ciborium::Value::Integer((-257).into())),
938 (-2, ciborium::Value::Bytes(vec![0x01, 0x00, 0x01])),
939 ]);
940 assert!(matches!(
941 parse_cose_key(&missing_n),
942 Err(WebauthnError::VerificationFailed(_))
943 ));
944
945 let missing_e = cose_key_from(vec![
946 (1, ciborium::Value::Integer(3.into())),
947 (3, ciborium::Value::Integer((-257).into())),
948 (-1, ciborium::Value::Bytes(vec![0xAA; 256])),
949 ]);
950 assert!(matches!(
951 parse_cose_key(&missing_e),
952 Err(WebauthnError::VerificationFailed(_))
953 ));
954 }
955
956 #[test]
957 fn parse_cose_key_unknown_kty_is_error() {
958 let buf = cose_key_from(vec![
959 (1, ciborium::Value::Integer(99.into())),
960 (3, ciborium::Value::Integer((-7).into())),
961 ]);
962 assert!(matches!(
963 parse_cose_key(&buf),
964 Err(WebauthnError::VerificationFailed(_))
965 ));
966 }
967
968 #[test]
969 fn cbor_map_entries_rejects_non_integer_keys() {
970 use ciborium::Value;
971 let map = Value::Map(vec![(
972 Value::Text("kty".to_string()),
973 Value::Integer(2.into()),
974 )]);
975 assert!(cbor_map_entries(&map).is_none());
976 }
977
978 #[test]
979 fn verify_rs256_with_ec_key_is_rejected() {
980 let key = CosePublicKey::Ec2 {
981 x: vec![0xAA; 32],
982 y: vec![0xBB; 32],
983 };
984 assert!(matches!(
985 verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]),
986 Err(WebauthnError::VerificationFailed(_))
987 ));
988 }
989
990 #[test]
991 fn verify_cose_signature_unknown_alg_is_rejected() {
992 let key = CosePublicKey::Ec2 {
993 x: vec![0xAA; 32],
994 y: vec![0xBB; 32],
995 };
996 assert!(matches!(
997 verify_cose_signature(-47, &key, b"data", &[0u8; 64]),
998 Err(WebauthnError::UnsupportedAlgorithm(-47))
999 ));
1000 }
1001
1002 #[test]
1003 fn alg_to_name_covers_known_and_unknown() {
1004 assert_eq!(alg_to_name(COSE_ALG_ES256), "ES256");
1005 assert_eq!(alg_to_name(COSE_ALG_ES384), "ES384");
1006 assert_eq!(alg_to_name(COSE_ALG_EDDSA), "EdDSA");
1007 assert_eq!(alg_to_name(COSE_ALG_RS256), "RS256");
1008 assert_eq!(alg_to_name(0), "unknown");
1009 }
1010
1011 #[test]
1016 fn verify_es384_real_signature_roundtrip() {
1017 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
1018
1019 let rng = ring::rand::SystemRandom::new();
1020 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
1021 let key_pair =
1022 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
1023 .unwrap();
1024
1025 let pub_bytes = key_pair.public_key().as_ref();
1026 assert_eq!(pub_bytes.len(), 97); let x = pub_bytes[1..49].to_vec();
1028 let y = pub_bytes[49..97].to_vec();
1029 let key = CosePublicKey::Ec2 { x, y };
1030
1031 let message = b"webauthn-kit es384 vector";
1032 let signature = key_pair.sign(&rng, message).unwrap();
1033 assert_eq!(signature.as_ref().len(), 96); assert!(verify_cose_signature(COSE_ALG_ES384, &key, message, signature.as_ref()).is_ok());
1036
1037 let mut tampered = *message;
1038 tampered[0] ^= 0x01;
1039 assert!(matches!(
1040 verify_cose_signature(COSE_ALG_ES384, &key, &tampered, signature.as_ref()),
1041 Err(WebauthnError::SignatureVerificationFailed)
1042 ));
1043
1044 assert!(matches!(
1046 verify_cose_signature(COSE_ALG_ES256, &key, message, signature.as_ref()),
1047 Err(WebauthnError::SignatureVerificationFailed)
1048 ));
1049 }
1050
1051 #[test]
1054 fn verify_eddsa_real_signature_roundtrip() {
1055 use ring::signature::{Ed25519KeyPair, KeyPair as _};
1056
1057 let rng = ring::rand::SystemRandom::new();
1058 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1059 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1060
1061 let public = key_pair.public_key().as_ref().to_vec();
1062 assert_eq!(public.len(), 32);
1063 let key = CosePublicKey::Okp { id: public };
1064
1065 let message = b"webauthn-kit ed25519 vector";
1066 let signature = key_pair.sign(message);
1067 assert_eq!(signature.as_ref().len(), 64);
1068
1069 assert!(verify_cose_signature(COSE_ALG_EDDSA, &key, message, signature.as_ref()).is_ok());
1070
1071 let mut tampered = *message;
1072 tampered[0] ^= 0x01;
1073 assert!(matches!(
1074 verify_cose_signature(COSE_ALG_EDDSA, &key, &tampered, signature.as_ref()),
1075 Err(WebauthnError::SignatureVerificationFailed)
1076 ));
1077 }
1078
1079 #[test]
1082 fn parse_cose_p384_key() {
1083 let x = vec![0xCC; 48];
1084 let y = vec![0xDD; 48];
1085 let cose_key = build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES384, &x, &y);
1086
1087 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1088 assert_eq!(alg, COSE_ALG_ES384);
1089 match key {
1090 CosePublicKey::Ec2 { x: kx, y: ky } => {
1091 assert_eq!(kx, x);
1092 assert_eq!(ky, y);
1093 }
1094 _ => panic!("Expected EC2 key"),
1095 }
1096 }
1097
1098 #[test]
1100 fn parse_cose_okp_ed25519_key() {
1101 let id = vec![0x42u8; 32];
1102 let cose_key = build_cose_okp_key(&id);
1103
1104 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1105 assert_eq!(alg, COSE_ALG_EDDSA);
1106 match key {
1107 CosePublicKey::Okp { id: kid } => assert_eq!(kid, id),
1108 _ => panic!("Expected OKP key"),
1109 }
1110 }
1111
1112 #[test]
1118 fn parse_p256_key_claiming_es384_rejected() {
1119 let cose_key =
1120 build_cose_ec2_key_alg(COSE_CRV_P256, COSE_ALG_ES384, &[0xAA; 32], &[0xBB; 32]);
1121 assert!(matches!(
1122 parse_cose_key(&cose_key),
1123 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1124 ));
1125 }
1126
1127 #[test]
1129 fn parse_p384_key_claiming_es256_rejected() {
1130 let cose_key =
1131 build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES256, &[0xAA; 48], &[0xBB; 48]);
1132 assert!(matches!(
1133 parse_cose_key(&cose_key),
1134 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1135 ));
1136 }
1137
1138 #[test]
1141 fn parse_okp_claiming_other_algorithms_rejected() {
1142 let bad_alg = cose_key_from(vec![
1144 (1, ciborium::Value::Integer(1.into())),
1145 (3, ciborium::Value::Integer((-7).into())),
1146 (-1, ciborium::Value::Integer(6.into())),
1147 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1148 ]);
1149 assert!(matches!(
1150 parse_cose_key(&bad_alg),
1151 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1152 ));
1153
1154 let bad_crv = cose_key_from(vec![
1156 (1, ciborium::Value::Integer(1.into())),
1157 (3, ciborium::Value::Integer((-8).into())),
1158 (-1, ciborium::Value::Integer(4.into())),
1159 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1160 ]);
1161 assert!(matches!(
1162 parse_cose_key(&bad_crv),
1163 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_EDDSA))
1164 ));
1165 }
1166
1167 #[test]
1170 fn verify_okp_ec2_type_mismatches_rejected() {
1171 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1172 assert!(matches!(
1173 verify_cose_signature(COSE_ALG_ES256, &okp, b"data", &[0u8; 64]),
1174 Err(WebauthnError::VerificationFailed(_))
1175 ));
1176
1177 let ec2 = CosePublicKey::Ec2 {
1178 x: vec![0xAA; 32],
1179 y: vec![0xBB; 32],
1180 };
1181 assert!(matches!(
1182 verify_cose_signature(COSE_ALG_EDDSA, &ec2, b"data", &[0u8; 64]),
1183 Err(WebauthnError::VerificationFailed(_))
1184 ));
1185 }
1186
1187 #[test]
1190 fn verify_es384_short_point_fails_closed() {
1191 let short = CosePublicKey::Ec2 {
1192 x: vec![0xAA; 32], y: vec![0xBB; 32],
1194 };
1195 assert!(matches!(
1196 verify_cose_signature(COSE_ALG_ES384, &short, b"data", &[0u8; 96]),
1197 Err(WebauthnError::SignatureVerificationFailed)
1198 ));
1199 }
1200
1201 #[test]
1204 fn verify_eddsa_wrong_key_length_fails_closed() {
1205 let long = CosePublicKey::Okp { id: vec![0x42; 31] };
1206 assert!(matches!(
1207 verify_cose_signature(COSE_ALG_EDDSA, &long, b"data", &[0u8; 64]),
1208 Err(WebauthnError::SignatureVerificationFailed)
1209 ));
1210 let short_sig = CosePublicKey::Okp { id: vec![0x42; 32] };
1211 assert!(matches!(
1212 verify_cose_signature(COSE_ALG_EDDSA, &short_sig, b"data", &[0u8; 63]),
1213 Err(WebauthnError::SignatureVerificationFailed)
1214 ));
1215 }
1216
1217 #[test]
1221 fn cose_key_unknown_labels_and_okp_error_paths() {
1222 let with_extra = cose_key_from(vec![
1224 (1, ciborium::Value::Integer(1.into())),
1225 (3, ciborium::Value::Integer((-8).into())),
1226 (-1, ciborium::Value::Integer(6.into())),
1227 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1228 (99, ciborium::Value::Text("ignored".to_string())),
1229 ]);
1230 let (alg, _) = parse_cose_key(&with_extra).unwrap();
1231 assert_eq!(alg, COSE_ALG_EDDSA);
1232
1233 let no_bytes = cose_key_from(vec![
1235 (1, ciborium::Value::Integer(1.into())),
1236 (3, ciborium::Value::Integer((-8).into())),
1237 (-1, ciborium::Value::Integer(6.into())),
1238 ]);
1239 assert!(matches!(
1240 parse_cose_key(&no_bytes),
1241 Err(WebauthnError::VerificationFailed(_))
1242 ));
1243
1244 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1246 assert!(matches!(
1247 verify_cose_signature(COSE_ALG_ES384, &okp, b"data", &[0u8; 96]),
1248 Err(WebauthnError::VerificationFailed(_))
1249 ));
1250 }
1251
1252 #[test]
1255 fn verify_rs256_real_signature_roundtrip() {
1256 use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
1257
1258 let (n, e, pkcs8) = rsa_test_material();
1259 let rsa = RsaKeyPair::from_pkcs8(&pkcs8).unwrap();
1260 let modulus_len = rsa.public().modulus_len();
1261 assert_eq!(modulus_len, 256);
1262
1263 let rng = ring::rand::SystemRandom::new();
1265 let message = b"webauthn-kit rs256 vector";
1266 let mut signature = vec![0u8; modulus_len];
1267 rsa.sign(&RSA_PKCS1_SHA256, &rng, message, &mut signature)
1268 .unwrap();
1269
1270 let key = CosePublicKey::Rsa { n, e };
1271 assert!(verify_cose_signature(COSE_ALG_RS256, &key, message, &signature).is_ok());
1272
1273 let mut tampered = *message;
1275 tampered[0] ^= 0x01;
1276 assert!(matches!(
1277 verify_cose_signature(COSE_ALG_RS256, &key, &tampered, &signature),
1278 Err(WebauthnError::SignatureVerificationFailed)
1279 ));
1280 }
1281
1282 #[test]
1286 fn der_long_form_length_encoding() {
1287 let n = [0x77u8; 128];
1288 let e = [0x01, 0x00, 0x01];
1289 let der = RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap();
1290
1291 assert_eq!(der[0], 0x30);
1293 assert_eq!(der[1], 0x81);
1294 assert_eq!(der[2] as usize + 3, der.len());
1295 assert!(der.windows(3).any(|w| w == [0x02, 0x81, 0x80]));
1297 }
1298}