Skip to content

Commit 7fe68dd

Browse files
author
Martin Sirringhaus
committed
Adding tests (and some minor fixes the tests revealed)
1 parent 98b6c13 commit 7fe68dd

File tree

3 files changed

+239
-9
lines changed

3 files changed

+239
-9
lines changed

examples/ctap2_discoverable_creds.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ fn extract_associated_large_blobs(key: Vec<u8>, array: Vec<LargeBlobArrayElement
271271
let plaintext = cipher.decrypt(e.nonce.as_slice().into(), payload).ok();
272272
plaintext
273273
})
274-
.map(|d| flate3::inflate(&d))
274+
.map(|d| flate3::inflate(&d)) // TODO: Check resulting length and compare to orig_size
275275
.map(|d| String::from_utf8_lossy(&d).to_string())
276276
.collect();
277277
valid_elements

src/ctap2/commands/large_blobs.rs

+231-5
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ impl<'de> Deserialize<'de> for LargeBlobArrayElement {
248248
}
249249
}
250250

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

298299
let mut hasher = Sha256::new();
299300
hasher.update(large_blob);
300301
let expected_hash = hasher.finalize();
301302
// 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).
302-
let default_large_blob = [
303-
0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9,
304-
0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c,
303+
let default_large_blob = [0x80];
304+
let default_hash = [
305+
0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d,
306+
0x6f, 0xa5, 0x7a, 0x6d, 0x3c,
305307
];
306308
// 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.
307309
if &expected_hash.as_slice()[0..16] != hash_slice {
308310
warn!("Large blob array hash doesn't match with the expected value! Assuming an empty array.");
309311
large_blob = &default_large_blob;
312+
hash_slice = &default_hash;
310313
}
311314

312315
let byte_len = large_blob.len() as u64;
@@ -449,3 +452,226 @@ where
449452
bytes.extend_from_slice(&hash[..16]);
450453
write_large_blob_segment(dev, keep_alive, &bytes, 0, pin_uv_auth_token)
451454
}
455+
456+
#[cfg(test)]
457+
pub mod tests {
458+
use super::*;
459+
use crate::consts::HIDCmd;
460+
use crate::transport::device_selector::Device;
461+
use crate::transport::hid::HIDDevice;
462+
use crate::transport::platform::device::IN_HID_RPT_SIZE;
463+
use crate::transport::{FidoDevice, FidoProtocol};
464+
use rand::{thread_rng, RngCore};
465+
466+
fn add_bytes_to_read(cid: &[u8], bytes: &[u8], device: &mut Device) {
467+
let mut data = Vec::new();
468+
let payload_len = (bytes.len() + 1) as u16;
469+
// We skip the very first byte (HIDCmd::Cbor), as we will insert it below
470+
data.extend(payload_len.to_be_bytes());
471+
data.push(0x00); // status == success
472+
data.extend(bytes);
473+
let chunks = data.chunks(IN_HID_RPT_SIZE - 5);
474+
for (id, chunk) in chunks.enumerate() {
475+
let mut msg = cid.to_vec();
476+
let state_or_seq = if id == 0 {
477+
HIDCmd::Cbor.into()
478+
} else {
479+
(id - 1) as u8 // SEQ
480+
};
481+
msg.push(state_or_seq);
482+
msg.extend(chunk);
483+
device.add_read(&msg, 0);
484+
}
485+
}
486+
487+
#[test]
488+
fn test_read_large_blob_array() {
489+
let keep_alive = || true;
490+
let mut device = Device::new("commands/large_blobs").unwrap();
491+
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
492+
493+
// 'initialize' the device
494+
let mut cid = [0u8; 4];
495+
thread_rng().fill_bytes(&mut cid);
496+
device.set_cid(cid);
497+
498+
let cmd = [
499+
0xa2, // map(2)
500+
0x01, // unsigned(1) - get
501+
0x19, 0x03, 0xc0, // unsigned(960)
502+
0x03, // unsigned(3) - offset
503+
0x00, // unsigned(0)
504+
];
505+
let mut msg = cid.to_vec();
506+
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
507+
msg.extend(vec![0x0C]); // LargeBlobs
508+
msg.extend(cmd); // Actual command
509+
device.add_write(&msg, 0);
510+
511+
add_bytes_to_read(&cid, &LARGE_BLOB_ARRAY, &mut device);
512+
let array = read_large_blob_array(&mut device, &keep_alive)
513+
.expect("Failed to read large blob array");
514+
let expected = get_expected_large_blobs_response();
515+
assert_eq!(expected, array);
516+
}
517+
518+
#[test]
519+
fn test_read_large_blob_array_with_wrong_hash() {
520+
let keep_alive = || true;
521+
let mut device = Device::new("commands/large_blobs").unwrap();
522+
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
523+
524+
// 'initialize' the device
525+
let mut cid = [0u8; 4];
526+
thread_rng().fill_bytes(&mut cid);
527+
device.set_cid(cid);
528+
529+
let cmd = [
530+
0xa2, // map(2)
531+
0x01, // unsigned(1) - get
532+
0x19, 0x03, 0xc0, // unsigned(960)
533+
0x03, // unsigned(3) - offset
534+
0x00, // unsigned(0)
535+
];
536+
let mut msg = cid.to_vec();
537+
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
538+
msg.extend(vec![0x0C]); // LargeBlobs
539+
msg.extend(cmd); // Actual command
540+
device.add_write(&msg, 0);
541+
542+
let mut payload = LARGE_BLOB_ARRAY;
543+
payload[483] += 1; // Changing one byte in the hash
544+
545+
add_bytes_to_read(&cid, &payload, &mut device);
546+
// Should succeed, but give us the default empty Large blob array, as defined by the spec
547+
let array = read_large_blob_array(&mut device, &keep_alive)
548+
.expect("Failed to read large blob array");
549+
let expected = LargeBlobsResponse {
550+
large_blob_array: vec![],
551+
hash: [
552+
0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a,
553+
0x6d, 0x3c,
554+
],
555+
byte_len: 1,
556+
};
557+
assert_eq!(expected, array);
558+
}
559+
560+
#[test]
561+
fn test_read_large_blob_array_multi_read() {
562+
let keep_alive = || true;
563+
let mut device = Device::new("commands/large_blobs").unwrap();
564+
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
565+
device.set_authenticator_info(crate::AuthenticatorInfo {
566+
max_msg_size: Some(164), // Note: This value minus 64 will be the fragment size
567+
..Default::default()
568+
});
569+
570+
// 'initialize' the device
571+
let mut cid = [0u8; 4];
572+
thread_rng().fill_bytes(&mut cid);
573+
device.set_cid(cid);
574+
575+
for ii in 0..5 {
576+
let mut cmd = vec![
577+
0xa2, // map(2)
578+
0x01, // unsigned(1) - get
579+
0x18, 0x64, // unsigned(100)
580+
0x03, // unsigned(3) - offset
581+
];
582+
cmd.extend(&to_vec(&serde_cbor::Value::Integer(ii * 100)).unwrap());
583+
let mut msg = cid.to_vec();
584+
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, cmd.len() as u8 + 1]); // cmd + bcnt
585+
msg.extend(vec![0x0C]); // LargeBlobs
586+
msg.extend(cmd); // Actual command
587+
device.add_write(&msg, 0);
588+
}
589+
590+
for chunk in LARGE_BLOB_ARRAY.chunks(100) {
591+
add_bytes_to_read(&cid, chunk, &mut device);
592+
}
593+
let array = read_large_blob_array(&mut device, &keep_alive)
594+
.expect("Failed to read large blob array");
595+
let expected = get_expected_large_blobs_response();
596+
assert_eq!(expected, array);
597+
}
598+
599+
fn get_expected_large_blobs_response() -> LargeBlobsResponse {
600+
LargeBlobsResponse {
601+
large_blob_array: vec![
602+
LargeBlobArrayElement {
603+
ciphertext: vec![
604+
116, 199, 82, 206, 68, 131, 237, 242, 213, 144, 244, 185, 155, 148, 217,
605+
62, 245, 5, 128, 162, 176, 99, 5, 160, 186, 68, 88, 140, 38, 255, 168, 254,
606+
88, 161, 188, 30, 113, 221, 67, 21, 88, 43, 211, 17, 190, 252, 14, 186,
607+
225, 200, 135, 186, 168, 255, 232, 51, 151, 183, 194, 134, 160, 250, 191,
608+
141,
609+
],
610+
nonce: [117, 86, 137, 126, 205, 2, 34, 50, 18, 20, 165, 104],
611+
orig_size: 34,
612+
},
613+
LargeBlobArrayElement {
614+
ciphertext: vec![
615+
71, 124, 111, 114, 77, 240, 163, 5, 124, 7, 191, 2, 177, 167, 200, 95, 248,
616+
163, 235, 77, 195, 106, 253, 23, 183, 119, 55, 17, 50, 238, 217, 248, 56,
617+
135, 48, 49, 101, 132, 66, 78, 58, 23, 101, 77, 52, 213, 89, 73, 34, 61,
618+
237, 8, 219, 1, 208, 245, 129, 101, 234, 114, 170, 54, 7, 147, 59, 226, 32,
619+
],
620+
nonce: [99, 132, 251, 236, 134, 156, 86, 195, 121, 49, 205, 162],
621+
orig_size: 36,
622+
},
623+
LargeBlobArrayElement {
624+
ciphertext: vec![
625+
212, 135, 116, 12, 170, 245, 186, 103, 147, 112, 196, 29, 43, 120, 236,
626+
175, 205, 84, 184, 231, 118, 152, 76, 60, 216, 128, 204, 166, 96, 8, 67, 3,
627+
163, 242, 243, 124, 156, 65, 138, 98, 66, 46, 201, 40, 219, 236, 53, 43,
628+
107, 14, 135, 23, 99, 150, 240, 14, 234, 153, 115, 94, 180, 117, 162, 213,
629+
],
630+
nonce: [231, 165, 15, 21, 64, 8, 234, 133, 6, 223, 226, 134],
631+
orig_size: 34,
632+
},
633+
],
634+
hash: [
635+
0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40,
636+
0x28, 0x65,
637+
],
638+
byte_len: 463,
639+
}
640+
}
641+
642+
#[rustfmt::skip]
643+
pub const LARGE_BLOB_ARRAY: [u8; 484] = [
644+
0xa1, // map(1)
645+
0x01, // unsigned(1)
646+
0x59, 0x01, 0xdf, // bytes(479)
647+
0x83, // array(3)
648+
0xa3, // map(3)
649+
0x01, // unsigned(1) - ciphertext
650+
0x98, 0x40, // array(64)
651+
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,
652+
0x02, // unsigned(2) - nonce
653+
0x8c, // array(12)
654+
0x18, 0x75, 0x18, 0x56, 0x18, 0x89, 0x18, 0x7e, 0x18, 0xcd, 0x02, 0x18, 0x22, 0x18, 0x32, 0x12, 0x14, 0x18, 0xa5, 0x18, 0x68,
655+
0x03, // unsigned(3) - origSize
656+
0x18, 0x22, // unsigned(34)
657+
0xa3, // map(3)
658+
0x01, // unsigned(1) - ciphertext
659+
0x98, 0x43, // array(67)
660+
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,
661+
0x02, // unsigned(2)
662+
0x8c, // array(12) - nonce
663+
0x18, 0x63, 0x18, 0x84, 0x18, 0xfb, 0x18, 0xec, 0x18, 0x86, 0x18, 0x9c, 0x18, 0x56, 0x18, 0xc3, 0x18, 0x79, 0x18, 0x31, 0x18, 0xcd, 0x18, 0xa2,
664+
0x03, // unsigned(3) - origSize
665+
0x18, 0x24, // unsigned(36)
666+
0xa3, // map(3)
667+
0x01, // unsigned(1) - ciphertext
668+
0x98, 0x40, // array(64)
669+
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,
670+
0x02, // unsigned(2) - nonce
671+
0x8c, // array(12)
672+
0x18, 0xe7, 0x18, 0xa5, 0x0f, 0x15, 0x18, 0x40, 0x08, 0x18, 0xea, 0x18, 0x85, 0x06, 0x18, 0xdf, 0x18, 0xe2, 0x18, 0x86,
673+
0x03, // unsigned(3) - origSize
674+
0x18, 0x22, // unsigned(34)
675+
0x15, 0xee, 0x84, 0xa0, 0xce, 0x5d, 0xa7, 0xd6, 0x6d, 0x3e, 0xb6, 0xf2, 0xc1, 0x40, 0x28, 0x65 // trailing hash-bytes
676+
];
677+
}

src/ctap2/mod.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -729,9 +729,13 @@ pub fn sign<Dev: FidoDevice>(
729729
// and only return valid elements. But for that, we would need AEAD and DEFLATE-algos.
730730
let large_blob_array =
731731
if has_large_blob && results.iter().any(|f| f.large_blob_key.is_some()) {
732-
large_blobs::read_large_blob_array(dev, alive)
733-
.ok()
734-
.map(|x| x.large_blob_array)
732+
match large_blobs::read_large_blob_array(dev, alive) {
733+
Ok(x) => Some(x.large_blob_array),
734+
Err(e) => {
735+
warn!("Failed to read large blob array: {e:?}");
736+
None
737+
}
738+
}
735739
} else {
736740
None
737741
};

0 commit comments

Comments
 (0)