Skip to content

Commit 234fdbc

Browse files
fix(pin): validate peer COSE EC2 key x/y length in PIN/UV ECDH
A malicious or buggy authenticator can return an `EcdhEsHkdf256PublicKey` whose x or y coordinate is shorter than 32 bytes. `cosey` accepts any length up to 32, but `EncodedPoint::from_affine_coordinates` requires exactly 32 bytes per coordinate; the `.into()` calls invoke `GenericArray::from_slice` which panics on length mismatch. CTAP 2.2 §6.5.6 requires x and y to be 32 bytes (P-256 field-element size). Validate explicitly via `try_into` and return `Error::Ctap(CtapError::Other)` on mismatch. Add regression tests for short and empty x, and short y.
1 parent 36cfe41 commit 234fdbc

1 file changed

Lines changed: 63 additions & 5 deletions

File tree

libwebauthn/src/pin.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,23 @@ where
175175
);
176176
return Err(Error::Ctap(CtapError::Other));
177177
};
178-
let encoded_point = EncodedPoint::from_affine_coordinates(
179-
peer_public_key.x.as_bytes().into(),
180-
peer_public_key.y.as_bytes().into(),
181-
false,
182-
);
178+
// x and y must be exactly 32 bytes (P-256 field size). `cosey` accepts
179+
// any length up to 32; validate before converting to `&FieldBytes`.
180+
let x: &[u8; 32] = peer_public_key.x.as_bytes().try_into().map_err(|_| {
181+
error!(
182+
x_len = peer_public_key.x.as_bytes().len(),
183+
"Peer public key x coordinate is not 32 bytes"
184+
);
185+
Error::Ctap(CtapError::Other)
186+
})?;
187+
let y: &[u8; 32] = peer_public_key.y.as_bytes().try_into().map_err(|_| {
188+
error!(
189+
y_len = peer_public_key.y.as_bytes().len(),
190+
"Peer public key y coordinate is not 32 bytes"
191+
);
192+
Error::Ctap(CtapError::Other)
193+
})?;
194+
let encoded_point = EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
183195
let Some(peer_public_key) = P256PublicKey::from_encoded_point(&encoded_point).into() else {
184196
error!("Failed to parse public key.");
185197
return Err(Error::Ctap(CtapError::Other));
@@ -578,3 +590,49 @@ where
578590
.await
579591
}
580592
}
593+
594+
#[cfg(test)]
595+
mod tests {
596+
use super::*;
597+
use cosey::{Bytes, EcdhEsHkdf256PublicKey};
598+
599+
fn make_peer_key(x: &[u8], y: &[u8]) -> cose::PublicKey {
600+
cose::PublicKey::EcdhEsHkdf256Key(EcdhEsHkdf256PublicKey {
601+
x: Bytes::from_slice(x).unwrap(),
602+
y: Bytes::from_slice(y).unwrap(),
603+
})
604+
}
605+
606+
#[test]
607+
fn ecdh_rejects_short_x() {
608+
let proto = PinUvAuthProtocolOne::new();
609+
let x = vec![0x01u8; 31];
610+
let y = vec![0x02u8; 32];
611+
let key = make_peer_key(&x, &y);
612+
613+
let result = PinUvAuthProtocol::encapsulate(&proto, &key);
614+
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
615+
}
616+
617+
#[test]
618+
fn ecdh_rejects_empty_x() {
619+
let proto = PinUvAuthProtocolOne::new();
620+
let x: Vec<u8> = Vec::new();
621+
let y = vec![0x02u8; 32];
622+
let key = make_peer_key(&x, &y);
623+
624+
let result = PinUvAuthProtocol::encapsulate(&proto, &key);
625+
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
626+
}
627+
628+
#[test]
629+
fn ecdh_rejects_short_y() {
630+
let proto = PinUvAuthProtocolTwo::new();
631+
let x = vec![0x01u8; 32];
632+
let y = vec![0x02u8; 16];
633+
let key = make_peer_key(&x, &y);
634+
635+
let result = PinUvAuthProtocol::encapsulate(&proto, &key);
636+
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
637+
}
638+
}

0 commit comments

Comments
 (0)