Skip to content
Merged
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
14 changes: 11 additions & 3 deletions libwebauthn/src/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,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 @@ -438,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
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
44 changes: 27 additions & 17 deletions libwebauthn/src/transport/ble/framing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,25 @@ impl BleFrameParser {
));
}

let cmd = self.fragments[0][0].try_into().or(Err(IOError::new(
let (initial, continuations) = self
.fragments
.split_first()
.ok_or_else(|| IOError::new(IOErrorKind::InvalidData, "Frame has no fragments"))?;
let cmd_byte = *initial
.first()
.ok_or_else(|| IOError::new(IOErrorKind::InvalidData, "Initial fragment is empty"))?;
let cmd = cmd_byte.try_into().or(Err(IOError::new(
IOErrorKind::InvalidData,
format!("Invalid BLE frame command: {:x}", self.fragments[0][0]),
format!("Invalid BLE frame command: {:x}", cmd_byte),
)))?;
let mut data = vec![];
data.extend(&self.fragments[0][INITIAL_FRAGMENT_HEADER_LENGTH..self.fragments[0].len()]);
for cont_fragment in &self.fragments[1..self.fragments.len()] {
data.extend_from_slice(
&cont_fragment[CONT_FRAGMENT_HEADER_LENGTH..cont_fragment.len()],
);
if let Some(initial_data) = initial.get(INITIAL_FRAGMENT_HEADER_LENGTH..) {
data.extend(initial_data);
}
for cont_fragment in continuations {
if let Some(cont_data) = cont_fragment.get(CONT_FRAGMENT_HEADER_LENGTH..) {
data.extend_from_slice(cont_data);
}
}

Ok(BleFrame::new(cmd, &data))
Expand All @@ -154,22 +163,23 @@ impl BleFrameParser {
}

fn expected_bytes(&self) -> Option<usize> {
if self.fragments.is_empty() {
return None;
}

let mut cursor = IOCursor::new(vec![self.fragments[0][1], self.fragments[0][2]]);
let initial = self.fragments.first()?;
let b1 = *initial.get(1)?;
let b2 = *initial.get(2)?;
let mut cursor = IOCursor::new(vec![b1, b2]);
Some(cursor.read_u16::<BigEndian>().ok()? as usize)
}

fn data_len(&self) -> usize {
if self.fragments.is_empty() {
let Some((initial, continuations)) = self.fragments.split_first() else {
return 0;
}
};

let mut data_len = self.fragments[0].len() - INITIAL_FRAGMENT_HEADER_LENGTH;
for cont_fragment in &self.fragments[1..self.fragments.len()] {
data_len += cont_fragment.len() - CONT_FRAGMENT_HEADER_LENGTH;
let mut data_len = initial.len().saturating_sub(INITIAL_FRAGMENT_HEADER_LENGTH);
for cont_fragment in continuations {
data_len += cont_fragment
.len()
.saturating_sub(CONT_FRAGMENT_HEADER_LENGTH);
}
data_len
}
Expand Down
19 changes: 13 additions & 6 deletions libwebauthn/src/transport/cable/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ pub fn derive(secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose) -> Result
}

fn reserved_bits_are_zero(plaintext: &[u8]) -> bool {
plaintext[0] == 0
plaintext.first().copied() == Some(0)
}

#[instrument]
pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[u8; 16]> {
// Both lengths are checked up front so the subsequent slicing is in bounds;
// use `.get(..)` regardless so the clippy::indexing_slicing lint is satisfied.
if candidate_advert.len() != 20 {
warn!("candidate advert is not 20 bytes");
return None;
Expand All @@ -41,15 +43,20 @@ pub fn trial_decrypt_advert(eid_key: &[u8], candidate_advert: &[u8]) -> Option<[
return None;
}

let expected_tag = hmac_sha256(&eid_key[32..], &candidate_advert[..16]).ok()?;
if expected_tag[..4] != candidate_advert[16..] {
warn!({ expected = ?expected_tag[..4], actual = ?candidate_advert[16..] },
let mac_key = eid_key.get(32..)?;
let advert_body = candidate_advert.get(..16)?;
let advert_tag = candidate_advert.get(16..)?;
let expected_tag = hmac_sha256(mac_key, advert_body).ok()?;
let expected_tag_truncated = expected_tag.get(..4)?;
if expected_tag_truncated != advert_tag {
warn!({ expected = ?expected_tag_truncated, actual = ?advert_tag },
"candidate advert HMAC tag does not match");
return None;
}

let cipher = Aes256::new(GenericArray::from_slice(&eid_key[..32]));
let mut block = Block::clone_from_slice(&candidate_advert[..16]);
let aes_key = eid_key.get(..32)?;
let cipher = Aes256::new(GenericArray::from_slice(aes_key));
let mut block = Block::clone_from_slice(advert_body);
cipher.decrypt_block(&mut block);

if !reserved_bits_are_zero(&block) {
Expand Down
27 changes: 13 additions & 14 deletions libwebauthn/src/transport/cable/digit_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,22 @@ const PARTIAL_CHUNK_DIGITS: usize = 0x0fda8530;

pub fn digit_encode(input: &[u8]) -> String {
let mut output = String::new();
let mut input = input;
while input.len() >= CHUNK_SIZE {
for chunk_slice in input.chunks(CHUNK_SIZE) {
let mut chunk = [0u8; 8];
chunk[..CHUNK_SIZE].copy_from_slice(&input[..CHUNK_SIZE]);
let (head, _) = chunk.split_at_mut(chunk_slice.len());
head.copy_from_slice(chunk_slice);
let v = u64::from_le_bytes(chunk);
let v = v.to_string();
output.push_str(&ZEROS[..CHUNK_DIGITS - v.len()]);
output.push_str(&v);
input = &input[CHUNK_SIZE..];
}
if !input.is_empty() {
let digits = 0x0F & (PARTIAL_CHUNK_DIGITS >> (4 * input.len()));
let mut chunk = [0u8; 8];
chunk[..input.len()].copy_from_slice(input);
let v = u64::from_le_bytes(chunk);
let v = v.to_string();
output.push_str(&ZEROS[..digits - v.len()]);
let digits = if chunk_slice.len() == CHUNK_SIZE {
CHUNK_DIGITS
} else {
0x0F & (PARTIAL_CHUNK_DIGITS >> (4 * chunk_slice.len()))
};
// ZEROS is 17 chars (CHUNK_DIGITS); slice within bounds.
let pad_len = digits.saturating_sub(v.len());
if let Some(pad) = ZEROS.get(..pad_len) {
output.push_str(pad);
}
output.push_str(&v);
}
output
Expand Down
Loading
Loading