Skip to content

Commit

Permalink
Adding tests (and some minor fixes the tests revealed)
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Sirringhaus committed Feb 9, 2024
1 parent 98b6c13 commit 7fe68dd
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 9 deletions.
2 changes: 1 addition & 1 deletion examples/ctap2_discoverable_creds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ fn extract_associated_large_blobs(key: Vec<u8>, array: Vec<LargeBlobArrayElement
let plaintext = cipher.decrypt(e.nonce.as_slice().into(), payload).ok();
plaintext
})
.map(|d| flate3::inflate(&d))
.map(|d| flate3::inflate(&d)) // TODO: Check resulting length and compare to orig_size
.map(|d| String::from_utf8_lossy(&d).to_string())
.collect();
valid_elements
Expand Down
236 changes: 231 additions & 5 deletions src/ctap2/commands/large_blobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ impl<'de> Deserialize<'de> for LargeBlobArrayElement {
}
}

#[derive(Default, Debug)]
#[derive(Default, Debug, PartialEq, Eq)]
pub struct LargeBlobsResponse {
pub(crate) large_blob_array: Vec<LargeBlobArrayElement>,
/// Truncated SHA-256 hash of the preceding bytes
Expand Down Expand Up @@ -293,20 +293,23 @@ impl<'de> Deserialize<'de> for LargeBlobsResponse {
));
}
// split off trailing hash-bytes
let (mut large_blob, hash_slice) = payload.split_at(payload.len() - 16);
let (mut large_blob, mut hash_slice) =
payload.split_at(payload.len() - 16);

let mut hasher = Sha256::new();
hasher.update(large_blob);
let expected_hash = hasher.finalize();
// The initial serialized large-blob array is the value of the serialized large-blob array on a fresh authenticator, as well as immediately after a reset. It is the byte string h'8076be8b528d0075f7aae98d6fa57a6d3c', which is an empty CBOR array (80) followed by LEFT(SHA-256(h'80'), 16).
let default_large_blob = [
0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9,
0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c,
let default_large_blob = [0x80];
let default_hash = [
0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d,
0x6f, 0xa5, 0x7a, 0x6d, 0x3c,
];
// Once complete, the platform MUST confirm that the embedded SHA-256 hash is correct, based on the definition above. If not, the configuration is corrupt and the platform MUST discard it and act as if the initial serialized large-blob array was received.
if &expected_hash.as_slice()[0..16] != hash_slice {
warn!("Large blob array hash doesn't match with the expected value! Assuming an empty array.");
large_blob = &default_large_blob;
hash_slice = &default_hash;
}

let byte_len = large_blob.len() as u64;
Expand Down Expand Up @@ -449,3 +452,226 @@ where
bytes.extend_from_slice(&hash[..16]);
write_large_blob_segment(dev, keep_alive, &bytes, 0, pin_uv_auth_token)
}

#[cfg(test)]
pub mod tests {
use super::*;
use crate::consts::HIDCmd;
use crate::transport::device_selector::Device;
use crate::transport::hid::HIDDevice;
use crate::transport::platform::device::IN_HID_RPT_SIZE;
use crate::transport::{FidoDevice, FidoProtocol};
use rand::{thread_rng, RngCore};

fn add_bytes_to_read(cid: &[u8], bytes: &[u8], device: &mut Device) {
let mut data = Vec::new();
let payload_len = (bytes.len() + 1) as u16;
// We skip the very first byte (HIDCmd::Cbor), as we will insert it below
data.extend(payload_len.to_be_bytes());
data.push(0x00); // status == success
data.extend(bytes);
let chunks = data.chunks(IN_HID_RPT_SIZE - 5);
for (id, chunk) in chunks.enumerate() {
let mut msg = cid.to_vec();
let state_or_seq = if id == 0 {
HIDCmd::Cbor.into()
} else {
(id - 1) as u8 // SEQ
};
msg.push(state_or_seq);
msg.extend(chunk);
device.add_read(&msg, 0);
}
}

#[test]
fn test_read_large_blob_array() {
let keep_alive = || true;
let mut device = Device::new("commands/large_blobs").unwrap();
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);

// 'initialize' the device
let mut cid = [0u8; 4];
thread_rng().fill_bytes(&mut cid);
device.set_cid(cid);

let cmd = [
0xa2, // map(2)
0x01, // unsigned(1) - get
0x19, 0x03, 0xc0, // unsigned(960)
0x03, // unsigned(3) - offset
0x00, // unsigned(0)
];
let mut msg = cid.to_vec();
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
msg.extend(vec![0x0C]); // LargeBlobs
msg.extend(cmd); // Actual command
device.add_write(&msg, 0);

add_bytes_to_read(&cid, &LARGE_BLOB_ARRAY, &mut device);
let array = read_large_blob_array(&mut device, &keep_alive)
.expect("Failed to read large blob array");
let expected = get_expected_large_blobs_response();
assert_eq!(expected, array);
}

