1use crate::error::WebauthnError;
14
15#[derive(Debug, Clone)]
17pub enum CosePublicKey {
18 Ec2 {
20 x: Vec<u8>,
22 y: Vec<u8>,
24 },
25 Rsa {
27 n: Vec<u8>,
29 e: Vec<u8>,
31 },
32}
33
34pub const COSE_KTY_OKP: i64 = 1;
36pub const COSE_KTY_EC2: i64 = 2;
38pub const COSE_KTY_RSA: i64 = 3;
40
41pub const COSE_ALG_ES256: i32 = -7;
43pub const COSE_ALG_RS256: i32 = -257;
45
46const COSE_KEY_KTY: i64 = 1;
48const COSE_KEY_ALG: i64 = 2;
50const COSE_KEY_CRV_N: i64 = -1;
52const COSE_KEY_X_E: i64 = -2;
54const COSE_KEY_Y: i64 = -3;
56
57const COSE_CRV_P256: i64 = 1;
59
60fn cbor_i64(val: &ciborium::Value) -> Option<i64> {
62 use ciborium::Value;
63 match val {
64 Value::Integer(i) => (*i).try_into().ok(),
65 _ => None,
66 }
67}
68
69pub fn cbor_bytes(val: &ciborium::Value) -> Option<Vec<u8>> {
78 use ciborium::Value;
79 match val {
80 Value::Bytes(b) => Some(b.clone()),
81 Value::Text(t) => Some(t.as_bytes().to_vec()),
82 _ => None,
83 }
84}
85
86pub fn cbor_map_entries(val: &ciborium::Value) -> Option<Vec<(i64, ciborium::Value)>> {
92 use ciborium::Value;
93 match val {
94 Value::Map(entries) => {
95 let mut result = Vec::with_capacity(entries.len());
96 for (k, v) in entries {
97 let key = cbor_i64(k)?;
98 result.push((key, v.clone()));
99 }
100 Some(result)
101 }
102 _ => None,
103 }
104}
105
106pub fn parse_cose_key(cose_bytes: &[u8]) -> Result<(i32, CosePublicKey), WebauthnError> {
129 use ciborium::Value;
130
131 let key_val: Value = ciborium::de::from_reader(cose_bytes).map_err(|e| {
132 WebauthnError::VerificationFailed(format!("COSE key CBOR parse error: {e}"))
133 })?;
134
135 let entries = cbor_map_entries(&key_val).ok_or_else(|| {
136 WebauthnError::VerificationFailed("COSE key is not a CBOR map".to_string())
137 })?;
138
139 let mut kty: Option<i64> = None;
140 let mut alg: Option<i32> = None;
141 let mut crv: Option<i64> = None;
142 let mut x: Option<Vec<u8>> = None;
143 let mut y: Option<Vec<u8>> = None;
144 let mut n: Option<Vec<u8>> = None;
145 let mut e: Option<Vec<u8>> = None;
146
147 for (label, val) in &entries {
148 match *label {
149 COSE_KEY_KTY => kty = cbor_i64(val),
150 COSE_KEY_ALG => alg = cbor_i64(val).map(|v| v as i32),
151 COSE_KEY_CRV_N => match kty {
152 Some(COSE_KTY_RSA) => n = cbor_bytes(val),
153 _ => crv = cbor_i64(val),
154 },
155 COSE_KEY_X_E => match kty {
156 Some(COSE_KTY_RSA) => e = cbor_bytes(val),
157 _ => x = cbor_bytes(val),
158 },
159 COSE_KEY_Y => y = cbor_bytes(val),
160 _ => {}
161 }
162 }
163
164 let kty =
165 kty.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'kty'".to_string()))?;
166 let alg =
167 alg.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'alg'".to_string()))?;
168
169 match kty {
170 COSE_KTY_EC2 => {
171 let crv = crv.ok_or_else(|| {
172 WebauthnError::VerificationFailed("EC2 key missing 'crv'".to_string())
173 })?;
174 if crv != COSE_CRV_P256 {
175 return Err(WebauthnError::UnsupportedAlgorithm(alg));
176 }
177 let x = x.ok_or_else(|| {
178 WebauthnError::VerificationFailed("EC2 key missing 'x'".to_string())
179 })?;
180 let y = y.ok_or_else(|| {
181 WebauthnError::VerificationFailed("EC2 key missing 'y'".to_string())
182 })?;
183 Ok((alg, CosePublicKey::Ec2 { x, y }))
184 }
185 COSE_KTY_RSA => {
186 let n = n.ok_or_else(|| {
187 WebauthnError::VerificationFailed("RSA key missing 'n'".to_string())
188 })?;
189 let e = e.ok_or_else(|| {
190 WebauthnError::VerificationFailed("RSA key missing 'e'".to_string())
191 })?;
192 Ok((alg, CosePublicKey::Rsa { n, e }))
193 }
194 COSE_KTY_OKP => Err(WebauthnError::UnsupportedAlgorithm(alg)),
195 other => Err(WebauthnError::VerificationFailed(format!(
196 "Unsupported COSE key type: {other}"
197 ))),
198 }
199}
200
201pub fn verify_cose_signature(
219 alg: i32,
220 public_key: &CosePublicKey,
221 signed_data: &[u8],
222 signature: &[u8],
223) -> Result<(), WebauthnError> {
224 use ring::signature;
225
226 match alg {
227 COSE_ALG_ES256 => {
228 let CosePublicKey::Ec2 { x, y } = public_key else {
229 return Err(WebauthnError::VerificationFailed(
230 "EC2 key expected for ES256".to_string(),
231 ));
232 };
233
234 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
235 public_key_bytes.push(0x04);
236 public_key_bytes.extend_from_slice(x);
237 public_key_bytes.extend_from_slice(y);
238
239 let public_key = signature::UnparsedPublicKey::new(
240 &signature::ECDSA_P256_SHA256_FIXED,
241 &public_key_bytes,
242 );
243 public_key
244 .verify(signed_data, signature)
245 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
246 Ok(())
247 }
248 COSE_ALG_RS256 => {
249 let CosePublicKey::Rsa { n, e } = public_key else {
250 return Err(WebauthnError::VerificationFailed(
251 "RSA key expected for RS256".to_string(),
252 ));
253 };
254
255 let rsa_public_key = RsaPublicKeyDer { n, e };
256 let der_bytes = rsa_public_key.to_der()?;
257
258 let public_key = signature::UnparsedPublicKey::new(
259 &signature::RSA_PKCS1_2048_8192_SHA256,
260 &der_bytes,
261 );
262 public_key
263 .verify(signed_data, signature)
264 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
265 Ok(())
266 }
267 other => Err(WebauthnError::UnsupportedAlgorithm(other)),
268 }
269}
270
271struct RsaPublicKeyDer<'a> {
274 n: &'a [u8],
275 e: &'a [u8],
276}
277
278impl RsaPublicKeyDer<'_> {
279 fn to_der(&self) -> Result<Vec<u8>, WebauthnError> {
286 let mut der = Vec::new();
287 Self::encode_integer(&mut der, self.n);
288 Self::encode_integer(&mut der, self.e);
289 Self::encode_sequence_in_place(&mut der);
290 Ok(der)
291 }
292
293 fn encode_integer(buf: &mut Vec<u8>, value: &[u8]) {
299 let start = value
300 .iter()
301 .position(|&b| b != 0)
302 .unwrap_or(value.len().saturating_sub(1));
303 let v = value.get(start..).unwrap_or(&[]);
304 if v.first().is_some_and(|&b| b & 0x80 != 0) {
305 buf.push(0x02);
306 Self::encode_length(buf, v.len() + 1);
307 buf.push(0x00);
308 buf.extend_from_slice(v);
309 } else {
310 buf.push(0x02);
311 Self::encode_length(buf, v.len());
312 buf.extend_from_slice(v);
313 }
314 }
315
316 fn encode_sequence_in_place(buf: &mut Vec<u8>) {
318 let content_len = buf.len();
319 let header_len = if content_len < 0x80 {
320 2
321 } else if content_len < 0x100 {
322 3
323 } else {
324 4
325 };
326 buf.resize(content_len + header_len, 0);
327 buf.copy_within(..content_len, header_len);
328 if let Some(tag) = buf.first_mut() {
329 *tag = 0x30;
330 }
331 Self::encode_length_at(buf, 1, content_len);
332 }
333
334 fn encode_length(buf: &mut Vec<u8>, len: usize) {
336 if len < 0x80 {
337 buf.push(len as u8);
338 } else if len < 0x100 {
339 buf.push(0x81);
340 buf.push(len as u8);
341 } else {
342 buf.push(0x82);
343 buf.push((len >> 8) as u8);
344 buf.push((len & 0xFF) as u8);
345 }
346 }
347
348 #[allow(clippy::indexing_slicing)]
355 fn encode_length_at(buf: &mut [u8], offset: usize, len: usize) {
356 if len < 0x80 {
357 buf[offset] = len as u8;
358 } else if len < 0x100 {
359 buf[offset] = 0x81;
360 buf[offset + 1] = len as u8;
361 } else {
362 buf[offset] = 0x82;
363 buf[offset + 1] = (len >> 8) as u8;
364 buf[offset + 2] = (len & 0xFF) as u8;
365 }
366 }
367}
368
369pub fn alg_to_name(alg: i32) -> &'static str {
371 match alg {
372 COSE_ALG_ES256 => "ES256",
373 COSE_ALG_RS256 => "RS256",
374 _ => "unknown",
375 }
376}
377
378pub fn generate_challenge_bytes() -> Vec<u8> {
391 use ring::rand::SecureRandom;
392 let mut bytes = [0u8; 32];
393 #[allow(clippy::expect_used)]
396 ring::rand::SystemRandom::new()
397 .fill(&mut bytes)
398 .expect("OS CSPRNG unavailable; cannot generate secure challenge");
399 bytes.to_vec()
400}
401
402pub fn base64_encode_urlsafe(data: &[u8]) -> String {
405 use base64::Engine;
406 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
407}
408
409pub fn base64_decode_urlsafe(data: &str) -> Result<Vec<u8>, WebauthnError> {
417 use base64::Engine;
418 base64::engine::general_purpose::URL_SAFE_NO_PAD
419 .decode(data)
420 .map_err(|e| WebauthnError::VerificationFailed(format!("base64 decode error: {e}")))
421}
422
423#[allow(
427 clippy::unwrap_used,
428 clippy::expect_used,
429 clippy::indexing_slicing,
430 clippy::panic
431)]
432#[cfg(test)]
433pub(crate) mod tests {
434 use super::*;
435
436 pub(crate) fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
438 use ciborium::Value;
439 let map = vec![
440 (Value::Integer(1.into()), Value::Integer(2.into())),
441 (Value::Integer(2.into()), Value::Integer((-7).into())),
442 (Value::Integer((-1).into()), Value::Integer(1.into())),
443 (Value::Integer((-2).into()), Value::Bytes(x.to_vec())),
444 (Value::Integer((-3).into()), Value::Bytes(y.to_vec())),
445 ];
446 let mut buf = Vec::new();
447 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
448 buf
449 }
450
451 pub(crate) fn build_cose_rsa_key(n: &[u8], e: &[u8]) -> Vec<u8> {
453 use ciborium::Value;
454 let map = vec![
455 (Value::Integer(1.into()), Value::Integer(3.into())),
456 (Value::Integer(2.into()), Value::Integer((-257).into())),
457 (Value::Integer((-1).into()), Value::Bytes(n.to_vec())),
458 (Value::Integer((-2).into()), Value::Bytes(e.to_vec())),
459 ];
460 let mut buf = Vec::new();
461 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
462 buf
463 }
464
465 #[test]
466 fn test_parse_cose_ec2_key() {
467 let x = vec![0xAA; 32];
468 let y = vec![0xBB; 32];
469 let cose_key = build_cose_ec2_key(&x, &y);
470
471 let (alg, key) = parse_cose_key(&cose_key).unwrap();
472 assert_eq!(alg, -7);
473 match key {
474 CosePublicKey::Ec2 { x: kx, y: ky } => {
475 assert_eq!(kx, x);
476 assert_eq!(ky, y);
477 }
478 _ => panic!("Expected EC2 key"),
479 }
480 }
481
482 #[test]
483 fn test_parse_cose_rsa_key() {
484 let n = vec![0xAA; 256];
485 let e = vec![0x01, 0x00, 0x01];
486 let cose_key = build_cose_rsa_key(&n, &e);
487
488 let (alg, key) = parse_cose_key(&cose_key).unwrap();
489 assert_eq!(alg, -257);
490 match key {
491 CosePublicKey::Rsa { n: kn, e: ke } => {
492 assert_eq!(kn, n);
493 assert_eq!(ke, e);
494 }
495 _ => panic!("Expected RSA key"),
496 }
497 }
498
499 #[test]
500 fn test_parse_cose_key_not_a_map() {
501 let buf = [0x00u8]; let result = parse_cose_key(&buf);
503 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
504 }
505
506 #[test]
507 fn test_parse_cose_key_missing_kty() {
508 use ciborium::Value;
509 let map = vec![(Value::Integer(2.into()), Value::Integer((-7).into()))];
510 let mut buf = Vec::new();
511 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
512 let result = parse_cose_key(&buf);
513 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
514 }
515
516 #[test]
517 fn test_parse_cose_key_okp_unsupported() {
518 use ciborium::Value;
519 let map = vec![
520 (Value::Integer(1.into()), Value::Integer(1.into())),
521 (Value::Integer(2.into()), Value::Integer((-8).into())),
522 ];
523 let mut buf = Vec::new();
524 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
525 let result = parse_cose_key(&buf);
526 assert!(matches!(
527 result,
528 Err(WebauthnError::UnsupportedAlgorithm(-8))
529 ));
530 }
531
532 #[test]
533 fn test_verify_cose_signature_es256() {
534 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P256_SHA256_FIXED_SIGNING};
535
536 let rng = ring::rand::SystemRandom::new();
537 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
538 let key_pair =
539 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
540 .unwrap();
541
542 let public_key_bytes = key_pair.public_key().as_ref().to_vec();
543 let x = public_key_bytes[1..33].to_vec();
544 let y = public_key_bytes[33..65].to_vec();
545
546 let cose_key = CosePublicKey::Ec2 { x, y };
547 let message = b"test message for WebAuthn";
548 let signature = key_pair.sign(&rng, message).unwrap();
549
550 let result = verify_cose_signature(COSE_ALG_ES256, &cose_key, message, signature.as_ref());
551 assert!(result.is_ok());
552
553 let wrong_result = verify_cose_signature(
554 COSE_ALG_ES256,
555 &cose_key,
556 b"wrong message",
557 signature.as_ref(),
558 );
559 assert!(matches!(
560 wrong_result,
561 Err(WebauthnError::SignatureVerificationFailed)
562 ));
563 }
564
565 #[test]
566 fn test_verify_cose_signature_alg_key_type_mismatch() {
567 let n = vec![0xAA; 256];
569 let e = vec![0x01, 0x00, 0x01];
570 let cose_key = build_cose_rsa_key(&n, &e);
571 let (alg, key) = parse_cose_key(&cose_key).unwrap();
572 assert_eq!(alg, -257);
573
574 let mismatch = verify_cose_signature(COSE_ALG_ES256, &key, b"data", &[0u8; 64]);
575 assert!(matches!(
576 mismatch,
577 Err(WebauthnError::VerificationFailed(_))
578 ));
579 }
580
581 #[test]
582 fn test_verify_cose_signature_rs256() {
583 let n = vec![
584 0xC0, 0xE9, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
585 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
586 0x00, 0x00, 0x00, 0x00,
587 ];
588 let e = vec![0x01, 0x00, 0x01];
589 let cose_key = build_cose_rsa_key(&n, &e);
590
591 let (alg, key) = parse_cose_key(&cose_key).unwrap();
592 assert_eq!(alg, -257);
593 match key {
594 CosePublicKey::Rsa { n: kn, e: ke } => {
595 assert_eq!(kn, n);
596 assert_eq!(ke, e);
597 }
598 _ => panic!("Expected RSA key"),
599 }
600
601 let rsa_key = RsaPublicKeyDer { n: &n, e: &e };
602 let der = rsa_key.to_der().unwrap();
603 assert_eq!(der[0], 0x30);
604 assert!(der.len() > 40);
605 }
606
607 #[test]
608 fn test_generate_challenge_bytes_length() {
609 let bytes = generate_challenge_bytes();
610 assert_eq!(bytes.len(), 32);
611 }
612
613 #[test]
614 fn test_generate_challenge_bytes_not_all_zero() {
615 let bytes = generate_challenge_bytes();
617 assert!(bytes.iter().any(|&b| b != 0));
618 }
619
620 #[test]
621 fn test_base64_roundtrip() {
622 let original = b"hello world";
623 let encoded = base64_encode_urlsafe(original);
624 let decoded = base64_decode_urlsafe(&encoded).unwrap();
625 assert_eq!(decoded, original);
626 }
627
628 #[test]
629 fn test_base64_decode_urlsafe_invalid() {
630 let result = base64_decode_urlsafe("NOT_VALID_BASE64!!!");
631 assert!(result.is_err());
632 }
633
634 #[test]
635 fn test_base64_encode_decode_empty() {
636 let encoded = base64_encode_urlsafe(b"");
637 let decoded = base64_decode_urlsafe(&encoded).unwrap();
638 assert!(decoded.is_empty());
639 }
640
641 #[test]
642 fn test_cbor_bytes_from_text_is_lenient() {
643 use ciborium::Value;
644 let v = Value::Text("abc".to_string());
645 assert_eq!(cbor_bytes(&v), Some(b"abc".to_vec()));
646 let v = Value::Null;
647 assert_eq!(cbor_bytes(&v), None);
648 }
649
650 #[test]
654 fn der_integer_encoding_edge_cases() {
655 let mut buf = Vec::new();
656 RsaPublicKeyDer::encode_integer(&mut buf, &[0u8; 8]);
657 assert_eq!(buf, vec![0x02, 0x01, 0x00]);
658
659 buf.clear();
660 buf.clear();
661 RsaPublicKeyDer::encode_integer(&mut buf, &[0x80, 0x00]);
663 assert_eq!(buf, vec![0x02, 0x03, 0x00, 0x80, 0x00]);
664
665 buf.clear();
666 RsaPublicKeyDer::encode_integer(&mut buf, &[0x00, 0x00, 0x7F]);
667 assert_eq!(buf, vec![0x02, 0x01, 0x7F]);
668
669 buf.clear();
670 RsaPublicKeyDer::encode_integer(&mut buf, &[]);
673 assert_eq!(buf, vec![0x02, 0x00]);
674 }
675
676 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=";
681
682 fn der_tlv(i: &[u8]) -> (&[u8], &[u8]) {
684 let l0 = *i.get(1).unwrap();
685 let (header, len) = if l0 < 0x80 {
686 (2usize, l0 as usize)
687 } else {
688 let n = (l0 & 0x7F) as usize;
689 let mut len = 0usize;
690 for b in &i[2..2 + n] {
691 len = (len << 8) | *b as usize;
692 }
693 (2 + n, len)
694 };
695 (&i[header..header + len], &i[header + len..])
696 }
697
698 pub(crate) fn rsa_test_material() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
700 use base64::Engine as _;
701 let pkcs8 = base64::engine::general_purpose::STANDARD
702 .decode(RSA_PKCS8_B64)
703 .unwrap();
704 let (info, _) = der_tlv(&pkcs8);
707 let (_version, rest) = der_tlv(info);
708 let (_algorithm, rest) = der_tlv(rest);
709 let (octet, _) = der_tlv(rest);
710 let (rsa_key, _) = der_tlv(octet);
711 let (_version, rest) = der_tlv(rsa_key);
712 let (n, rest) = der_tlv(rest);
713 let (e, _) = der_tlv(rest);
714 (n.to_vec(), e.to_vec(), pkcs8)
715 }
716
717 pub(crate) fn rsa_test_spki() -> Vec<u8> {
720 let (n, e, _) = rsa_test_material();
721 RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap()
722 }
723
724 fn cose_key_from(entries: Vec<(i64, ciborium::Value)>) -> Vec<u8> {
725 use ciborium::Value;
726 let map: Vec<(Value, Value)> = entries
727 .into_iter()
728 .map(|(k, v)| (Value::Integer(k.into()), v))
729 .collect();
730 let mut buf = Vec::new();
731 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
732 buf
733 }
734
735 #[test]
736 fn parse_cose_key_garbage_cbor_is_error() {
737 let result = parse_cose_key(&[0xFF, 0xFF, 0xFF, 0xFF]);
739 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
740 }
741
742 #[test]
743 fn parse_cose_key_ec2_missing_crv() {
744 let buf = cose_key_from(vec![
745 (1, ciborium::Value::Integer(2.into())),
746 (2, ciborium::Value::Integer((-7).into())),
747 (-2, ciborium::Value::Bytes(vec![0xAA; 32])),
748 (-3, ciborium::Value::Bytes(vec![0xBB; 32])),
749 ]);
750 assert!(matches!(
751 parse_cose_key(&buf),
752 Err(WebauthnError::VerificationFailed(_))
753 ));
754 }
755
756 #[test]
757 fn parse_cose_key_ec2_wrong_curve_is_unsupported() {
758 let buf = cose_key_from(vec![
759 (1, ciborium::Value::Integer(2.into())),
760 (2, ciborium::Value::Integer((-7).into())),
761 (-1, ciborium::Value::Integer(3.into())), (-2, ciborium::Value::Bytes(vec![0xAA; 48])),
763 (-3, ciborium::Value::Bytes(vec![0xBB; 48])),
764 ]);
765 assert!(matches!(
766 parse_cose_key(&buf),
767 Err(WebauthnError::UnsupportedAlgorithm(-7))
768 ));
769 }
770
771 #[test]
772 fn parse_cose_key_ec2_missing_coordinates() {
773 let kty_alg = vec![
774 (1, ciborium::Value::Integer(2.into())),
775 (2, ciborium::Value::Integer((-7).into())),
776 (-1, ciborium::Value::Integer(1.into())),
777 ];
778 let missing_x = cose_key_from(
779 [
780 kty_alg.clone(),
781 vec![(-3, ciborium::Value::Bytes(vec![0xBB; 32]))],
782 ]
783 .concat(),
784 );
785 assert!(matches!(
786 parse_cose_key(&missing_x),
787 Err(WebauthnError::VerificationFailed(_))
788 ));
789
790 let missing_y =
791 cose_key_from([kty_alg, vec![(-2, ciborium::Value::Bytes(vec![0xAA; 32]))]].concat());
792 assert!(matches!(
793 parse_cose_key(&missing_y),
794 Err(WebauthnError::VerificationFailed(_))
795 ));
796 }
797
798 #[test]
799 fn parse_cose_key_rsa_missing_modulus_or_exponent() {
800 let missing_n = cose_key_from(vec![
801 (1, ciborium::Value::Integer(3.into())),
802 (2, ciborium::Value::Integer((-257).into())),
803 (-2, ciborium::Value::Bytes(vec![0x01, 0x00, 0x01])),
804 ]);
805 assert!(matches!(
806 parse_cose_key(&missing_n),
807 Err(WebauthnError::VerificationFailed(_))
808 ));
809
810 let missing_e = cose_key_from(vec![
811 (1, ciborium::Value::Integer(3.into())),
812 (2, ciborium::Value::Integer((-257).into())),
813 (-1, ciborium::Value::Bytes(vec![0xAA; 256])),
814 ]);
815 assert!(matches!(
816 parse_cose_key(&missing_e),
817 Err(WebauthnError::VerificationFailed(_))
818 ));
819 }
820
821 #[test]
822 fn parse_cose_key_unknown_kty_is_error() {
823 let buf = cose_key_from(vec![
824 (1, ciborium::Value::Integer(99.into())),
825 (2, ciborium::Value::Integer((-7).into())),
826 ]);
827 assert!(matches!(
828 parse_cose_key(&buf),
829 Err(WebauthnError::VerificationFailed(_))
830 ));
831 }
832
833 #[test]
834 fn cbor_map_entries_rejects_non_integer_keys() {
835 use ciborium::Value;
836 let map = Value::Map(vec![(
837 Value::Text("kty".to_string()),
838 Value::Integer(2.into()),
839 )]);
840 assert!(cbor_map_entries(&map).is_none());
841 }
842
843 #[test]
844 fn verify_rs256_with_ec_key_is_rejected() {
845 let key = CosePublicKey::Ec2 {
846 x: vec![0xAA; 32],
847 y: vec![0xBB; 32],
848 };
849 assert!(matches!(
850 verify_cose_signature(COSE_ALG_RS256, &key, b"data", &[0u8; 256]),
851 Err(WebauthnError::VerificationFailed(_))
852 ));
853 }
854
855 #[test]
856 fn verify_cose_signature_unknown_alg_is_rejected() {
857 let key = CosePublicKey::Ec2 {
858 x: vec![0xAA; 32],
859 y: vec![0xBB; 32],
860 };
861 assert!(matches!(
862 verify_cose_signature(-47, &key, b"data", &[0u8; 64]),
863 Err(WebauthnError::UnsupportedAlgorithm(-47))
864 ));
865 }
866
867 #[test]
868 fn alg_to_name_covers_known_and_unknown() {
869 assert_eq!(alg_to_name(COSE_ALG_ES256), "ES256");
870 assert_eq!(alg_to_name(COSE_ALG_RS256), "RS256");
871 assert_eq!(alg_to_name(0), "unknown");
872 }
873
874 #[test]
877 fn verify_rs256_real_signature_roundtrip() {
878 use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
879
880 let (n, e, pkcs8) = rsa_test_material();
881 let rsa = RsaKeyPair::from_pkcs8(&pkcs8).unwrap();
882 let modulus_len = rsa.public().modulus_len();
883 assert_eq!(modulus_len, 256);
884
885 let rng = ring::rand::SystemRandom::new();
887 let message = b"webauthn-kit rs256 vector";
888 let mut signature = vec![0u8; modulus_len];
889 rsa.sign(&RSA_PKCS1_SHA256, &rng, message, &mut signature)
890 .unwrap();
891
892 let key = CosePublicKey::Rsa { n, e };
893 assert!(verify_cose_signature(COSE_ALG_RS256, &key, message, &signature).is_ok());
894
895 let mut tampered = *message;
897 tampered[0] ^= 0x01;
898 assert!(matches!(
899 verify_cose_signature(COSE_ALG_RS256, &key, &tampered, &signature),
900 Err(WebauthnError::SignatureVerificationFailed)
901 ));
902 }
903
904 #[test]
908 fn der_long_form_length_encoding() {
909 let n = [0x77u8; 128];
910 let e = [0x01, 0x00, 0x01];
911 let der = RsaPublicKeyDer { n: &n, e: &e }.to_der().unwrap();
912
913 assert_eq!(der[0], 0x30);
915 assert_eq!(der[1], 0x81);
916 assert_eq!(der[2] as usize + 3, der.len());
917 assert!(der.windows(3).any(|w| w == [0x02, 0x81, 0x80]));
919 }
920}