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 = 2;
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(2.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(2.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(2.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(2.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(2.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]
748 fn der_integer_encoding_edge_cases() {
749 let mut buf = Vec::new();
750 RsaPublicKeyDer::encode_integer(&mut buf, &[0u8; 8]);
751 assert_eq!(buf, vec![0x02, 0x01, 0x00]);
752
753 buf.clear();
754 buf.clear();
755 RsaPublicKeyDer::encode_integer(&mut buf, &[0x80, 0x00]);
757 assert_eq!(buf, vec![0x02, 0x03, 0x00, 0x80, 0x00]);
758
759 buf.clear();
760 RsaPublicKeyDer::encode_integer(&mut buf, &[0x00, 0x00, 0x7F]);
761 assert_eq!(buf, vec![0x02, 0x01, 0x7F]);
762
763 buf.clear();
764 RsaPublicKeyDer::encode_integer(&mut buf, &[]);
767 assert_eq!(buf, vec![0x02, 0x00]);
768 }
769
770 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=";
775
776 fn der_tlv(i: &[u8]) -> (&[u8], &[u8]) {
778 let l0 = *i.get(1).unwrap();
779 let (header, len) = if l0 < 0x80 {
780 (2usize, l0 as usize)
781 } else {
782 let n = (l0 & 0x7F) as usize;
783 let mut len = 0usize;
784 for b in &i[2..2 + n] {
785 len = (len << 8) | *b as usize;
786 }
787 (2 + n, len)
788 };
789 (&i[header..header + len], &i[header + len..])
790 }
791
792 pub(crate) fn rsa_test_material() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
794 use base64::Engine as _;
795 let pkcs8 = base64::engine::general_purpose::STANDARD
796 .decode(RSA_PKCS8_B64)
797 .unwrap();
798 let (info, _) = der_tlv(&pkcs8);
801 let (_version, rest) = der_tlv(info);
802 let (_algorithm, rest) = der_tlv(rest);
803 let (octet, _) = der_tlv(rest);
804 let (rsa_key, _) = der_tlv(octet);
805 let (_version, rest) = der_tlv(rsa_key);
806 let (n, rest) = der_tlv(rest);
807 let (e, _) = der_tlv(rest);
808 (n.to_vec(), e.to_vec(), pkcs8)
809 }
810
811 pub(crate) fn rsa_test_spki() -> Vec<u8> {
814 let (n, e, _) = rsa_test_material();
815 RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap()
816 }
817
818 fn cose_key_from(entries: Vec<(i64, ciborium::Value)>) -> Vec<u8> {
819 use ciborium::Value;
820 let map: Vec<(Value, Value)> = entries
821 .into_iter()
822 .map(|(k, v)| (Value::Integer(k.into()), v))
823 .collect();
824 let mut buf = Vec::new();
825 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
826 buf
827 }
828
829 #[test]
830 fn parse_cose_key_garbage_cbor_is_error() {
831 let result = parse_cose_key(&[0xFF, 0xFF, 0xFF, 0xFF]);
833 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
834 }
835
836 #[test]
837 fn parse_cose_key_ec2_missing_crv() {
838 let buf = cose_key_from(vec![
839 (1, ciborium::Value::Integer(2.into())),
840 (2, ciborium::Value::Integer((-7).into())),
841 (-2, ciborium::Value::Bytes(vec![0xAA; 32])),
842 (-3, ciborium::Value::Bytes(vec![0xBB; 32])),
843 ]);
844 assert!(matches!(
845 parse_cose_key(&buf),
846 Err(WebauthnError::VerificationFailed(_))
847 ));
848 }
849
850 #[test]
851 fn parse_cose_key_ec2_wrong_curve_is_unsupported() {
852 let buf = cose_key_from(vec![
853 (1, ciborium::Value::Integer(2.into())),
854 (2, ciborium::Value::Integer((-7).into())),
855 (-1, ciborium::Value::Integer(3.into())), (-2, ciborium::Value::Bytes(vec![0xAA; 48])),
857 (-3, ciborium::Value::Bytes(vec![0xBB; 48])),
858 ]);
859 assert!(matches!(
860 parse_cose_key(&buf),
861 Err(WebauthnError::UnsupportedAlgorithm(-7))
862 ));
863 }
864
865 #[test]
866 fn parse_cose_key_ec2_missing_coordinates() {
867 let kty_alg = vec![
868 (1, ciborium::Value::Integer(2.into())),
869 (2, ciborium::Value::Integer((-7).into())),
870 (-1, ciborium::Value::Integer(1.into())),
871 ];
872 let missing_x = cose_key_from(
873 [
874 kty_alg.clone(),
875 vec![(-3, ciborium::Value::Bytes(vec![0xBB; 32]))],
876 ]
877 .concat(),
878 );
879 assert!(matches!(
880 parse_cose_key(&missing_x),
881 Err(WebauthnError::VerificationFailed(_))
882 ));
883
884 let missing_y =
885 cose_key_from([kty_alg, vec![(-2, ciborium::Value::Bytes(vec![0xAA; 32]))]].concat());
886 assert!(matches!(
887 parse_cose_key(&missing_y),
888 Err(WebauthnError::VerificationFailed(_))
889 ));
890 }
891
892 #[test]
893 fn parse_cose_key_rsa_missing_modulus_or_exponent() {
894 let missing_n = cose_key_from(vec![
895 (1, ciborium::Value::Integer(3.into())),
896 (2, ciborium::Value::Integer((-257).into())),
897 (-2, ciborium::Value::Bytes(vec![0x01, 0x00, 0x01])),
898 ]);
899 assert!(matches!(
900 parse_cose_key(&missing_n),
901 Err(WebauthnError::VerificationFailed(_))
902 ));
903
904 let missing_e = cose_key_from(vec![
905 (1, ciborium::Value::Integer(3.into())),
906 (2, ciborium::Value::Integer((-257).into())),
907 (-1, ciborium::Value::Bytes(vec![0xAA; 256])),
908 ]);
909 assert!(matches!(
910 parse_cose_key(&missing_e),
911 Err(WebauthnError::VerificationFailed(_))
912 ));
913 }
914
915 #[test]
916 fn parse_cose_key_unknown_kty_is_error() {
917 let buf = cose_key_from(vec![
918 (1, ciborium::Value::Integer(99.into())),
919 (2, ciborium::Value::Integer((-7).into())),
920 ]);
921 assert!(matches!(
922 parse_cose_key(&buf),
923 Err(WebauthnError::VerificationFailed(_))
924 ));
925 }
926
927 #[test]
928 fn cbor_map_entries_rejects_non_integer_keys() {
929 use ciborium::Value;
930 let map = Value::Map(vec![(
931 Value::Text("kty".to_string()),
932 Value::Integer(2.into()),
933 )]);
934 assert!(cbor_map_entries(&map).is_none());
935 }
936
937 #[test]
938 fn verify_rs256_with_ec_key_is_rejected() {
939 let key = CosePublicKey::Ec2 {
940 x: vec![0xAA; 32],
941 y: vec![0xBB; 32],
942 };
943 assert!(matches!(
944 verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]),
945 Err(WebauthnError::VerificationFailed(_))
946 ));
947 }
948
949 #[test]
950 fn verify_cose_signature_unknown_alg_is_rejected() {
951 let key = CosePublicKey::Ec2 {
952 x: vec![0xAA; 32],
953 y: vec![0xBB; 32],
954 };
955 assert!(matches!(
956 verify_cose_signature(-47, &key, b"data", &[0u8; 64]),
957 Err(WebauthnError::UnsupportedAlgorithm(-47))
958 ));
959 }
960
961 #[test]
962 fn alg_to_name_covers_known_and_unknown() {
963 assert_eq!(alg_to_name(COSE_ALG_ES256), "ES256");
964 assert_eq!(alg_to_name(COSE_ALG_ES384), "ES384");
965 assert_eq!(alg_to_name(COSE_ALG_EDDSA), "EdDSA");
966 assert_eq!(alg_to_name(COSE_ALG_RS256), "RS256");
967 assert_eq!(alg_to_name(0), "unknown");
968 }
969
970 #[test]
975 fn verify_es384_real_signature_roundtrip() {
976 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P384_SHA384_FIXED_SIGNING};
977
978 let rng = ring::rand::SystemRandom::new();
979 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, &rng).unwrap();
980 let key_pair =
981 EcdsaKeyPair::from_pkcs8(&ECDSA_P384_SHA384_FIXED_SIGNING, pkcs8.as_ref(), &rng)
982 .unwrap();
983
984 let pub_bytes = key_pair.public_key().as_ref();
985 assert_eq!(pub_bytes.len(), 97); let x = pub_bytes[1..49].to_vec();
987 let y = pub_bytes[49..97].to_vec();
988 let key = CosePublicKey::Ec2 { x, y };
989
990 let message = b"webauthn-kit es384 vector";
991 let signature = key_pair.sign(&rng, message).unwrap();
992 assert_eq!(signature.as_ref().len(), 96); assert!(verify_cose_signature(COSE_ALG_ES384, &key, message, signature.as_ref()).is_ok());
995
996 let mut tampered = *message;
997 tampered[0] ^= 0x01;
998 assert!(matches!(
999 verify_cose_signature(COSE_ALG_ES384, &key, &tampered, signature.as_ref()),
1000 Err(WebauthnError::SignatureVerificationFailed)
1001 ));
1002
1003 assert!(matches!(
1005 verify_cose_signature(COSE_ALG_ES256, &key, message, signature.as_ref()),
1006 Err(WebauthnError::SignatureVerificationFailed)
1007 ));
1008 }
1009
1010 #[test]
1013 fn verify_eddsa_real_signature_roundtrip() {
1014 use ring::signature::{Ed25519KeyPair, KeyPair as _};
1015
1016 let rng = ring::rand::SystemRandom::new();
1017 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1018 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1019
1020 let public = key_pair.public_key().as_ref().to_vec();
1021 assert_eq!(public.len(), 32);
1022 let key = CosePublicKey::Okp { id: public };
1023
1024 let message = b"webauthn-kit ed25519 vector";
1025 let signature = key_pair.sign(message);
1026 assert_eq!(signature.as_ref().len(), 64);
1027
1028 assert!(verify_cose_signature(COSE_ALG_EDDSA, &key, message, signature.as_ref()).is_ok());
1029
1030 let mut tampered = *message;
1031 tampered[0] ^= 0x01;
1032 assert!(matches!(
1033 verify_cose_signature(COSE_ALG_EDDSA, &key, &tampered, signature.as_ref()),
1034 Err(WebauthnError::SignatureVerificationFailed)
1035 ));
1036 }
1037
1038 #[test]
1041 fn parse_cose_p384_key() {
1042 let x = vec![0xCC; 48];
1043 let y = vec![0xDD; 48];
1044 let cose_key = build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES384, &x, &y);
1045
1046 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1047 assert_eq!(alg, COSE_ALG_ES384);
1048 match key {
1049 CosePublicKey::Ec2 { x: kx, y: ky } => {
1050 assert_eq!(kx, x);
1051 assert_eq!(ky, y);
1052 }
1053 _ => panic!("Expected EC2 key"),
1054 }
1055 }
1056
1057 #[test]
1059 fn parse_cose_okp_ed25519_key() {
1060 let id = vec![0x42u8; 32];
1061 let cose_key = build_cose_okp_key(&id);
1062
1063 let (alg, key) = parse_cose_key(&cose_key).unwrap();
1064 assert_eq!(alg, COSE_ALG_EDDSA);
1065 match key {
1066 CosePublicKey::Okp { id: kid } => assert_eq!(kid, id),
1067 _ => panic!("Expected OKP key"),
1068 }
1069 }
1070
1071 #[test]
1077 fn parse_p256_key_claiming_es384_rejected() {
1078 let cose_key =
1079 build_cose_ec2_key_alg(COSE_CRV_P256, COSE_ALG_ES384, &[0xAA; 32], &[0xBB; 32]);
1080 assert!(matches!(
1081 parse_cose_key(&cose_key),
1082 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES384))
1083 ));
1084 }
1085
1086 #[test]
1088 fn parse_p384_key_claiming_es256_rejected() {
1089 let cose_key =
1090 build_cose_ec2_key_alg(COSE_CRV_P384, COSE_ALG_ES256, &[0xAA; 48], &[0xBB; 48]);
1091 assert!(matches!(
1092 parse_cose_key(&cose_key),
1093 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1094 ));
1095 }
1096
1097 #[test]
1100 fn parse_okp_claiming_other_algorithms_rejected() {
1101 let bad_alg = cose_key_from(vec![
1103 (1, ciborium::Value::Integer(1.into())),
1104 (2, ciborium::Value::Integer((-7).into())),
1105 (-1, ciborium::Value::Integer(6.into())),
1106 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1107 ]);
1108 assert!(matches!(
1109 parse_cose_key(&bad_alg),
1110 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_ES256))
1111 ));
1112
1113 let bad_crv = cose_key_from(vec![
1115 (1, ciborium::Value::Integer(1.into())),
1116 (2, ciborium::Value::Integer((-8).into())),
1117 (-1, ciborium::Value::Integer(4.into())),
1118 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1119 ]);
1120 assert!(matches!(
1121 parse_cose_key(&bad_crv),
1122 Err(WebauthnError::UnsupportedAlgorithm(COSE_ALG_EDDSA))
1123 ));
1124 }
1125
1126 #[test]
1129 fn verify_okp_ec2_type_mismatches_rejected() {
1130 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1131 assert!(matches!(
1132 verify_cose_signature(COSE_ALG_ES256, &okp, b"data", &[0u8; 64]),
1133 Err(WebauthnError::VerificationFailed(_))
1134 ));
1135
1136 let ec2 = CosePublicKey::Ec2 {
1137 x: vec![0xAA; 32],
1138 y: vec![0xBB; 32],
1139 };
1140 assert!(matches!(
1141 verify_cose_signature(COSE_ALG_EDDSA, &ec2, b"data", &[0u8; 64]),
1142 Err(WebauthnError::VerificationFailed(_))
1143 ));
1144 }
1145
1146 #[test]
1149 fn verify_es384_short_point_fails_closed() {
1150 let short = CosePublicKey::Ec2 {
1151 x: vec![0xAA; 32], y: vec![0xBB; 32],
1153 };
1154 assert!(matches!(
1155 verify_cose_signature(COSE_ALG_ES384, &short, b"data", &[0u8; 96]),
1156 Err(WebauthnError::SignatureVerificationFailed)
1157 ));
1158 }
1159
1160 #[test]
1163 fn verify_eddsa_wrong_key_length_fails_closed() {
1164 let long = CosePublicKey::Okp { id: vec![0x42; 31] };
1165 assert!(matches!(
1166 verify_cose_signature(COSE_ALG_EDDSA, &long, b"data", &[0u8; 64]),
1167 Err(WebauthnError::SignatureVerificationFailed)
1168 ));
1169 let short_sig = CosePublicKey::Okp { id: vec![0x42; 32] };
1170 assert!(matches!(
1171 verify_cose_signature(COSE_ALG_EDDSA, &short_sig, b"data", &[0u8; 63]),
1172 Err(WebauthnError::SignatureVerificationFailed)
1173 ));
1174 }
1175
1176 #[test]
1180 fn cose_key_unknown_labels_and_okp_error_paths() {
1181 let with_extra = cose_key_from(vec![
1183 (1, ciborium::Value::Integer(1.into())),
1184 (2, ciborium::Value::Integer((-8).into())),
1185 (-1, ciborium::Value::Integer(6.into())),
1186 (-2, ciborium::Value::Bytes(vec![0x42; 32])),
1187 (99, ciborium::Value::Text("ignored".to_string())),
1188 ]);
1189 let (alg, _) = parse_cose_key(&with_extra).unwrap();
1190 assert_eq!(alg, COSE_ALG_EDDSA);
1191
1192 let no_bytes = cose_key_from(vec![
1194 (1, ciborium::Value::Integer(1.into())),
1195 (2, ciborium::Value::Integer((-8).into())),
1196 (-1, ciborium::Value::Integer(6.into())),
1197 ]);
1198 assert!(matches!(
1199 parse_cose_key(&no_bytes),
1200 Err(WebauthnError::VerificationFailed(_))
1201 ));
1202
1203 let okp = CosePublicKey::Okp { id: vec![0x42; 32] };
1205 assert!(matches!(
1206 verify_cose_signature(COSE_ALG_ES384, &okp, b"data", &[0u8; 96]),
1207 Err(WebauthnError::VerificationFailed(_))
1208 ));
1209 }
1210
1211 #[test]
1214 fn verify_rs256_real_signature_roundtrip() {
1215 use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
1216
1217 let (n, e, pkcs8) = rsa_test_material();
1218 let rsa = RsaKeyPair::from_pkcs8(&pkcs8).unwrap();
1219 let modulus_len = rsa.public().modulus_len();
1220 assert_eq!(modulus_len, 256);
1221
1222 let rng = ring::rand::SystemRandom::new();
1224 let message = b"webauthn-kit rs256 vector";
1225 let mut signature = vec![0u8; modulus_len];
1226 rsa.sign(&RSA_PKCS1_SHA256, &rng, message, &mut signature)
1227 .unwrap();
1228
1229 let key = CosePublicKey::Rsa { n, e };
1230 assert!(verify_cose_signature(COSE_ALG_RS256, &key, message, &signature).is_ok());
1231
1232 let mut tampered = *message;
1234 tampered[0] ^= 0x01;
1235 assert!(matches!(
1236 verify_cose_signature(COSE_ALG_RS256, &key, &tampered, &signature),
1237 Err(WebauthnError::SignatureVerificationFailed)
1238 ));
1239 }
1240
1241 #[test]
1245 fn der_long_form_length_encoding() {
1246 let n = [0x77u8; 128];
1247 let e = [0x01, 0x00, 0x01];
1248 let der = RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap();
1249
1250 assert_eq!(der[0], 0x30);
1252 assert_eq!(der[1], 0x81);
1253 assert_eq!(der[2] as usize + 3, der.len());
1254 assert!(der.windows(3).any(|w| w == [0x02, 0x81, 0x80]));
1256 }
1257}