From 8a487544f08ad28198fdb1455f03610b9a5ecf3f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 24 Jun 2024 15:42:18 +0200 Subject: [PATCH] Extend serialization tests This patch adds de-/serialization tests for the modules ctap1, ctap2::client_pin and ctap2::large_blobs. See also: https://github.com/trussed-dev/ctap-types/issues/54 --- Cargo.toml | 5 +- src/ctap1.rs | 140 ++++++++++++++ src/ctap2/client_pin.rs | 381 ++++++++++++++++++++++++++++++++++++++- src/ctap2/large_blobs.rs | 87 +++++++++ 4 files changed, 610 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ccc0f1..8eb885f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,18 @@ repository = "https://github.com/trussed-dev/ctap-types" [dependencies] bitflags = "1.3" cbor-smol = "0.4" -cosey = "0.3" +cosey = "0.3.1" delog = "0.1" heapless = { version = "0.7", default-features = false, features = ["serde"] } heapless-bytes = "0.3" -iso7816 = "0.1" +iso7816 = "0.1.2" serde = { version = "1", default-features = false, features = ["derive"] } serde-indexed = "0.1.1" serde_bytes = { version = "0.11.14", default-features = false } serde_repr = "0.1" [dev-dependencies] +hex-literal = "0.4.1" serde_test = "1.0.176" [features] diff --git a/src/ctap1.rs b/src/ctap1.rs index def7486..13e0639 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -253,3 +253,143 @@ impl crate::Rpc, Response> for A { self.call_ctap1(request) } } + +#[cfg(test)] +mod tests { + use super::*; + use heapless::Vec; + use hex_literal::hex; + use iso7816::command::{ + class::Class, instruction::Instruction, Command, CommandBuilder, ExpectedLen, + }; + + // examples taken from: + // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#examples + + fn command(ins: u8, p1: u8, p2: u8, data: &[u8]) -> Command<1024> { + let builder = CommandBuilder::new( + Class::from_byte(0).unwrap(), + Instruction::from(ins), + p1, + p2, + data, + ExpectedLen::Max, + ); + let mut apdu = Vec::<_, 1024>::new(); + builder.serialize_into(&mut apdu).unwrap(); + Command::try_from(&apdu).unwrap() + } + + #[test] + fn test_register_request() { + let mut input = [0; 64]; + // challenge + input[..32].copy_from_slice(&hex!( + "4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb" + )); + // application + input[32..].copy_from_slice(&hex!( + "f0e6a6a97042a4f1f1c87f5f7d44315b2d852c2df5c7991cc66241bf7072d1c4" + )); + + let command = command(1, 0, 0, &input); + let request = Request::try_from(&command).unwrap(); + let Request::Register(request) = request else { + panic!("expected register request, got: {:?}", request); + }; + assert_eq!(request.challenge, &input[..32]); + assert_eq!(request.app_id, &input[32..]); + } + + #[test] + fn test_register_response() { + let public_key = hex!("b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9"); + let public_key = cosey::EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&public_key[..32]).unwrap(), + y: Bytes::from_slice(&public_key[32..]).unwrap(), + }; + let key_handle = hex!("2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25"); + let key_handle = Bytes::from_slice(&key_handle).unwrap(); + let signature = hex!("304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871"); + let signature = Bytes::from_slice(&signature).unwrap(); + let attestation_certificate = hex!("3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df"); + let attestation_certificate = Bytes::from_slice(&attestation_certificate).unwrap(); + let response = register::Response::new( + 0x05, + &public_key, + key_handle, + signature, + attestation_certificate, + ); + let mut output = Vec::<_, 1024>::new(); + Response::Register(response).serialize(&mut output).unwrap(); + assert_eq!( + output.as_slice(), + &hex!("0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871"), + ); + } + + #[test] + fn test_authenticate_request() { + let challenge = &hex!("ccd6ee2e47baef244d49a222db496bad0ef5b6f93aa7cc4d30c4821b3b9dbc57"); + let application = &hex!("4b0be934baebb5d12d26011b69227fa5e86df94e7d94aa2949a89f2d493992ca"); + let key_handle = &hex!("2a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c25"); + let mut input = Vec::<_, 1024>::new(); + input.extend_from_slice(challenge).unwrap(); + input.extend_from_slice(application).unwrap(); + input.push(u8::try_from(key_handle.len()).unwrap()).unwrap(); + input.extend_from_slice(key_handle).unwrap(); + + let control_bytes = [ + (0x07, ControlByte::CheckOnly), + (0x03, ControlByte::EnforceUserPresenceAndSign), + (0x08, ControlByte::DontEnforceUserPresenceAndSign), + ]; + + for (byte, variant) in control_bytes { + let command = command(2, byte, 0, &input); + let request = Request::try_from(&command).unwrap(); + let Request::Authenticate(request) = request else { + panic!("expected authenticate request, got: {:?}", request); + }; + assert_eq!(request.control_byte, variant); + assert_eq!(request.challenge, challenge); + assert_eq!(request.app_id, application); + assert_eq!(request.key_handle, key_handle); + } + } + + #[test] + fn test_authenticate_response() { + let signature = &hex!("304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f"); + let signature = Bytes::from_slice(signature).unwrap(); + let response = authenticate::Response { + user_presence: 1, + count: 1, + signature, + }; + let mut output = Vec::<_, 1024>::new(); + Response::Authenticate(response) + .serialize(&mut output) + .unwrap(); + assert_eq!( + output.as_slice(), + &hex!("0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f"), + ); + } + + #[test] + fn test_version_request() { + let command = command(3, 0, 0, &[]); + let request = Request::try_from(&command).unwrap(); + assert_eq!(request, Request::Version); + } + + #[test] + fn test_version_response() { + let response = Response::Version(*b"U2F_V2"); + let mut output = Vec::<_, 1024>::new(); + response.serialize(&mut output).unwrap(); + assert_eq!(output.as_slice(), b"U2F_V2"); + } +} diff --git a/src/ctap2/client_pin.rs b/src/ctap2/client_pin.rs index 76c27c9..9c81c07 100644 --- a/src/ctap2/client_pin.rs +++ b/src/ctap2/client_pin.rs @@ -116,6 +116,385 @@ pub struct Response { #[cfg(test)] mod tests { + use super::*; + use hex_literal::hex; + use serde_test::{assert_de_tokens, assert_ser_tokens, assert_tokens, Token}; + + const KEY_AGREEMENT: &[u8] = &hex!("b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9"); + const NEW_PIN_ENC: &[u8] = &[0xde; 64]; + const PIN_AUTH: &[u8] = &[0xad; 32]; + const PIN_HASH_ENC: &[u8] = &[0xda; 16]; + const PIN_TOKEN: &[u8] = &[0xed; 32]; + + #[test] + fn test_de_request_get_retries() { + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::GetRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + _placeholder07: None, + _placeholder08: None, + permissions: None, + rp_id: None, + }; + assert_tokens( + &request, + &[ + Token::Map { len: Some(2) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x01), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_request_get_key_agreement() { + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::GetKeyAgreement, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + _placeholder07: None, + _placeholder08: None, + permissions: None, + rp_id: None, + }; + assert_tokens( + &request, + &[ + Token::Map { len: Some(2) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x02), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_request_set_pin() { + let key_agreement = EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&KEY_AGREEMENT[..32]).unwrap(), + y: Bytes::from_slice(&KEY_AGREEMENT[32..]).unwrap(), + }; + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::SetPin, + key_agreement: Some(key_agreement), + pin_auth: Some(serde_bytes::Bytes::new(PIN_AUTH)), + new_pin_enc: Some(serde_bytes::Bytes::new(NEW_PIN_ENC)), + pin_hash_enc: None, + _placeholder07: None, + _placeholder08: None, + permissions: None, + rp_id: None, + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(5) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x03), + // 0x03: keyAgreement + Token::U64(0x03), + Token::Map { len: Some(5) }, + // 1: kty + Token::I8(1), + Token::I8(2), + // 3: alg + Token::I8(3), + Token::I8(-25), + // -1: crv + Token::I8(-1), + Token::I8(1), + // -2: x + Token::I8(-2), + Token::BorrowedBytes(&KEY_AGREEMENT[..32]), + // -3: y + Token::I8(-3), + Token::BorrowedBytes(&KEY_AGREEMENT[32..]), + Token::MapEnd, + // 0x04: pinUvAuthParam + Token::U64(0x04), + Token::BorrowedBytes(PIN_AUTH), + // 0x05: newPinEnc + Token::U64(0x05), + Token::BorrowedBytes(NEW_PIN_ENC), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_request_change_pin() { + let key_agreement = EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&KEY_AGREEMENT[..32]).unwrap(), + y: Bytes::from_slice(&KEY_AGREEMENT[32..]).unwrap(), + }; + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::ChangePin, + key_agreement: Some(key_agreement), + pin_auth: Some(serde_bytes::Bytes::new(PIN_AUTH)), + new_pin_enc: Some(serde_bytes::Bytes::new(NEW_PIN_ENC)), + pin_hash_enc: Some(serde_bytes::Bytes::new(PIN_HASH_ENC)), + _placeholder07: None, + _placeholder08: None, + permissions: None, + rp_id: None, + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(6) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x04), + // 0x03: keyAgreement + Token::U64(0x03), + Token::Map { len: Some(5) }, + // 1: kty + Token::I8(1), + Token::I8(2), + // 3: alg + Token::I8(3), + Token::I8(-25), + // -1: crv + Token::I8(-1), + Token::I8(1), + // -2: x + Token::I8(-2), + Token::BorrowedBytes(&KEY_AGREEMENT[..32]), + // -3: y + Token::I8(-3), + Token::BorrowedBytes(&KEY_AGREEMENT[32..]), + Token::MapEnd, + // 0x04: pinUvAuthParam + Token::U64(0x04), + Token::BorrowedBytes(PIN_AUTH), + // 0x05: newPinEnc + Token::U64(0x05), + Token::BorrowedBytes(NEW_PIN_ENC), + // 0x06: pinHashEnc + Token::U64(0x06), + Token::BorrowedBytes(PIN_HASH_ENC), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_get_pin_token() { + let key_agreement = EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&KEY_AGREEMENT[..32]).unwrap(), + y: Bytes::from_slice(&KEY_AGREEMENT[32..]).unwrap(), + }; + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::GetPinToken, + key_agreement: Some(key_agreement), + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: Some(serde_bytes::Bytes::new(PIN_HASH_ENC)), + _placeholder07: None, + _placeholder08: None, + permissions: None, + rp_id: None, + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(4) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x05), + // 0x03: keyAgreement + Token::U64(0x03), + Token::Map { len: Some(5) }, + // 1: kty + Token::I8(1), + Token::I8(2), + // 3: alg + Token::I8(3), + Token::I8(-25), + // -1: crv + Token::I8(-1), + Token::I8(1), + // -2: x + Token::I8(-2), + Token::BorrowedBytes(&KEY_AGREEMENT[..32]), + // -3: y + Token::I8(-3), + Token::BorrowedBytes(&KEY_AGREEMENT[32..]), + Token::MapEnd, + // 0x06: pinHashEnc + Token::U64(0x06), + Token::BorrowedBytes(PIN_HASH_ENC), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_get_pin_token_with_permissions() { + let key_agreement = EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&KEY_AGREEMENT[..32]).unwrap(), + y: Bytes::from_slice(&KEY_AGREEMENT[32..]).unwrap(), + }; + let request = Request { + pin_protocol: 1, + sub_command: PinV1Subcommand::GetPinUvAuthTokenUsingPinWithPermissions, + key_agreement: Some(key_agreement), + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: Some(serde_bytes::Bytes::new(PIN_HASH_ENC)), + _placeholder07: None, + _placeholder08: None, + permissions: Some(0x04), + rp_id: Some("example.com"), + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(6) }, + // 0x01: pinProtocol + Token::U64(0x01), + Token::U8(1), + // 0x02: subCommand + Token::U64(0x02), + Token::U8(0x09), + // 0x03: keyAgreement + Token::U64(0x03), + Token::Map { len: Some(5) }, + // 1: kty + Token::I8(1), + Token::I8(2), + // 3: alg + Token::I8(3), + Token::I8(-25), + // -1: crv + Token::I8(-1), + Token::I8(1), + // -2: x + Token::I8(-2), + Token::BorrowedBytes(&KEY_AGREEMENT[..32]), + // -3: y + Token::I8(-3), + Token::BorrowedBytes(&KEY_AGREEMENT[32..]), + Token::MapEnd, + // 0x06: pinHashEnc + Token::U64(0x06), + Token::BorrowedBytes(PIN_HASH_ENC), + // 0x09: permissions + Token::U64(0x09), + Token::U8(0x04), + // 0x0A: rpId + Token::U64(0x0A), + Token::BorrowedStr("example.com"), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_ser_response_get_retries() { + let response = Response { + retries: Some(3), + ..Default::default() + }; + assert_ser_tokens( + &response, + &[ + Token::Map { len: Some(1) }, + // 0x03: pinRetries + Token::U64(0x03), + Token::Some, + Token::U8(3), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_ser_response_get_key_agreement() { + let key_agreement = EcdhEsHkdf256PublicKey { + x: Bytes::from_slice(&KEY_AGREEMENT[..32]).unwrap(), + y: Bytes::from_slice(&KEY_AGREEMENT[32..]).unwrap(), + }; + let response = Response { + key_agreement: Some(key_agreement), + ..Default::default() + }; + assert_ser_tokens( + &response, + &[ + Token::Map { len: Some(1) }, + // 0x01: keyAgreement + Token::U64(0x01), + Token::Some, + Token::Map { len: Some(5) }, + // 1: kty + Token::I8(1), + Token::I8(2), + // 3: alg + Token::I8(3), + Token::I8(-25), + // -1: crv + Token::I8(-1), + Token::I8(1), + // -2: x + Token::I8(-2), + Token::BorrowedBytes(&KEY_AGREEMENT[..32]), + // -3: y + Token::I8(-3), + Token::BorrowedBytes(&KEY_AGREEMENT[32..]), + Token::MapEnd, + Token::MapEnd, + ], + ); + } + + #[test] + fn test_ser_response_get_pin_token() { + let response = Response { + pin_token: Some(Bytes::from_slice(PIN_TOKEN).unwrap()), + ..Default::default() + }; + assert_ser_tokens( + &response, + &[ + Token::Map { len: Some(1) }, + // 0x02: pinAuvAuthToken + Token::U64(0x02), + Token::Some, + Token::BorrowedBytes(PIN_TOKEN), + Token::MapEnd, + ], + ); + } #[test] fn pin_v1_subcommand() { @@ -127,7 +506,7 @@ mod tests { // to CBOR would output) is 0. // The following test would then fail, as [1] != [2] let mut buf = [0u8; 64]; - let example = super::PinV1Subcommand::GetKeyAgreement; + let example = PinV1Subcommand::GetKeyAgreement; let ser = crate::serde::cbor_serialize(&example, &mut buf).unwrap(); assert_eq!(ser, &[0x02]); } diff --git a/src/ctap2/large_blobs.rs b/src/ctap2/large_blobs.rs index e36ce55..11a68e4 100644 --- a/src/ctap2/large_blobs.rs +++ b/src/ctap2/large_blobs.rs @@ -34,3 +34,90 @@ pub struct Response { #[serde(skip_serializing_if = "Option::is_none")] pub config: Option>, } + +#[cfg(test)] +mod tests { + use super::*; + use serde_test::{assert_de_tokens, assert_ser_tokens, Token}; + + const FRAGMENT: &[u8] = &[0xaf; 255]; + const PIN_AUTH: &[u8] = &[0xad; 32]; + + #[test] + fn test_de_request_get() { + let request = Request { + get: Some(255), + set: None, + offset: 0, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(2) }, + // 0x01: get + Token::U64(0x01), + Token::U32(255), + // 0x03: offset + Token::U64(0x03), + Token::U32(0), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_de_request_set() { + let request = Request { + get: None, + set: Some(serde_bytes::Bytes::new(FRAGMENT)), + offset: 0, + length: Some(255), + pin_uv_auth_param: Some(serde_bytes::Bytes::new(PIN_AUTH)), + pin_uv_auth_protocol: Some(1), + }; + assert_de_tokens( + &request, + &[ + Token::Map { len: Some(5) }, + // 0x02: set + Token::U64(0x02), + Token::BorrowedBytes(FRAGMENT), + // 0x03: offset + Token::U64(0x03), + Token::U32(0), + // 0x04: length + Token::U64(0x04), + Token::U32(255), + // 0x05: pinUvAuthParam + Token::U64(0x05), + Token::BorrowedBytes(PIN_AUTH), + // 0x06: pinUvAuthProtocol + Token::U64(0x06), + Token::U32(1), + Token::MapEnd, + ], + ); + } + + #[test] + fn test_ser_response() { + let response = Response { + config: Some(Bytes::from_slice(&[]).unwrap()), + ..Default::default() + }; + assert_ser_tokens( + &response, + &[ + Token::Map { len: Some(1) }, + // 0x01: config + Token::U64(0x01), + Token::Some, + Token::BorrowedBytes(&[]), + Token::MapEnd, + ], + ); + } +}