Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion libwebauthn/src/fido.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData<T> {
};

// Check if we have trailing data
if !&data[cursor.position() as usize..].is_empty() {
let pos = cursor.position() as usize;
let trailing = data.get(pos..).ok_or_else(|| {
DesError::custom("cursor advanced past end of authenticator data")
})?;
if !trailing.is_empty() {
return Err(DesError::invalid_length(data.len(), &"trailing data"));
}

Expand Down
3 changes: 2 additions & 1 deletion libwebauthn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Deny panic-inducing patterns in production code.
// Tests and the virt test-utility feature are allowed to use unwrap/expect/panic for convenience.
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unwrap_used))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::expect_used))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::panic))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::todo))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unreachable))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::indexing_slicing))]
#![cfg_attr(not(any(test, feature = "virt")), deny(clippy::unwrap_in_result))]

#[cfg(all(
feature = "nfc",
Expand Down
13 changes: 12 additions & 1 deletion libwebauthn/src/ops/u2f.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for Regis
y: y.into(),
});
let cose_encoded_public_key = cbor::to_vec(&cose_public_key)?;
assert!(cose_encoded_public_key.len() == 77);
// Canonical CBOR encoding of the COSE P-256 key is 77 bytes for the
// fields we set; return a typed error if a future encoder change
// produces a different length rather than `assert!`-panicking.
if cose_encoded_public_key.len() != 77 {
error!(
len = cose_encoded_public_key.len(),
"COSE-encoded P-256 public key is not 77 bytes"
);
return Err(Error::Platform(PlatformError::CryptoError(
"unexpected COSE-encoded public key length".into(),
)));
}

