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> {
126 use ciborium::Value;
127
128 let key_val: Value = ciborium::de::from_reader(cose_bytes).map_err(|e| {
129 WebauthnError::VerificationFailed(format!("COSE key CBOR parse error: {e}"))
130 })?;
131
132 let entries = cbor_map_entries(&key_val).ok_or_else(|| {
133 WebauthnError::VerificationFailed("COSE key is not a CBOR map".to_string())
134 })?;
135
136 let mut kty: Option<i64> = None;
137 let mut alg: Option<i32> = None;
138 let mut crv: Option<i64> = None;
139 let mut x: Option<Vec<u8>> = None;
140 let mut y: Option<Vec<u8>> = None;
141 let mut n: Option<Vec<u8>> = None;
142 let mut e: Option<Vec<u8>> = None;
143
144 for (label, val) in &entries {
145 match *label {
146 COSE_KEY_KTY => kty = cbor_i64(val),
147 COSE_KEY_ALG => alg = cbor_i64(val).map(|v| v as i32),
148 COSE_KEY_CRV_N => match kty {
149 Some(COSE_KTY_RSA) => n = cbor_bytes(val),
150 _ => crv = cbor_i64(val),
151 },
152 COSE_KEY_X_E => match kty {
153 Some(COSE_KTY_RSA) => e = cbor_bytes(val),
154 _ => x = cbor_bytes(val),
155 },
156 COSE_KEY_Y => y = cbor_bytes(val),
157 _ => {}
158 }
159 }
160
161 let kty =
162 kty.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'kty'".to_string()))?;
163 let alg =
164 alg.ok_or_else(|| WebauthnError::VerificationFailed("COSE key missing 'alg'".to_string()))?;
165
166 match kty {
167 COSE_KTY_EC2 => {
168 let crv = crv.ok_or_else(|| {
169 WebauthnError::VerificationFailed("EC2 key missing 'crv'".to_string())
170 })?;
171 if crv != COSE_CRV_P256 {
172 return Err(WebauthnError::UnsupportedAlgorithm(alg));
173 }
174 let x = x.ok_or_else(|| {
175 WebauthnError::VerificationFailed("EC2 key missing 'x'".to_string())
176 })?;
177 let y = y.ok_or_else(|| {
178 WebauthnError::VerificationFailed("EC2 key missing 'y'".to_string())
179 })?;
180 Ok((alg, CosePublicKey::Ec2 { x, y }))
181 }
182 COSE_KTY_RSA => {
183 let n = n.ok_or_else(|| {
184 WebauthnError::VerificationFailed("RSA key missing 'n'".to_string())
185 })?;
186 let e = e.ok_or_else(|| {
187 WebauthnError::VerificationFailed("RSA key missing 'e'".to_string())
188 })?;
189 Ok((alg, CosePublicKey::Rsa { n, e }))
190 }
191 COSE_KTY_OKP => Err(WebauthnError::UnsupportedAlgorithm(alg)),
192 other => Err(WebauthnError::VerificationFailed(format!(
193 "Unsupported COSE key type: {other}"
194 ))),
195 }
196}
197
198pub fn verify_cose_signature(
213 alg: i32,
214 public_key: &CosePublicKey,
215 signed_data: &[u8],
216 signature: &[u8],
217) -> Result<(), WebauthnError> {
218 use ring::signature;
219
220 match alg {
221 COSE_ALG_ES256 => {
222 let CosePublicKey::Ec2 { x, y } = public_key else {
223 return Err(WebauthnError::VerificationFailed(
224 "EC2 key expected for ES256".to_string(),
225 ));
226 };
227
228 let mut public_key_bytes = Vec::with_capacity(1 + x.len() + y.len());
229 public_key_bytes.push(0x04);
230 public_key_bytes.extend_from_slice(x);
231 public_key_bytes.extend_from_slice(y);
232
233 let public_key = signature::UnparsedPublicKey::new(
234 &signature::ECDSA_P256_SHA256_FIXED,
235 &public_key_bytes,
236 );
237 public_key
238 .verify(signed_data, signature)
239 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
240 Ok(())
241 }
242 COSE_ALG_RS256 => {
243 let CosePublicKey::Rsa { n, e } = public_key else {
244 return Err(WebauthnError::VerificationFailed(
245 "RSA key expected for RS256".to_string(),
246 ));
247 };
248
249 let rsa_public_key = RsaPublicKeyDer { n, e };
250 let der_bytes = rsa_public_key.to_der()?;
251
252 let public_key = signature::UnparsedPublicKey::new(
253 &signature::RSA_PKCS1_2048_8192_SHA256,
254 &der_bytes,
255 );
256 public_key
257 .verify(signed_data, signature)
258 .map_err(|_| WebauthnError::SignatureVerificationFailed)?;
259 Ok(())
260 }
261 other => Err(WebauthnError::UnsupportedAlgorithm(other)),
262 }
263}
264
265struct RsaPublicKeyDer<'a> {
267 n: &'a [u8],
268 e: &'a [u8],
269}
270
271impl RsaPublicKeyDer<'_> {
272 fn to_der(&self) -> Result<Vec<u8>, WebauthnError> {
277 let mut der = Vec::new();
278
279 let alg_id = Self::encode_sequence_owned({
280 let mut buf = Vec::new();
281 buf.extend_from_slice(&[
283 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B,
284 ]);
285 buf.extend_from_slice(&[0x05, 0x00]);
286 buf
287 });
288
289 let rsa_key = Self::encode_sequence_owned({
290 let mut buf = Vec::new();
291 Self::encode_integer(&mut buf, self.n);
292 Self::encode_integer(&mut buf, self.e);
293 buf
294 });
295
296 der.extend_from_slice(&alg_id);
297 Self::encode_bit_string(&mut der, &rsa_key);
298 Self::encode_sequence_in_place(&mut der);
299
300 Ok(der)
301 }
302
303 fn encode_integer(buf: &mut Vec<u8>, value: &[u8]) {
305 let mut v = value;
306 while v.len() > 1 && v[0] == 0 {
307 v = &v[1..];
308 }
309 if v.first().is_some_and(|&b| b & 0x80 != 0) {
310 buf.push(0x02);
311 buf.push((v.len() + 1) as u8);
312 buf.push(0x00);
313 buf.extend_from_slice(v);
314 } else {
315 buf.push(0x02);
316 buf.push(v.len() as u8);
317 buf.extend_from_slice(v);
318 }
319 }
320
321 fn encode_bit_string(buf: &mut Vec<u8>, content: &[u8]) {
323 let len = content.len() + 1;
324 buf.push(0x03);
325 Self::encode_length(buf, len);
326 buf.push(0x00);
327 buf.extend_from_slice(content);
328 }
329
330 fn encode_sequence_owned(content: Vec<u8>) -> Vec<u8> {
332 let mut result = Vec::with_capacity(2 + content.len());
333 result.push(0x30);
334 Self::encode_length(&mut result, content.len());
335 result.extend_from_slice(&content);
336 result
337 }
338
339 fn encode_sequence_in_place(buf: &mut Vec<u8>) {
341 let content_len = buf.len();
342 let header_len = if content_len < 0x80 {
343 2
344 } else if content_len < 0x100 {
345 3
346 } else {
347 4
348 };
349 buf.resize(content_len + header_len, 0);
350 for i in (0..content_len).rev() {
351 buf[i + header_len] = buf[i];
352 }
353 buf[0] = 0x30;
354 Self::encode_length_at(buf, 1, content_len);
355 }
356
357 fn encode_length(buf: &mut Vec<u8>, len: usize) {
359 if len < 0x80 {
360 buf.push(len as u8);
361 } else if len < 0x100 {
362 buf.push(0x81);
363 buf.push(len as u8);
364 } else {
365 buf.push(0x82);
366 buf.push((len >> 8) as u8);
367 buf.push((len & 0xFF) as u8);
368 }
369 }
370
371 fn encode_length_at(buf: &mut [u8], offset: usize, len: usize) {
373 if len < 0x80 {
374 buf[offset] = len as u8;
375 } else if len < 0x100 {
376 buf[offset] = 0x81;
377 buf[offset + 1] = len as u8;
378 } else {
379 buf[offset] = 0x82;
380 buf[offset + 1] = (len >> 8) as u8;
381 buf[offset + 2] = (len & 0xFF) as u8;
382 }
383 }
384}
385
386pub fn alg_to_name(alg: i32) -> &'static str {
388 match alg {
389 COSE_ALG_ES256 => "ES256",
390 COSE_ALG_RS256 => "RS256",
391 _ => "unknown",
392 }
393}
394
395pub fn generate_challenge_bytes() -> Vec<u8> {
405 use ring::rand::SecureRandom;
406 let mut bytes = [0u8; 32];
407 ring::rand::SystemRandom::new()
408 .fill(&mut bytes)
409 .expect("OS CSPRNG unavailable; cannot generate secure challenge");
410 bytes.to_vec()
411}
412
413pub fn base64_encode_urlsafe(data: &[u8]) -> String {
416 use base64::Engine;
417 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
418}
419
420pub fn base64_decode_urlsafe(data: &str) -> Result<Vec<u8>, WebauthnError> {
425 use base64::Engine;
426 base64::engine::general_purpose::URL_SAFE_NO_PAD
427 .decode(data)
428 .map_err(|e| WebauthnError::VerificationFailed(format!("base64 decode error: {e}")))
429}
430
431#[cfg(test)]
432pub(crate) mod tests {
433 use super::*;
434
435 pub(crate) fn build_cose_ec2_key(x: &[u8], y: &[u8]) -> Vec<u8> {
437 use ciborium::Value;
438 let map = vec![
439 (Value::Integer(1.into()), Value::Integer(2.into())),
440 (Value::Integer(2.into()), Value::Integer((-7).into())),
441 (Value::Integer((-1).into()), Value::Integer(1.into())),
442 (Value::Integer((-2).into()), Value::Bytes(x.to_vec())),
443 (Value::Integer((-3).into()), Value::Bytes(y.to_vec())),
444 ];
445 let mut buf = Vec::new();
446 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
447 buf
448 }
449
450 pub(crate) fn build_cose_rsa_key(n: &[u8], e: &[u8]) -> Vec<u8> {
452 use ciborium::Value;
453 let map = vec![
454 (Value::Integer(1.into()), Value::Integer(3.into())),
455 (Value::Integer(2.into()), Value::Integer((-257).into())),
456 (Value::Integer((-1).into()), Value::Bytes(n.to_vec())),
457 (Value::Integer((-2).into()), Value::Bytes(e.to_vec())),
458 ];
459 let mut buf = Vec::new();
460 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
461 buf
462 }
463
464 #[test]
465 fn test_parse_cose_ec2_key() {
466 let x = vec![0xAA; 32];
467 let y = vec![0xBB; 32];
468 let cose_key = build_cose_ec2_key(&x, &y);
469
470 let (alg, key) = parse_cose_key(&cose_key).unwrap();
471 assert_eq!(alg, -7);
472 match key {
473 CosePublicKey::Ec2 { x: kx, y: ky } => {
474 assert_eq!(kx, x);
475 assert_eq!(ky, y);
476 }
477 _ => panic!("Expected EC2 key"),
478 }
479 }
480
481 #[test]
482 fn test_parse_cose_rsa_key() {
483 let n = vec![0xAA; 256];
484 let e = vec![0x01, 0x00, 0x01];
485 let cose_key = build_cose_rsa_key(&n, &e);
486
487 let (alg, key) = parse_cose_key(&cose_key).unwrap();
488 assert_eq!(alg, -257);
489 match key {
490 CosePublicKey::Rsa { n: kn, e: ke } => {
491 assert_eq!(kn, n);
492 assert_eq!(ke, e);
493 }
494 _ => panic!("Expected RSA key"),
495 }
496 }
497
498 #[test]
499 fn test_parse_cose_key_not_a_map() {
500 let buf = [0x00u8]; let result = parse_cose_key(&buf);
502 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
503 }
504
505 #[test]
506 fn test_parse_cose_key_missing_kty() {
507 use ciborium::Value;
508 let map = vec![(Value::Integer(2.into()), Value::Integer((-7).into()))];
509 let mut buf = Vec::new();
510 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
511 let result = parse_cose_key(&buf);
512 assert!(matches!(result, Err(WebauthnError::VerificationFailed(_))));
513 }
514
515 #[test]
516 fn test_parse_cose_key_okp_unsupported() {
517 use ciborium::Value;
518 let map = vec![
519 (Value::Integer(1.into()), Value::Integer(1.into())),
520 (Value::Integer(2.into()), Value::Integer((-8).into())),
521 ];
522 let mut buf = Vec::new();
523 ciborium::ser::into_writer(&Value::Map(map), &mut buf).unwrap();
524 let result = parse_cose_key(&buf);
525 assert!(matches!(
526 result,
527 Err(WebauthnError::UnsupportedAlgorithm(-8))
528 ));
529 }
530
531 #[test]
532 fn test_verify_cose_signature_es256() {
533 use ring::signature::{EcdsaKeyPair, KeyPair as _, ECDSA_P256_SHA256_FIXED_SIGNING};
534
535 let rng = ring::rand::SystemRandom::new();
536 let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
537 let key_pair =
538 EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
539 .unwrap();
540
541 let public_key_bytes = key_pair.public_key().as_ref().to_vec();
542 let x = public_key_bytes[1..33].to_vec();
543 let y = public_key_bytes[33..65].to_vec();
544
545 let cose_key = CosePublicKey::Ec2 { x, y };
546 let message = b"test message for WebAuthn";
547 let signature = key_pair.sign(&rng, message).unwrap();
548
549 let result = verify_cose_signature(COSE_ALG_ES256, &cose_key, message, signature.as_ref());
550 assert!(result.is_ok());
551
552 let wrong_result = verify_cose_signature(
553 COSE_ALG_ES256,
554 &cose_key,
555 b"wrong message",
556 signature.as_ref(),
557 );
558 assert!(matches!(
559 wrong_result,
560 Err(WebauthnError::SignatureVerificationFailed)
561 ));
562 }
563
564 #[test]
565 fn test_verify_cose_signature_alg_key_type_mismatch() {
566 let n = vec![0xAA; 256];
568 let e = vec![0x01, 0x00, 0x01];
569 let cose_key = build_cose_rsa_key(&n, &e);
570 let (alg, key) = parse_cose_key(&cose_key).unwrap();
571 assert_eq!(alg, -257);
572
573 let mismatch = verify_cose_signature(COSE_ALG_ES256, &key, b"data", &[0u8; 64]);
574 assert!(matches!(
575 mismatch,
576 Err(WebauthnError::VerificationFailed(_))
577 ));
578 }
579
580 #[test]
581 fn test_verify_cose_signature_rs256() {
582 let n = vec![
583 0xC0, 0xE9, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
584 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
585 0x00, 0x00, 0x00, 0x00,
586 ];
587 let e = vec![0x01, 0x00, 0x01];
588 let cose_key = build_cose_rsa_key(&n, &e);
589
590 let (alg, key) = parse_cose_key(&cose_key).unwrap();
591 assert_eq!(alg, -257);
592 match key {
593 CosePublicKey::Rsa { n: kn, e: ke } => {
594 assert_eq!(kn, n);
595 assert_eq!(ke, e);
596 }
597 _ => panic!("Expected RSA key"),
598 }
599
600 let rsa_key = RsaPublicKeyDer { n: &n, e: &e };
601 let der = rsa_key.to_der().unwrap();
602 assert_eq!(der[0], 0x30);
603 assert!(der.len() > 40);
604 }
605
606 #[test]
607 fn test_generate_challenge_bytes_length() {
608 let bytes = generate_challenge_bytes();
609 assert_eq!(bytes.len(), 32);
610 }
611
612 #[test]
613 fn test_generate_challenge_bytes_not_all_zero() {
614 let bytes = generate_challenge_bytes();
616 assert!(bytes.iter().any(|&b| b != 0));
617 }
618
619 #[test]
620 fn test_base64_roundtrip() {
621 let original = b"hello world";
622 let encoded = base64_encode_urlsafe(original);
623 let decoded = base64_decode_urlsafe(&encoded).unwrap();
624 assert_eq!(decoded, original);
625 }
626
627 #[test]
628 fn test_base64_decode_urlsafe_invalid() {
629 let result = base64_decode_urlsafe("NOT_VALID_BASE64!!!");
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn test_base64_encode_decode_empty() {
635 let encoded = base64_encode_urlsafe(b"");
636 let decoded = base64_decode_urlsafe(&encoded).unwrap();
637 assert!(decoded.is_empty());
638 }
639
640 #[test]
641 fn test_cbor_bytes_from_text_is_lenient() {
642 use ciborium::Value;
643 let v = Value::Text("abc".to_string());
644 assert_eq!(cbor_bytes(&v), Some(b"abc".to_vec()));
645 let v = Value::Null;
646 assert_eq!(cbor_bytes(&v), None);
647 }
648}