#[test]
fn test_read_large_blob_array_with_wrong_hash() {
let keep_alive = || true;
let mut device = Device::new("commands/large_blobs").unwrap();
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);

// 'initialize' the device
let mut cid = [0u8; 4];
thread_rng().fill_bytes(&mut cid);
device.set_cid(cid);

let cmd = [
0xa2, // map(2)
0x01, // unsigned(1) - get
0x19, 0x03, 0xc0, // unsigned(960)
0x03, // unsigned(3) - offset
0x00, // unsigned(0)
];
let mut msg = cid.to_vec();
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
msg.extend(vec![0x0C]); // LargeBlobs
msg.extend(cmd); // Actual command
device.add_write(&msg, 0);

let mut payload = LARGE_BLOB_ARRAY;
payload[483] += 1; // Changing one byte in the hash

add_bytes_to_read(&cid, &payload, &mut device);
// Should succeed, but give us the default empty Large blob array, as defined by the spec
let array = read_large_blob_array(&mut device, &keep_alive)
.expect("Failed to read large blob array");
let expected = LargeBlobsResponse {
large_blob_array: vec![],
hash: [
0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a,
0x6d, 0x3c,
],
byte_len: 1,
};
assert_eq!(expected, array);
}

#[test]
fn test_read_large_blob_array_multi_read() {
let keep_alive = || true;
let mut device = Device::new("commands/large_blobs").unwrap();
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
device.set_authenticator_info(crate::AuthenticatorInfo {
max_msg_size: Some(164), // Note: This value minus 64 will be the fragment size
..Default::default()
});

// 'initialize' the device
let mut cid = [0u8; 4];
thread_rng().fill_bytes(&mut cid);
device.set_cid(cid);

for ii in 0..5 {
let mut cmd = vec![
0xa2, // map(2)
0x01, // unsigned(1) - get
0x18, 0x64, // unsigned(100)
0x03, // unsigned(3) - offset
];
cmd.extend(&to_vec(&serde_cbor::Value::Integer(ii * 100)).unwrap());
let mut msg = cid.to_vec();
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
msg.extend(vec![0x0C]); // LargeBlobs
msg.extend(cmd); // Actual command
device.add_write(&msg, 0);
}

for chunk in LARGE_BLOB_ARRAY.chunks(100) {
add_bytes_to_read(&cid, chunk, &mut device);
}
let array = read_large_blob_array(&mut device, &keep_alive)
.expect("Failed to read large blob array");
let expected = get_expected_large_blobs_response();
assert_eq!(expected, array);
}

fn get_expected_large_blobs_response() -> LargeBlobsResponse {
LargeBlobsResponse {
large_blob_array: vec![
LargeBlobArrayElement {
ciphertext: vec![
116, 199, 82, 206, 68, 131, 237, 242, 213, 144, 244, 185, 155, 148, 217,
62, 245, 5, 128, 162, 176, 99, 5, 160, 186, 68, 88, 140, 38, 255, 168, 254,
88, 161, 188, 30, 113, 221, 67, 21, 88, 43, 211, 17, 190, 252, 14, 186,
225, 200, 135, 186, 168, 255, 232, 51, 151, 183, 194, 134, 160, 250, 191,
141,
],
nonce: [117, 86, 137, 126, 205, 2, 34, 50, 18, 20, 165, 104],
orig_size: 34,
},
LargeBlobArrayElement {
ciphertext: vec![
71, 124, 111, 114, 77, 240, 163, 5, 124, 7, 191, 2, 177, 167, 200, 95, 248,
163, 235, 77, 195, 106, 253, 23, 183, 119, 55, 17, 50, 238, 217, 248, 56,
135, 48, 49, 101, 132, 66, 78, 58, 23, 101, 77, 52, 213, 89, 73, 34, 61,
237, 8, 219, 1, 208, 245, 129, 101, 234, 114, 170, 54, 7, 147, 59, 226, 32,
],
nonce: [99, 132, 251, 236, 134, 156, 86, 195, 121, 49, 205, 162],
orig_size: 36,
},
LargeBlobArrayElement {
ciphertext: vec![
212, 135, 116, 12, 170, 245, 186, 103, 147, 112, 196, 29, 43, 120, 236,
175, 205, 84, 184, 231, 118, 152, 76, 60, 216, 128, 204, 166, 96, 8, 67, 3,
163, 242, 243, 124, 156, 65, 138, 98, 66, 46, 201, 40, 219, 236, 53, 43,
107, 14, 135, 23, 99, 150, 240, 14, 234, 153, 115, 94, 180, 117, 162, 213,
],
nonce: [231, 165, 15, 21, 64, 8, 234, 133, 6, 223, 226, 134],
orig_size: 34,
},
],
hash: [
0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40,
0x28, 0x65,
],
byte_len: 463,
}
}

