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