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> {
273 n: &'a [u8],
274 e: &'a [u8],
275}
276
277impl RsaPublicKeyDer<'_> {
278 fn to_der(&self) -> Result<Vec<u8>, WebauthnError> {
283 let mut der = Vec::new();
284
285 let alg_id = Self::encode_sequence_owned({
286 let mut buf = Vec::new();
287 buf.extend_from_slice(&[
289 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B,
290 ]);
291 buf.extend_from_slice(&[0x05, 0x00]);
292 buf
293 });
294
295 let rsa_key = Self::encode_sequence_owned({
296 let mut buf = Vec::new();
297 Self::encode_integer(&mut buf, self.n);
298 Self::encode_integer(&mut buf, self.e);
299 buf
300 });
301
302 der.extend_from_slice(&alg_id);
303 Self::encode_bit_string(&mut der, &rsa_key);
304 Self::encode_sequence_in_place(&mut der);
305
306 Ok(der)
307 }
308
309 fn encode_integer(buf: &mut Vec<u8>, value: &[u8]) {
311 let start = value
314 .iter()
315 .position(|&b| b != 0)
316 .unwrap_or(value.len().saturating_sub(1));
317 let v = value.get(start..).unwrap_or(&[]);
318 if v.first().is_some_and(|&b| b & 0x80 != 0) {
319 buf.push(0x02);
320 buf.push((v.len() + 1) as u8);
321 buf.push(0x00);
322 buf.extend_from_slice(v);
323 } else {
324 buf.push(0x02);
325 buf.push(v.len() as u8);
326 buf.extend_from_slice(v);
327 }
328 }
329
330 fn encode_bit_string(buf: &mut Vec<u8>, content: &[u8]) {
332 let len = content.len() + 1;
333 buf.push(0x03);
334 Self::encode_length(buf, len);
335 buf.push(0x00);
336 buf.extend_from_slice(content);
337 }
338
339 fn encode_sequence_owned(content: Vec<u8>) -> Vec<u8> {
341 let mut result = Vec::with_capacity(2 + content.len());
342 result.push(0x30);
343 Self::encode_length(&mut result, content.len());
344 result.extend_from_slice(&content);
345 result
346 }
347
348 fn encode_sequence_in_place(buf: &mut Vec<u8>) {
350 let content_len = buf.len();
351 let header_len = if content_len < 0x80 {
352 2
353 } else if content_len < 0x100 {
354 3
355 } else {
356 4
357 };
358 buf.resize(content_len + header_len, 0);
359 buf.copy_within(..content_len, header_len);
360 if let Some(tag) = buf.first_mut() {
361 *tag = 0x30;
362 }
363 Self::encode_length_at(buf, 1, content_len);
364 }
365
366 fn encode_length(buf: &mut Vec<u8>, len: usize) {
368 if len < 0x80 {
369 buf.push(len as u8);
370 } else if len < 0x100 {
371 buf.push(0x81);
372 buf.push(len as u8);
373 } else {
374 buf.push(0x82);
375 buf.push((len >> 8) as u8);
376 buf.push((len & 0xFF) as u8);
377 }
378 }
379
380 #[allow(clippy::indexing_slicing)]
387 fn encode_length_at(buf: &mut [u8], offset: usize, len: usize) {
388 if len < 0x80 {
389 buf[offset] = len as u8;
390 } else if len < 0x100 {
391 buf[offset] = 0x81;
392 buf[offset + 1] = len as u8;
393 } else {
394 buf[offset] = 0x82;
395 buf[offset + 1] = (len >> 8) as u8;
396 buf[offset + 2] = (len & 0xFF) as u8;
397 }
398 }
399}
400
401pub fn alg_to_name(alg: i32) -> &'static str {
403 match alg {
404 COSE_ALG_ES256 => "ES256",
405 COSE_ALG_RS256 => "RS256",
406 _ => "unknown",
407 }
408}
409
410pub fn generate_challenge_bytes() -> Vec<u8> {
423 use ring::rand::SecureRandom;
424 let mut bytes = [0u8; 32];
425 #[allow(clippy::expect_used)]
428 ring::rand::SystemRandom::new()
429 .fill(&mut bytes)
430 .expect("OS CSPRNG unavailable; cannot generate secure challenge");
431 bytes.to_vec()
432}
433
434pub fn base64_encode_urlsafe(data: &[u8]) -> String {
437 use base64::Engine;
438 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
439}
440
441pub fn base64_decode_urlsafe(data: &str) -> Result<Vec<u8>, WebauthnError> {
449 use base64::Engine;
450 base64::engine::general_purpose::URL_SAFE_NO_PAD
451 .decode(data)
452 .map_err(|e| WebauthnError::VerificationFailed(format!("base64 decode error: {e}")))
453}
454
455#[allow(
459 clippy::unwrap_used,
460 clippy::expect_used,
461 clippy::indexing_slicing,
462 clippy::panic
463)]
464#[cfg(test)]
465pub(crate) mod tests {
466 use super::*;
467
468 pub(crate) fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
470 use ciborium::Value;
471 let map = vec![
472 (Value::Integer(1.into()), Value::Integer(2.into())),
473 (Value::Integer(2.into()), Value::Integer((-7).into())),
474 (Value::Integer((-1).into()), Value::Integer(1.into())),
475 (Value::Integer((-2).into()), Value::Bytes(x.to_vec())),
476 (Value::Integer((-3).into()), Value::Bytes(y.to_vec())),
477 ];
478 let mut buf = Vec::new();
479 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
480 buf
481 }
482
483 pub(crate) fn build_cose_rsa_key(n: &[u8], e: &[u8]) -> Vec<u8> {
485 use ciborium::Value;
486 let map = vec![
487 (Value::Integer(1.into()), Value::Integer(3.into())),
488 (Value::Integer(2.into()), Value::Integer((-257).into())),
489 (Value::Integer((-1).into()), Value::Bytes(n.to_vec())),
490 (Value::Integer((-2).into()), Value::Bytes(e.to_vec())),
491 ];
492 let mut buf = Vec::new();
493 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
494 buf
495 }
496
497 #[test]
498 fn test_parse_cose_ec2_key() {
499 let x = vec![0xAA; 32];
500 let y = vec![0xBB; 32];
501 let cose_key = build_cose_ec2_key(&x, &y);
502
503 let (alg, key) = parse_cose_key(&cose_key).unwrap();
504 assert_eq!(alg, -7);
505 match key {
506 CosePublicKey::Ec2 { x: kx, y: ky } => {
507 assert_eq!(kx, x);
508 assert_eq!(ky, y);
509 }
510 _ => panic!("Expected EC2 key"),
511 }
512 }
513
514 #[test]
515 fn test_parse_cose_rsa_key() {
516 let n = vec![0xAA; 256];
517 let e = vec![0x01, 0x00, 0x01];
518 let cose_key = build_cose_rsa_key(&n, &e);
519
520 let (alg, key) = parse_cose_key(&cose_key).unwrap();
521 assert_eq!(alg, -257);
522 match key {
523 CosePublicKey::Rsa { n: kn, e: ke } => {
524 assert_eq!(kn, n);
525 assert_eq!(ke, e);
526 }
527 _ => panic!("Expected RSA key"),
528 }
529 }
530
531 #[test]
532 fn test_parse_cose_key_not_a_map() {
533 let buf = [0x00u8]; let result = parse_cose_key(&buf);
535 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
536 }
537
538 #[test]
539 fn test_parse_cose_key_missing_kty() {
540 use ciborium::Value;
541 let map = vec![(Value::Integer(2.into()), Value::Integer((-7).into()))];
542 let mut buf = Vec::new();
543 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
544 let result = parse_cose_key(&buf);
545 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
546 }
547
548 #[test]
549 fn test_parse_cose_key_okp_unsupported() {
550 use ciborium::Value;
551 let map = vec![
552 (Value::Integer(1.into()), Value::Integer(1.into())),
553 (Value::Integer(2.into()), Value::Integer((-8).into())),
554 ];
555 let mut buf = Vec::new();
556 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
557 let result = parse_cose_key(&buf);
558 assert!(matches!(
559 result,
560 Err(WebauthnError::UnsupportedAlgorithm(-8))
561 ));
562 }
563
564 #[test]
565 fn test_verify_cose_signature_es256() {
566 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P256_SHA256_FIXED_SIGNING};
567
568 let rng = ring::rand::SystemRandom::new();
569 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
570 let key_pair =
571 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
572 .unwrap();
573
574 let public_key_bytes = key_pair.public_key().as_ref().to_vec();
575 let x = public_key_bytes[1..33].to_vec();
576 let y = public_key_bytes[33..65].to_vec();
577
578 let cose_key = CosePublicKey::Ec2 { x, y };
579 let message = b"test message for WebAuthn";
580 let signature = key_pair.sign(&rng, message).unwrap();
581
582 let result = verify_cose_signature(COSE_ALG_ES256, &cose_key, message, signature.as_ref());
583 assert!(result.is_ok());
584
585 let wrong_result = verify_cose_signature(
586 COSE_ALG_ES256,
587 &cose_key,
588 b"wrong message",
589 signature.as_ref(),
590 );
591 assert!(matches!(
592 wrong_result,
593 Err(WebauthnError::SignatureVerificationFailed)
594 ));
595 }
596
597 #[test]
598 fn test_verify_cose_signature_alg_key_type_mismatch() {
599 let n = vec![0xAA; 256];
601 let e = vec![0x01, 0x00, 0x01];
602 let cose_key = build_cose_rsa_key(&n, &e);
603 let (alg, key) = parse_cose_key(&cose_key).unwrap();
604 assert_eq!(alg, -257);
605
606 let mismatch = verify_cose_signature(COSE_ALG_ES256, &key, b"data", &[0u8; 64]);
607 assert!(matches!(
608 mismatch,
609 Err(WebauthnError::VerificationFailed(_))
610 ));
611 }
612
613 #[test]
614 fn test_verify_cose_signature_rs256() {
615 let n = vec![
616 0xC0, 0xE9, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
617 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
618 0x00, 0x00, 0x00, 0x00,
619 ];
620 let e = vec![0x01, 0x00, 0x01];
621 let cose_key = build_cose_rsa_key(&n, &e);
622
623 let (alg, key) = parse_cose_key(&cose_key).unwrap();
624 assert_eq!(alg, -257);
625 match key {
626 CosePublicKey::Rsa { n: kn, e: ke } => {
627 assert_eq!(kn, n);
628 assert_eq!(ke, e);
629 }
630 _ => panic!("Expected RSA key"),
631 }
632
633 let rsa_key = RsaPublicKeyDer { n: &n, e: &e };
634 let der = rsa_key.to_der().unwrap();
635 assert_eq!(der[0], 0x30);
636 assert!(der.len() > 40);
637 }
638
639 #[test]
640 fn test_generate_challenge_bytes_length() {
641 let bytes = generate_challenge_bytes();
642 assert_eq!(bytes.len(), 32);
643 }
644
645 #[test]
646 fn test_generate_challenge_bytes_not_all_zero() {
647 let bytes = generate_challenge_bytes();
649 assert!(bytes.iter().any(|&b| b != 0));
650 }
651
652 #[test]
653 fn test_base64_roundtrip() {
654 let original = b"hello world";
655 let encoded = base64_encode_urlsafe(original);
656 let decoded = base64_decode_urlsafe(&encoded).unwrap();
657 assert_eq!(decoded, original);
658 }
659
660 #[test]
661 fn test_base64_decode_urlsafe_invalid() {
662 let result = base64_decode_urlsafe("NOT_VALID_BASE64!!!");
663 assert!(result.is_err());
664 }
665
666 #[test]
667 fn test_base64_encode_decode_empty() {
668 let encoded = base64_encode_urlsafe(b"");
669 let decoded = base64_decode_urlsafe(&encoded).unwrap();
670 assert!(decoded.is_empty());
671 }
672
673 #[test]
674 fn test_cbor_bytes_from_text_is_lenient() {
675 use ciborium::Value;
676 let v = Value::Text("abc".to_string());
677 assert_eq!(cbor_bytes(&v), Some(b"abc".to_vec()));
678 let v = Value::Null;
679 assert_eq!(cbor_bytes(&v), None);
680 }
681
682 #[test]
686 fn der_integer_encoding_edge_cases() {
687 let mut buf = Vec::new();
688 RsaPublicKeyDer::encode_integer(&mut buf, &[0u8; 8]);
689 assert_eq!(buf, vec![0x02, 0x01, 0x00]);
690
691 buf.clear();
692 RsaPublicKeyDer::encode_integer(&mut buf, &[0x80, 0x00]);
693 assert_eq!(buf, vec![0x02, 0x03, 0x00, 0x80, 0x00]);
694
695 buf.clear();
696 RsaPublicKeyDer::encode_integer(&mut buf, &[0x00, 0x00, 0x7F]);
697 assert_eq!(buf, vec![0x02, 0x01, 0x7F]);
698
699 buf.clear();
700 RsaPublicKeyDer::encode_integer(&mut buf, &[]);
703 assert_eq!(buf, vec![0x02, 0x00]);
704 }
705}