1#![allow(clippy::unnecessary_unwrap)]
2use std::convert::TryInto;
18
19use crate::cbor::Value as CborValue;
20use ed25519_dalek::{Signature as Ed25519Signature, Verifier, VerifyingKey as Ed25519VerifyingKey};
21use p256::ecdsa::Signature as P256Signature;
22use p256::ecdsa::VerifyingKey as P256VerifyingKey;
23use sha2::{Digest, Sha256};
24
25use crate::bridges::BridgeError;
26
27const FLAG_USER_PRESENT: u8 = 0x01;
28const FLAG_ATTESTED_CREDENTIAL_DATA: u8 = 0x40;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CoseAlgorithm {
32 Es256,
33 EdDsa,
34 Rs256,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AttestationFormat {
39 None_,
40 Packed,
41 FidoU2f,
42}
43
44#[derive(Debug, Clone)]
45pub struct CosePublicKey {
46 pub kty: i64,
47 pub alg: Option<CoseAlgorithm>,
48 pub crv: Option<i64>,
49 pub x: Option<Vec<u8>>,
50 pub y: Option<Vec<u8>>,
51 pub n: Option<Vec<u8>>,
52 pub e: Option<Vec<u8>>,
53}
54
55#[derive(Debug, Clone)]
56pub struct ParsedAuthData {
57 pub rp_id_hash: Vec<u8>,
58 pub flags: u8,
59 pub sign_count: u32,
60 pub aaguid: Option<Vec<u8>>,
61 pub credential_id: Option<Vec<u8>>,
62 pub credential_public_key_cose: Option<Vec<u8>>,
63 pub credential_public_key: Option<CosePublicKey>,
64}
65
66#[derive(Debug, Clone)]
67pub struct AttestationObject {
68 pub fmt: AttestationFormat,
69 pub att_stmt: CborValue,
70 pub auth_data: Vec<u8>,
71}
72
73#[derive(Debug, Clone)]
74pub struct ClientData {
75 pub r#type: String,
76 pub challenge: String,
77 pub origin: String,
78}
79
80#[derive(Debug, Clone)]
81pub struct VerifyAttestationOptions {
82 pub rp_id: String,
83 pub expected_origin: String,
84 pub expected_challenge: String,
85 pub allowed_algorithms: Option<Vec<CoseAlgorithm>>,
86 pub require_attestation_signature: bool,
87}
88
89#[derive(Debug, Clone)]
90pub struct VerifiedAttestation {
91 pub format: AttestationFormat,
92 pub auth_data: ParsedAuthData,
93 pub client_data: ClientData,
94 pub credential_public_key: Vec<u8>,
95 pub credential_id: Vec<u8>,
96 pub algorithm: CoseAlgorithm,
97 pub x5c: Option<Vec<Vec<u8>>>,
98 pub sign_count: u32,
99 pub flags: u8,
100 pub aaguid: Option<Vec<u8>>,
101}
102
103pub fn parse_authenticator_data(buf: &[u8]) -> Result<ParsedAuthData, BridgeError> {
104 if buf.len() < 37 {
105 return Err(BridgeError::InvalidInput(format!(
106 "authData too short ({} bytes)",
107 buf.len()
108 )));
109 }
110 let rp_id_hash = buf[0..32].to_vec();
111 let flags = buf[32];
112 let sign_count = u32::from_be_bytes([buf[33], buf[34], buf[35], buf[36]]);
113 if flags & FLAG_ATTESTED_CREDENTIAL_DATA == 0 {
114 return Ok(ParsedAuthData {
115 rp_id_hash,
116 flags,
117 sign_count,
118 aaguid: None,
119 credential_id: None,
120 credential_public_key_cose: None,
121 credential_public_key: None,
122 });
123 }
124 if buf.len() < 55 {
125 return Err(BridgeError::InvalidInput(
126 "authData has AT flag but is too short for attested credential data".into(),
127 ));
128 }
129 let aaguid = buf[37..53].to_vec();
130 let cred_id_len = u16::from_be_bytes([buf[53], buf[54]]) as usize;
131 if buf.len() < 55 + cred_id_len {
132 return Err(BridgeError::InvalidInput(format!(
133 "authData truncated reading credentialId (declared {} bytes)",
134 cred_id_len
135 )));
136 }
137 let credential_id = buf[55..55 + cred_id_len].to_vec();
138 let cose_bytes = &buf[55 + cred_id_len..];
139 let credential_public_key = parse_cose_public_key(cose_bytes)?;
140
141 Ok(ParsedAuthData {
142 rp_id_hash,
143 flags,
144 sign_count,
145 aaguid: Some(aaguid),
146 credential_id: Some(credential_id),
147 credential_public_key_cose: Some(cose_bytes.to_vec()),
148 credential_public_key: Some(credential_public_key),
149 })
150}
151
152pub fn parse_cose_public_key(cose: &[u8]) -> Result<CosePublicKey, BridgeError> {
153 let val: CborValue = crate::cbor::decode(cose)
154 .map_err(|e| BridgeError::InvalidInput(format!("COSE key not valid CBOR: {}", e)))?;
155 let map = match &val {
156 CborValue::Map(m) => m,
157 _ => return Err(BridgeError::InvalidInput("COSE key not a map".into())),
158 };
159 let mut kty = None;
160 let mut alg: Option<CoseAlgorithm> = None;
161 let mut crv = None;
162 let mut x = None;
163 let mut y = None;
164 let mut n = None;
165 let mut e_field = None;
166 for (k, v) in map {
167 let key = match k {
168 CborValue::Integer(i) => Some(i64::try_from(*i).unwrap_or(0)),
169 _ => None,
170 };
171 match key {
172 Some(1) => {
173 if let CborValue::Integer(i) = v {
174 kty = Some(i64::try_from(*i).unwrap_or(0));
175 }
176 }
177 Some(3) => {
178 if let CborValue::Integer(i) = v {
179 alg = match i64::try_from(*i).unwrap_or(0) {
180 -7 => Some(CoseAlgorithm::Es256),
181 -8 => Some(CoseAlgorithm::EdDsa),
182 -257 => Some(CoseAlgorithm::Rs256),
183 _ => None,
184 };
185 }
186 }
187 Some(-1) => {
188 if let CborValue::Integer(i) = v {
189 crv = Some(i64::try_from(*i).unwrap_or(0));
190 } else if let CborValue::Bytes(b) = v {
191 n = Some(b.clone());
193 }
194 }
195 Some(-2) => {
196 if let CborValue::Bytes(b) = v {
197 x = Some(b.clone());
198 }
199 }
200 Some(-3) => {
201 if let CborValue::Bytes(b) = v {
202 y = Some(b.clone());
203 }
204 }
205 Some(-4) => {
206 if let CborValue::Bytes(b) = v {
207 e_field = Some(b.clone());
208 }
209 }
210 _ => {}
211 }
212 }
213 let kty = kty.ok_or_else(|| BridgeError::InvalidInput("COSE key missing kty".into()))?;
214 let (n_final, e_final) = if kty == 3 {
218 (n.clone().or(None), x.clone().or(None))
221 } else {
222 (None, None)
223 };
224 Ok(CosePublicKey {
225 kty,
226 alg,
227 crv,
228 x: if kty == 3 { None } else { x },
229 y: if kty == 3 { None } else { y },
230 n: n_final,
231 e: e_final.or(e_field),
232 })
233}
234
235pub fn decode_attestation_object(buf: &[u8]) -> Result<AttestationObject, BridgeError> {
236 let val: CborValue = crate::cbor::decode(buf).map_err(|e| {
237 BridgeError::InvalidInput(format!("attestationObject not valid CBOR: {}", e))
238 })?;
239 let map = match val {
240 CborValue::Map(m) => m,
241 _ => {
242 return Err(BridgeError::InvalidInput(
243 "attestationObject not a map".into(),
244 ))
245 }
246 };
247 let mut fmt = None;
248 let mut att_stmt = None;
249 let mut auth_data = None;
250 for (k, v) in map {
251 let key = match k {
252 CborValue::Text(t) => t,
253 _ => continue,
254 };
255 match key.as_str() {
256 "fmt" => fmt = v.as_text().map(|s| s.to_string()),
257 "attStmt" => att_stmt = Some(v),
258 "authData" => auth_data = v.as_bytes().map(|b| b.to_vec()),
259 _ => {}
260 }
261 }
262 let fmt = fmt.ok_or_else(|| BridgeError::InvalidInput("missing fmt".into()))?;
263 let auth_data =
264 auth_data.ok_or_else(|| BridgeError::InvalidInput("missing authData bytes".into()))?;
265 let att_stmt = att_stmt.ok_or_else(|| BridgeError::InvalidInput("missing attStmt".into()))?;
266 let format = match fmt.as_str() {
267 "none" => AttestationFormat::None_,
268 "packed" => AttestationFormat::Packed,
269 "fido-u2f" => AttestationFormat::FidoU2f,
270 other => {
271 return Err(BridgeError::Unsupported(format!(
272 "attestation format {} not supported",
273 other
274 )))
275 }
276 };
277 Ok(AttestationObject {
278 fmt: format,
279 att_stmt,
280 auth_data,
281 })
282}
283
284pub fn parse_client_data(buf: &[u8]) -> Result<ClientData, BridgeError> {
285 let json: serde_json::Value = serde_json::from_slice(buf)
286 .map_err(|e| BridgeError::InvalidInput(format!("clientDataJSON not valid JSON: {}", e)))?;
287 let obj = json
288 .as_object()
289 .ok_or_else(|| BridgeError::InvalidInput("clientDataJSON not an object".into()))?;
290 let r#type = obj
291 .get("type")
292 .and_then(|v| v.as_str())
293 .ok_or_else(|| BridgeError::InvalidInput("missing clientData.type".into()))?
294 .to_string();
295 let challenge = obj
296 .get("challenge")
297 .and_then(|v| v.as_str())
298 .ok_or_else(|| BridgeError::InvalidInput("missing clientData.challenge".into()))?
299 .to_string();
300 let origin = obj
301 .get("origin")
302 .and_then(|v| v.as_str())
303 .ok_or_else(|| BridgeError::InvalidInput("missing clientData.origin".into()))?
304 .to_string();
305 Ok(ClientData {
306 r#type,
307 challenge,
308 origin,
309 })
310}
311
312pub fn verify_attestation(
313 attestation_object: &[u8],
314 client_data_json: &[u8],
315 opts: &VerifyAttestationOptions,
316) -> Result<VerifiedAttestation, BridgeError> {
317 let att = decode_attestation_object(attestation_object)?;
318 let auth = parse_authenticator_data(&att.auth_data)?;
319 let client = parse_client_data(client_data_json)?;
320 if client.r#type != "webauthn.create" {
321 return Err(BridgeError::Rejected(format!(
322 "clientData.type {} is not webauthn.create",
323 client.r#type
324 )));
325 }
326 if client.origin != opts.expected_origin {
327 return Err(BridgeError::Rejected(format!(
328 "clientData.origin {} does not match expected {}",
329 client.origin, opts.expected_origin
330 )));
331 }
332 if client.challenge != opts.expected_challenge {
333 return Err(BridgeError::Rejected(
334 "clientData.challenge does not match expected".into(),
335 ));
336 }
337 let expected_rp_hash: [u8; 32] = Sha256::digest(opts.rp_id.as_bytes()).into();
338 if auth.rp_id_hash != expected_rp_hash {
339 return Err(BridgeError::Rejected(
340 "authData rpIdHash does not match sha256(rpId)".into(),
341 ));
342 }
343 if auth.flags & FLAG_USER_PRESENT == 0 {
344 return Err(BridgeError::Rejected(
345 "authData missing User Present flag".into(),
346 ));
347 }
348 if auth.flags & FLAG_ATTESTED_CREDENTIAL_DATA == 0 {
349 return Err(BridgeError::Rejected(
350 "authData missing AT flag (no attested credential data)".into(),
351 ));
352 }
353 let cose = auth
354 .credential_public_key
355 .as_ref()
356 .ok_or_else(|| BridgeError::InvalidInput("credential public key missing".into()))?;
357 let credential_id = auth
358 .credential_id
359 .as_ref()
360 .ok_or_else(|| BridgeError::InvalidInput("credential id missing".into()))?
361 .clone();
362 let alg = cose.alg.ok_or_else(|| {
363 BridgeError::InvalidInput("credential public key has no algorithm".into())
364 })?;
365 if let Some(allowed) = &opts.allowed_algorithms {
366 if !allowed.contains(&alg) {
367 return Err(BridgeError::Rejected(format!(
368 "algorithm {:?} not in allow-list",
369 alg
370 )));
371 }
372 }
373 let client_data_hash: [u8; 32] = Sha256::digest(client_data_json).into();
374
375 match att.fmt {
376 AttestationFormat::Packed => verify_packed(&att, &auth, &client_data_hash)?,
377 AttestationFormat::FidoU2f => verify_fido_u2f(&att, &auth, &client_data_hash)?,
378 AttestationFormat::None_ => {
379 if opts.require_attestation_signature {
380 return Err(BridgeError::Rejected(
381 "format=none rejected when require_attestation_signature=true".into(),
382 ));
383 }
384 }
385 }
386
387 let credential_public_key = encode_raw_public_key(cose)?;
388 let x5c = pick_x5c(&att.att_stmt);
389
390 Ok(VerifiedAttestation {
391 format: att.fmt,
392 auth_data: auth.clone(),
393 client_data: client,
394 credential_public_key,
395 credential_id,
396 algorithm: alg,
397 x5c,
398 sign_count: auth.sign_count,
399 flags: auth.flags,
400 aaguid: auth.aaguid,
401 })
402}
403
404fn verify_packed(
405 att: &AttestationObject,
406 auth: &ParsedAuthData,
407 client_data_hash: &[u8; 32],
408) -> Result<(), BridgeError> {
409 let map = match &att.att_stmt {
410 CborValue::Map(m) => m,
411 _ => return Err(BridgeError::InvalidInput("packed attStmt not a map".into())),
412 };
413 let mut sig: Option<Vec<u8>> = None;
414 let mut alg: Option<i64> = None;
415 let mut x5c: Option<Vec<Vec<u8>>> = None;
416 for (k, v) in map {
417 let key = match k {
418 CborValue::Text(t) => t.as_str(),
419 _ => continue,
420 };
421 match key {
422 "sig" => sig = v.as_bytes().map(|b| b.to_vec()),
423 "alg" => {
424 if let CborValue::Integer(i) = v {
425 alg = Some(i64::try_from(*i).unwrap_or(0));
426 }
427 }
428 "x5c" => {
429 if let CborValue::Array(arr) = v {
430 x5c = Some(
431 arr.iter()
432 .filter_map(|c| c.as_bytes().map(|b| b.to_vec()))
433 .collect(),
434 );
435 }
436 }
437 _ => {}
438 }
439 }
440 let sig = sig.ok_or_else(|| BridgeError::InvalidInput("packed attStmt missing sig".into()))?;
441 let alg = alg.ok_or_else(|| BridgeError::InvalidInput("packed attStmt missing alg".into()))?;
442 let mut data = att.auth_data.clone();
443 data.extend_from_slice(client_data_hash);
444 if let Some(chain) = x5c.as_ref() {
445 if let Some(cert_der) = chain.first() {
446 verify_with_cert(cert_der, &data, &sig, alg)?;
447 return Ok(());
448 }
449 }
450 let cose = auth.credential_public_key.as_ref().ok_or_else(|| {
451 BridgeError::InvalidInput("self-attestation needs credential public key".into())
452 })?;
453 verify_cose_signature(cose, &data, &sig, alg)
454}
455
456fn verify_fido_u2f(
457 att: &AttestationObject,
458 auth: &ParsedAuthData,
459 client_data_hash: &[u8; 32],
460) -> Result<(), BridgeError> {
461 let map = match &att.att_stmt {
462 CborValue::Map(m) => m,
463 _ => {
464 return Err(BridgeError::InvalidInput(
465 "fido-u2f attStmt not a map".into(),
466 ))
467 }
468 };
469 let mut sig: Option<Vec<u8>> = None;
470 let mut x5c: Option<Vec<Vec<u8>>> = None;
471 for (k, v) in map {
472 let key = match k {
473 CborValue::Text(t) => t.as_str(),
474 _ => continue,
475 };
476 match key {
477 "sig" => sig = v.as_bytes().map(|b| b.to_vec()),
478 "x5c" => {
479 if let CborValue::Array(arr) = v {
480 x5c = Some(
481 arr.iter()
482 .filter_map(|c| c.as_bytes().map(|b| b.to_vec()))
483 .collect(),
484 );
485 }
486 }
487 _ => {}
488 }
489 }
490 let sig =
491 sig.ok_or_else(|| BridgeError::InvalidInput("fido-u2f attStmt missing sig".into()))?;
492 let x5c =
493 x5c.ok_or_else(|| BridgeError::InvalidInput("fido-u2f attStmt missing x5c".into()))?;
494 let cose = auth
495 .credential_public_key
496 .as_ref()
497 .ok_or_else(|| BridgeError::InvalidInput("fido-u2f needs credential pubkey".into()))?;
498 if cose.kty != 2 || cose.x.is_none() || cose.y.is_none() {
499 return Err(BridgeError::InvalidInput(
500 "fido-u2f requires EC2 P-256 credential public key".into(),
501 ));
502 }
503 let mut data = Vec::new();
504 data.push(0x00);
505 data.extend_from_slice(&auth.rp_id_hash);
506 data.extend_from_slice(client_data_hash);
507 data.extend_from_slice(auth.credential_id.as_ref().unwrap());
508 data.push(0x04);
509 data.extend_from_slice(cose.x.as_ref().unwrap());
510 data.extend_from_slice(cose.y.as_ref().unwrap());
511 let cert = x5c
512 .first()
513 .ok_or_else(|| BridgeError::InvalidInput("fido-u2f x5c empty".into()))?;
514 verify_with_cert(cert, &data, &sig, -7)
515}
516
517fn verify_with_cert(
518 cert_der: &[u8],
519 data: &[u8],
520 signature: &[u8],
521 cose_alg: i64,
522) -> Result<(), BridgeError> {
523 use x509_parser::certificate::X509Certificate;
524 use x509_parser::prelude::FromDer;
525 let (_, cert) = X509Certificate::from_der(cert_der)
526 .map_err(|e| BridgeError::InvalidInput(format!("cert DER parse: {}", e)))?;
527 let alg_oid = cert.public_key().algorithm.algorithm.to_id_string();
528 let key_bytes = cert.public_key().subject_public_key.data.as_ref();
529 match alg_oid.as_str() {
530 "1.2.840.10045.2.1" if cose_alg == -7 => verify_p256_der(key_bytes, data, signature),
531 "1.3.101.112" if cose_alg == -8 => verify_ed25519(key_bytes, data, signature),
532 _ => Err(BridgeError::Unsupported(format!(
533 "x5c algorithm {} not supported for cose alg {}",
534 alg_oid, cose_alg
535 ))),
536 }
537}
538
539fn verify_cose_signature(
540 cose: &CosePublicKey,
541 data: &[u8],
542 sig: &[u8],
543 alg: i64,
544) -> Result<(), BridgeError> {
545 if alg == -7 && cose.kty == 2 {
546 let x = cose
547 .x
548 .as_ref()
549 .ok_or_else(|| BridgeError::InvalidInput("EC2 missing x".into()))?;
550 let y = cose
551 .y
552 .as_ref()
553 .ok_or_else(|| BridgeError::InvalidInput("EC2 missing y".into()))?;
554 let mut pub_bytes = vec![0x04];
555 pub_bytes.extend_from_slice(x);
556 pub_bytes.extend_from_slice(y);
557 return verify_p256_der(&pub_bytes, data, sig);
558 }
559 if alg == -8 && cose.kty == 1 {
560 let x = cose
561 .x
562 .as_ref()
563 .ok_or_else(|| BridgeError::InvalidInput("OKP missing x".into()))?;
564 return verify_ed25519(x, data, sig);
565 }
566 Err(BridgeError::Unsupported(format!(
567 "self-attestation alg {} on kty {} not supported",
568 alg, cose.kty
569 )))
570}
571
572fn verify_p256_der(
573 public_uncompressed: &[u8],
574 data: &[u8],
575 der_sig: &[u8],
576) -> Result<(), BridgeError> {
577 let vk = P256VerifyingKey::from_sec1_bytes(public_uncompressed)
578 .map_err(|e| BridgeError::InvalidInput(format!("bad P-256 SEC1 key: {}", e)))?;
579 let sig = P256Signature::from_der(der_sig)
580 .map_err(|e| BridgeError::InvalidInput(format!("bad ECDSA DER sig: {}", e)))?;
581 vk.verify(data, &sig)
582 .map_err(|e| BridgeError::Rejected(format!("ES256 verify failed: {}", e)))
583}
584
585fn verify_ed25519(public: &[u8], data: &[u8], sig: &[u8]) -> Result<(), BridgeError> {
586 let public_arr: [u8; 32] = public
587 .try_into()
588 .map_err(|_| BridgeError::InvalidInput("Ed25519 key not 32 bytes".into()))?;
589 let vk = Ed25519VerifyingKey::from_bytes(&public_arr)
590 .map_err(|e| BridgeError::InvalidInput(format!("bad Ed25519 key: {}", e)))?;
591 let sig_arr: [u8; 64] = sig
592 .try_into()
593 .map_err(|_| BridgeError::InvalidInput("Ed25519 signature not 64 bytes".into()))?;
594 let sig = Ed25519Signature::from_bytes(&sig_arr);
595 vk.verify(data, &sig)
596 .map_err(|e| BridgeError::Rejected(format!("EdDSA verify failed: {}", e)))
597}
598
599fn pick_x5c(att_stmt: &CborValue) -> Option<Vec<Vec<u8>>> {
600 if let CborValue::Map(m) = att_stmt {
601 for (k, v) in m {
602 if let CborValue::Text(t) = k {
603 if t == "x5c" {
604 if let CborValue::Array(arr) = v {
605 let collected: Vec<Vec<u8>> = arr
606 .iter()
607 .filter_map(|c| c.as_bytes().map(|b| b.to_vec()))
608 .collect();
609 if !collected.is_empty() {
610 return Some(collected);
611 }
612 }
613 }
614 }
615 }
616 }
617 None
618}
619
620fn encode_raw_public_key(cose: &CosePublicKey) -> Result<Vec<u8>, BridgeError> {
621 if cose.kty == 2 && cose.x.is_some() && cose.y.is_some() {
622 let mut out = vec![0x04];
623 out.extend_from_slice(cose.x.as_ref().unwrap());
624 out.extend_from_slice(cose.y.as_ref().unwrap());
625 return Ok(out);
626 }
627 if cose.kty == 1 && cose.x.is_some() {
628 return Ok(cose.x.clone().unwrap());
629 }
630 if cose.kty == 3 && cose.n.is_some() {
631 return Ok(cose.n.clone().unwrap());
632 }
633 Err(BridgeError::InvalidInput(
634 "unsupported COSE key shape".into(),
635 ))
636}