// Let attestedCredData be a byte string with following structure:
//
Expand Down
139 changes: 128 additions & 11 deletions libwebauthn/src/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,23 @@ where
);
return Err(Error::Ctap(CtapError::Other));
};
let encoded_point = EncodedPoint::from_affine_coordinates(
peer_public_key.x.as_bytes().into(),
peer_public_key.y.as_bytes().into(),
false,
);
// x and y must be exactly 32 bytes (P-256 field size). `cosey` accepts
// any length up to 32; validate before converting to `&FieldBytes`.
let x: &[u8; 32] = peer_public_key.x.as_bytes().try_into().map_err(|_| {
error!(
x_len = peer_public_key.x.as_bytes().len(),
"Peer public key x coordinate is not 32 bytes"
);
Error::Ctap(CtapError::Other)
})?;
let y: &[u8; 32] = peer_public_key.y.as_bytes().try_into().map_err(|_| {
error!(
y_len = peer_public_key.y.as_bytes().len(),
"Peer public key y coordinate is not 32 bytes"
);
Error::Ctap(CtapError::Other)
})?;
let encoded_point = EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
let Some(peer_public_key) = P256PublicKey::from_encoded_point(&encoded_point).into() else {
error!("Failed to parse public key.");
return Err(Error::Ctap(CtapError::Other));
Expand Down Expand Up @@ -250,7 +262,14 @@ impl PinUvAuthProtocol for PinUvAuthProtocolOne {
fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
// Return the first 16 bytes of the result of computing HMAC-SHA-256 with the given key and message.
let hmac = hmac_sha256(key, message)?;
Ok(Vec::from(&hmac[..16]))
// HMAC-SHA-256 produces 32 bytes, so this slice is always valid.
let truncated = hmac.get(..16).ok_or_else(|| {
error!(len = hmac.len(), "HMAC output shorter than 16 bytes");
Error::Platform(PlatformError::CryptoError(
"HMAC output shorter than 16 bytes".into(),
))
})?;
Ok(Vec::from(truncated))
}

#[instrument(skip_all)]
Expand Down Expand Up @@ -350,7 +369,13 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo {

fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
// Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.)
let key = &key[32..];
let key = key.get(32..).ok_or_else(|| {
error!(
key_len = key.len(),
"key shorter than 32 bytes; cannot select AES-key portion"
);
Error::Ctap(CtapError::Other)
})?;

// Let iv be a 16-byte, random bytestring.
let iv: [u8; 16] = thread_rng().gen();
Expand All @@ -371,7 +396,13 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo {

fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
// Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.)
let key = &key[32..];
let key = key.get(32..).ok_or_else(|| {
error!(
key_len = key.len(),
"key shorter than 32 bytes; cannot select AES-key portion"
);
Error::Ctap(CtapError::Other)
})?;

// If demPlaintext is less than 16 bytes in length, return an error
if ciphertext.len() < 16 {
Expand All @@ -397,7 +428,13 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo {
fn authenticate(&self, key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
// If key is longer than 32 bytes, discard the excess. (This selects the HMAC-key portion of the shared secret.
// When key is the pinUvAuthToken, it is exactly 32 bytes long and thus this step has no effect.)
let key = &key[..32];
let key = key.get(..32).ok_or_else(|| {
error!(
key_len = key.len(),
"key shorter than 32 bytes; cannot select HMAC-key portion"
);
Error::Ctap(CtapError::Other)
})?;

// Return the result of computing HMAC-SHA-256 on key and message.
hmac_sha256(key, message)
Expand All @@ -408,8 +445,9 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo {
pub fn pin_hash(pin: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::default();
hasher.update(pin);
let hashed = hasher.finalize().to_vec();
Vec::from(&hashed[..16])
let hashed = hasher.finalize();
// SHA-256 output is fixed at 32 bytes; keep only the first 16 per the spec.
hashed.into_iter().take(16).collect()
}

pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result<Vec<u8>, Error> {
Expand Down Expand Up @@ -578,3 +616,82 @@ where
.await
}
}

#[cfg(test)]
mod tests {
use super::*;
use cosey::{Bytes, EcdhEsHkdf256PublicKey};

fn make_peer_key(x: &[u8], y: &[u8]) -> cose::PublicKey {
cose::PublicKey::EcdhEsHkdf256Key(EcdhEsHkdf256PublicKey {
x: Bytes::from_slice(x).unwrap(),
y: Bytes::from_slice(y).unwrap(),
})
}

#[test]
fn ecdh_rejects_short_x() {
let proto = PinUvAuthProtocolOne::new();
let x = vec![0x01u8; 31];
let y = vec![0x02u8; 32];
let key = make_peer_key(&x, &y);

let result = PinUvAuthProtocol::encapsulate(&proto, &key);
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn ecdh_rejects_empty_x() {
let proto = PinUvAuthProtocolOne::new();
let x: Vec<u8> = Vec::new();
let y = vec![0x02u8; 32];
let key = make_peer_key(&x, &y);

let result = PinUvAuthProtocol::encapsulate(&proto, &key);
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn ecdh_rejects_short_y() {
let proto = PinUvAuthProtocolTwo::new();
let x = vec![0x01u8; 32];
let y = vec![0x02u8; 16];
let key = make_peer_key(&x, &y);

let result = PinUvAuthProtocol::encapsulate(&proto, &key);
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn proto_two_authenticate_rejects_empty_key() {
let proto = PinUvAuthProtocolTwo::new();
let result = proto.authenticate(&[], b"clientDataHash");
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn proto_two_authenticate_rejects_short_key() {
let proto = PinUvAuthProtocolTwo::new();
let short_key = [0u8; 16];
let result = proto.authenticate(&short_key, b"hello");
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn proto_two_encrypt_rejects_short_key() {
let proto = PinUvAuthProtocolTwo::new();
let short_key = [0u8; 16];
let plaintext = [0u8; 16];
let result = proto.encrypt(&short_key, &plaintext);
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}

#[test]
fn proto_two_decrypt_rejects_short_key() {
let proto = PinUvAuthProtocolTwo::new();
let short_key = [0u8; 16];
let ct = [0u8; 32];
let result = proto.decrypt(&short_key, &ct);
assert!(matches!(result, Err(Error::Ctap(CtapError::Other))));
}
}
23 changes: 15 additions & 8 deletions libwebauthn/src/proto/ctap1/apdu/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,26 @@ impl ApduResponse {
impl TryFrom<&Vec<u8>> for ApduResponse {
type Error = IOError;
fn try_from(packet: &Vec<u8>) -> Result<Self, Self::Error> {
if packet.len() < 2 {
return Err(IOError::new(
let split_at = packet.len().checked_sub(2).ok_or_else(|| {
IOError::new(
IOErrorKind::InvalidData,
"Apdu response packets must contain at least 2 bytes.",
));
}
)
})?;
let (body, status) = packet.split_at(split_at);
// `status` is guaranteed to have exactly 2 elements by construction above.
let sw1 = *status
.first()
.ok_or_else(|| IOError::new(IOErrorKind::InvalidData, "Missing APDU status byte 1"))?;
let sw2 = *status
.get(1)
.ok_or_else(|| IOError::new(IOErrorKind::InvalidData, "Missing APDU status byte 2"))?;

let data = if packet.len() > 2 {
Some(Vec::from(&packet[0..packet.len() - 2]))
} else {
let data = if body.is_empty() {
None
} else {
Some(Vec::from(body))
};
let (sw1, sw2) = (packet[packet.len() - 2], packet[packet.len() - 1]);

Ok(Self { data, sw1, sw2 })
}
Expand Down
13 changes: 12 additions & 1 deletion libwebauthn/src/proto/ctap1/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,18 @@ impl TryFrom<ApduResponse> for Ctap1RegisterResponse {
"Failed to parse X509 attestation data",
)))?;
let signature = Vec::from(signature);
let attestation = Vec::from(&remaining[0..remaining.len() - signature.len()]);
// `signature` is a suffix of `remaining` returned by `X509Certificate::from_der`,
// so the split index is always within bounds in practice; bound it explicitly.
let split_at = remaining
.len()
.checked_sub(signature.len())
.ok_or_else(|| {
IOError::new(
IOErrorKind::InvalidData,
"Signature longer than remaining U2F register response data",
)
})?;
let attestation = Vec::from(remaining.get(..split_at).unwrap_or(&[]));

Ok(Ctap1RegisterResponse {
version: Ctap1Version::U2fV2,
Expand Down
20 changes: 10 additions & 10 deletions libwebauthn/src/proto/ctap2/cbor/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ impl CborResponse {
impl TryFrom<&Vec<u8>> for CborResponse {
type Error = IOError;
fn try_from(packet: &Vec<u8>) -> Result<Self, Self::Error> {
if packet.is_empty() {
return Err(IOError::new(
let (status_byte, body) = packet.split_first().ok_or_else(|| {
IOError::new(
IOErrorKind::InvalidData,
"Cbor response packets must contain at least 1 byte.",
));
}
)
})?;

let Ok(status_code) = packet[0].try_into() else {
error!({ code = ?packet[0] }, "Invalid CTAP error code");
let Ok(status_code) = (*status_byte).try_into() else {
error!({ code = ?*status_byte }, "Invalid CTAP error code");
return Err(IOError::new(
IOErrorKind::InvalidData,
format!("Invalid CTAP error code: {:x}", packet[0]),
format!("Invalid CTAP error code: {:x}", status_byte),
));
};

let data = if packet.len() > 1 {
Some(Vec::from(&packet[1..]))
} else {
let data = if body.is_empty() {
None
} else {
Some(Vec::from(body))
};
Ok(CborResponse { status_code, data })
}
Expand Down
13 changes: 6 additions & 7 deletions libwebauthn/src/proto/ctap2/model/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,19 +332,18 @@ impl Ctap2GetAssertionRequestExtensions {

let mut hasher = Sha256::default();
hasher.update(salt1_input);
let salt1_hash = hasher.finalize().to_vec();
input.salt1.copy_from_slice(&salt1_hash[..32]);
// SHA-256 produces a fixed 32-byte output, which lines up with salt1.
let salt1_hash: [u8; 32] = hasher.finalize().into();
input.salt1.copy_from_slice(&salt1_hash);

// 5.2 If ev.second is present, let salt2 be the value of SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || ev.second).
if let Some(second) = ev.second {
let mut salt2_input = prefix.clone();
salt2_input.extend(second);
let mut hasher = Sha256::default();
hasher.update(salt2_input);
let salt2_hash = hasher.finalize().to_vec();
let mut salt2 = [0u8; 32];
salt2.copy_from_slice(&salt2_hash[..32]);
input.salt2 = Some(salt2);
let salt2_hash: [u8; 32] = hasher.finalize().into();
input.salt2 = Some(salt2_hash);
};

Ok(Some(input))
Expand Down Expand Up @@ -481,7 +480,7 @@ impl Ctap2GetAssertionResponse {
// We always return it, for convenience.
let credential_id = self.credential_id.or_else(|| {
if request.allow.len() == 1 {
Some(request.allow[0].clone())
request.allow.first().cloned()
} else {
None
}
Expand Down
24 changes: 23 additions & 1 deletion libwebauthn/src/proto/ctap2/model/get_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,16 @@ impl Ctap2GetInfoResponse {

// If we do have a PIN, check if we need to use legacy getPinToken or new getPinUvAuthToken..-command
if self.option_enabled("pinUvAuthToken") {
assert!(self.option_enabled("clientPin"));
// `pinUvAuthToken` and `clientPin` are independent options
// (CTAP 2.2 §6.4). Tolerate the combination without `clientPin`
// and fall back to a shared-secret-only flow.
if !self.option_enabled("clientPin") {
debug!(
"Device advertises pinUvAuthToken without clientPin; \
falling back to OnlyForSharedSecret"
);
return Some(Ctap2UserVerificationOperation::OnlyForSharedSecret);
}
debug!("getPinUvAuthTokenUsingPinWithPermissions");
Some(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions)
} else if self.option_enabled("clientPin") {
Expand Down Expand Up @@ -574,4 +583,17 @@ mod test {
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}

#[test]
fn device_pin_uv_auth_token_without_client_pin_does_not_panic() {
let info = create_info(&[("pinUvAuthToken", true)]);
assert_eq!(
info.uv_operation(false),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
assert_eq!(
info.uv_operation(true),
Some(Ctap2UserVerificationOperation::OnlyForSharedSecret)
);
}
}
Loading
Loading