#[rustfmt::skip]
pub const LARGE_BLOB_ARRAY: [u8; 484] = [
0xa1, // map(1)
0x01, // unsigned(1)
0x59, 0x01, 0xdf, // bytes(479)
0x83, // array(3)
0xa3, // map(3)
0x01, // unsigned(1) - ciphertext
0x98, 0x40, // array(64)
0x18, 0x74, 0x18, 0xc7, 0x18, 0x52, 0x18, 0xce, 0x18, 0x44, 0x18, 0x83, 0x18, 0xed, 0x18, 0xf2, 0x18, 0xd5, 0x18, 0x90, 0x18, 0xf4, 0x18, 0xb9, 0x18, 0x9b, 0x18, 0x94, 0x18, 0xd9, 0x18, 0x3e, 0x18, 0xf5, 0x05, 0x18, 0x80, 0x18, 0xa2, 0x18, 0xb0, 0x18, 0x63, 0x05, 0x18, 0xa0, 0x18, 0xba, 0x18, 0x44, 0x18, 0x58, 0x18, 0x8c, 0x18, 0x26, 0x18, 0xff, 0x18, 0xa8, 0x18, 0xfe, 0x18, 0x58, 0x18, 0xa1, 0x18, 0xbc, 0x18, 0x1e, 0x18, 0x71, 0x18, 0xdd, 0x18, 0x43, 0x15, 0x18, 0x58, 0x18, 0x2b, 0x18, 0xd3, 0x11, 0x18, 0xbe, 0x18, 0xfc, 0x0e, 0x18, 0xba, 0x18, 0xe1, 0x18, 0xc8, 0x18, 0x87, 0x18, 0xba, 0x18, 0xa8, 0x18, 0xff, 0x18, 0xe8, 0x18, 0x33, 0x18, 0x97, 0x18, 0xb7, 0x18, 0xc2, 0x18, 0x86, 0x18, 0xa0, 0x18, 0xfa, 0x18, 0xbf, 0x18, 0x8d,
0x02, // unsigned(2) - nonce
0x8c, // array(12)
0x18, 0x75, 0x18, 0x56, 0x18, 0x89, 0x18, 0x7e, 0x18, 0xcd, 0x02, 0x18, 0x22, 0x18, 0x32, 0x12, 0x14, 0x18, 0xa5, 0x18, 0x68,
0x03, // unsigned(3) - origSize
0x18, 0x22, // unsigned(34)
0xa3, // map(3)
0x01, // unsigned(1) - ciphertext
0x98, 0x43, // array(67)
0x18, 0x47, 0x18, 0x7c, 0x18, 0x6f, 0x18, 0x72, 0x18, 0x4d, 0x18, 0xf0, 0x18, 0xa3, 0x05, 0x18, 0x7c, 0x07, 0x18, 0xbf, 0x02, 0x18, 0xb1, 0x18, 0xa7, 0x18, 0xc8, 0x18, 0x5f, 0x18, 0xf8, 0x18, 0xa3, 0x18, 0xeb, 0x18, 0x4d, 0x18, 0xc3, 0x18, 0x6a, 0x18, 0xfd, 0x17, 0x18, 0xb7, 0x18, 0x77, 0x18, 0x37, 0x11, 0x18, 0x32, 0x18, 0xee, 0x18, 0xd9, 0x18, 0xf8, 0x18, 0x38, 0x18, 0x87, 0x18, 0x30, 0x18, 0x31, 0x18, 0x65, 0x18, 0x84, 0x18, 0x42, 0x18, 0x4e, 0x18, 0x3a, 0x17, 0x18, 0x65, 0x18, 0x4d, 0x18, 0x34, 0x18, 0xd5, 0x18, 0x59, 0x18, 0x49, 0x18, 0x22, 0x18, 0x3d, 0x18, 0xed, 0x08, 0x18, 0xdb, 0x01, 0x18, 0xd0, 0x18, 0xf5, 0x18, 0x81, 0x18, 0x65, 0x18, 0xea, 0x18, 0x72, 0x18, 0xaa, 0x18, 0x36, 0x07, 0x18, 0x93, 0x18, 0x3b, 0x18, 0xe2, 0x18, 0x20,
0x02, // unsigned(2)
0x8c, // array(12) - nonce
0x18, 0x63, 0x18, 0x84, 0x18, 0xfb, 0x18, 0xec, 0x18, 0x86, 0x18, 0x9c, 0x18, 0x56, 0x18, 0xc3, 0x18, 0x79, 0x18, 0x31, 0x18, 0xcd, 0x18, 0xa2,
0x03, // unsigned(3) - origSize
0x18, 0x24, // unsigned(36)
0xa3, // map(3)
0x01, // unsigned(1) - ciphertext
0x98, 0x40, // array(64)
0x18, 0xd4, 0x18, 0x87, 0x18, 0x74, 0x0c, 0x18, 0xaa, 0x18, 0xf5, 0x18, 0xba, 0x18, 0x67, 0x18, 0x93, 0x18, 0x70, 0x18, 0xc4, 0x18, 0x1d, 0x18, 0x2b, 0x18, 0x78, 0x18, 0xec, 0x18, 0xaf, 0x18, 0xcd, 0x18, 0x54, 0x18, 0xb8, 0x18, 0xe7, 0x18, 0x76, 0x18, 0x98, 0x18, 0x4c, 0x18, 0x3c, 0x18, 0xd8, 0x18, 0x80, 0x18, 0xcc, 0x18, 0xa6, 0x18, 0x60, 0x08, 0x18, 0x43, 0x03, 0x18, 0xa3, 0x18, 0xf2, 0x18, 0xf3, 0x18, 0x7c, 0x18, 0x9c, 0x18, 0x41, 0x18, 0x8a, 0x18, 0x62, 0x18, 0x42, 0x18, 0x2e, 0x18, 0xc9, 0x18, 0x28, 0x18, 0xdb, 0x18, 0xec, 0x18, 0x35, 0x18, 0x2b, 0x18, 0x6b, 0x0e, 0x18, 0x87, 0x17, 0x18, 0x63, 0x18, 0x96, 0x18, 0xf0, 0x0e, 0x18, 0xea, 0x18, 0x99, 0x18, 0x73, 0x18, 0x5e, 0x18, 0xb4, 0x18, 0x75, 0x18, 0xa2, 0x18, 0xd5,
0x02, // unsigned(2) - nonce
0x8c, // array(12)
0x18, 0xe7, 0x18, 0xa5, 0x0f, 0x15, 0x18, 0x40, 0x08, 0x18, 0xea, 0x18, 0x85, 0x06, 0x18, 0xdf, 0x18, 0xe2, 0x18, 0x86,
0x03, // unsigned(3) - origSize
0x18, 0x22, // unsigned(34)
0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40, 0x28, 0x65 // trailing hash-bytes
];
}
10 changes: 7 additions & 3 deletions src/ctap2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,9 +729,13 @@ pub fn sign<Dev: FidoDevice>(
// and only return valid elements. But for that, we would need AEAD and DEFLATE-algos.
let large_blob_array =
if has_large_blob && results.iter().any(|f| f.large_blob_key.is_some()) {
large_blobs::read_large_blob_array(dev, alive)
.ok()
.map(|x| x.large_blob_array)
match large_blobs::read_large_blob_array(dev, alive) {
Ok(x) => Some(x.large_blob_array),
Err(e) => {
warn!("Failed to read large blob array: {e:?}");
None
}
}
} else {
None
};
Expand Down

0 comments on commit 7fe68dd

Please sign in to comment.