Skip to content

Commit 532c3d5

Browse files
committed
Add tests to check that all CBOR requests are in canonicalized form.
Closes mozilla#225.
1 parent b21f8fa commit 532c3d5

11 files changed

+252
-9
lines changed

src/crypto/mod.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ mod der;
3939

4040
pub use backend::ecdsa_p256_sha256_sign_raw;
4141

42-
pub struct PinUvAuthProtocol(Box<dyn PinProtocolImpl + Send + Sync>);
42+
pub struct PinUvAuthProtocol(pub(crate) Box<dyn PinProtocolImpl + Send + Sync>);
4343
impl PinUvAuthProtocol {
4444
pub fn id(&self) -> u64 {
4545
self.0.protocol_id()
@@ -53,7 +53,7 @@ impl PinUvAuthProtocol {
5353
/// PinProtocolImpl. So we stash a copy of the calling PinUvAuthProtocol in the output SharedSecret.
5454
/// We need a trick here to tell the compiler that every PinProtocolImpl we define will implement
5555
/// Clone.
56-
trait ClonablePinProtocolImpl {
56+
pub(crate) trait ClonablePinProtocolImpl {
5757
fn clone_box(&self) -> Box<dyn PinProtocolImpl + Send + Sync>;
5858
}
5959

@@ -73,7 +73,7 @@ impl Clone for PinUvAuthProtocol {
7373
}
7474

7575
/// CTAP 2.1, Section 6.5.4. PIN/UV Auth Protocol Abstract Definition
76-
trait PinProtocolImpl: ClonablePinProtocolImpl {
76+
pub(crate) trait PinProtocolImpl: ClonablePinProtocolImpl {
7777
fn protocol_id(&self) -> u64;
7878
fn initialize(&self);
7979
fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, CryptoError>;
@@ -364,10 +364,10 @@ impl PinUvAuthToken {
364364

365365
#[derive(Clone, Debug)]
366366
pub struct PinUvAuthParam {
367-
pin_auth: Vec<u8>,
367+
pub(crate) pin_auth: Vec<u8>,
368368
pub pin_protocol: PinUvAuthProtocol,
369369
#[allow(dead_code)] // Not yet used
370-
permissions: PinUvAuthTokenPermission,
370+
pub(crate) permissions: PinUvAuthTokenPermission,
371371
}
372372

373373
impl PinUvAuthParam {

src/ctap2/commands/authenticator_config.rs

+35
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,38 @@ impl PinUvAuthCommand for AuthenticatorConfig {
223223
None
224224
}
225225
}
226+
227+
#[cfg(test)]
228+
mod test {
229+
use super::*;
230+
use crate::ctap2::commands::assert_canonical_cbor_encoding;
231+
232+
#[test]
233+
fn test_cbor_canonical() {
234+
for subcommand in [
235+
AuthConfigCommand::EnableEnterpriseAttestation,
236+
AuthConfigCommand::ToggleAlwaysUv,
237+
AuthConfigCommand::SetMinPINLength(SetMinPINLength {
238+
/// Minimum PIN length in code points
239+
new_min_pin_length: Some(42),
240+
/// RP IDs which are allowed to get this information via the minPinLength extension.
241+
/// This parameter MUST NOT be used unless the minPinLength extension is supported.
242+
min_pin_length_rpids: Some(vec!["foobar".to_string()]),
243+
/// The authenticator returns CTAP2_ERR_PIN_POLICY_VIOLATION until changePIN is successful.
244+
force_change_pin: Some(true),
245+
}),
246+
] {
247+
let request = AuthenticatorConfig {
248+
subcommand,
249+
pin_uv_auth_param: Some(PinUvAuthParam {
250+
pin_auth: vec![0xDE, 0xAD, 0xBE, 0xEF],
251+
pin_protocol: crate::crypto::PinUvAuthProtocol(Box::new(
252+
crate::crypto::PinUvAuth2 {},
253+
)),
254+
permissions: crate::ctap2::PinUvAuthTokenPermission::MakeCredential,
255+
}),
256+
};
257+
assert_canonical_cbor_encoding(&request);
258+
}
259+
}
260+
}

src/ctap2/commands/bio_enrollment.rs

+36
Original file line numberDiff line numberDiff line change
@@ -658,3 +658,39 @@ pub enum BioEnrollmentResult {
658658
FingerprintSensorInfo(FingerprintSensorInfo),
659659
SampleStatus(LastEnrollmentSampleStatus, u64),
660660
}
661+
662+
#[cfg(test)]
663+
mod test {
664+
use super::*;
665+
use crate::ctap2::commands::assert_canonical_cbor_encoding;
666+
667+
#[test]
668+
fn test_cbor_canonical() {
669+
for subcommand in [
670+
BioEnrollmentCommand::EnrollBegin(Some(42)),
671+
BioEnrollmentCommand::EnrollCaptureNextSample((vec![0xDE, 0xAD, 0xBE, 0xEF], Some(42))),
672+
BioEnrollmentCommand::CancelCurrentEnrollment,
673+
BioEnrollmentCommand::EnumerateEnrollments,
674+
BioEnrollmentCommand::SetFriendlyName((
675+
vec![0xDE, 0xAD, 0xBE, 0xEF],
676+
"foobar".to_string(),
677+
)),
678+
BioEnrollmentCommand::RemoveEnrollment(vec![0xDE, 0xAD, 0xBE, 0xEF]),
679+
BioEnrollmentCommand::GetFingerprintSensorInfo,
680+
] {
681+
let request = BioEnrollment {
682+
modality: BioEnrollmentModality::Fingerprint,
683+
subcommand,
684+
pin_uv_auth_param: Some(PinUvAuthParam {
685+
pin_auth: vec![0xDE, 0xAD, 0xBE, 0xEF],
686+
pin_protocol: crate::crypto::PinUvAuthProtocol(Box::new(
687+
crate::crypto::PinUvAuth2 {},
688+
)),
689+
permissions: crate::ctap2::PinUvAuthTokenPermission::MakeCredential,
690+
}),
691+
use_legacy_preview: false,
692+
};
693+
assert_canonical_cbor_encoding(&request);
694+
}
695+
}
696+
}

src/ctap2/commands/client_pin.rs

+54-1
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,16 @@ impl From<CryptoError> for PinError {
713713

714714
#[cfg(test)]
715715
mod test {
716-
use super::ClientPinResponse;
716+
use super::{ClientPIN, ClientPinResponse, PINSubcommand, PinUvAuthProtocol};
717717
use crate::crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve};
718+
use crate::ctap2::attestation::AAGuid;
719+
use crate::ctap2::commands::assert_canonical_cbor_encoding;
720+
use crate::ctap2::commands::get_info::AuthenticatorOptions;
721+
use crate::ctap2::AuthenticatorVersion;
722+
use crate::util::decode_hex;
723+
use crate::AuthenticatorInfo;
718724
use serde_cbor::de::from_slice;
725+
use std::convert::TryFrom;
719726

720727
#[test]
721728
fn test_get_key_agreement() {
@@ -848,4 +855,50 @@ mod test {
848855
from_slice(&reference).expect("could not deserialize reference");
849856
assert_eq!(expected, result);
850857
}
858+
859+
#[test]
860+
#[allow(non_snake_case)]
861+
fn test_cbor_canonical() {
862+
let pin_protocol = PinUvAuthProtocol::try_from(&AuthenticatorInfo {
863+
versions: vec![AuthenticatorVersion::U2F_V2, AuthenticatorVersion::FIDO_2_0],
864+
extensions: vec![],
865+
aaguid: AAGuid([0u8; 16]),
866+
options: AuthenticatorOptions {
867+
platform_device: false,
868+
resident_key: true,
869+
client_pin: Some(false),
870+
user_presence: true,
871+
..Default::default()
872+
},
873+
max_msg_size: Some(1200),
874+
pin_protocols: None,
875+
..Default::default()
876+
})
877+
.unwrap();
878+
879+
// from tests for crate::crypto
880+
let DEV_PUB_X =
881+
decode_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168");
882+
let DEV_PUB_Y =
883+
decode_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47");
884+
let key = COSEKey {
885+
alg: COSEAlgorithm::ES256,
886+
key: COSEKeyType::EC2(COSEEC2Key {
887+
curve: Curve::SECP256R1,
888+
x: DEV_PUB_X,
889+
y: DEV_PUB_Y,
890+
}),
891+
};
892+
let request = ClientPIN {
893+
pin_protocol: Some(pin_protocol),
894+
subcommand: PINSubcommand::GetPinRetries,
895+
key_agreement: Some(key),
896+
pin_auth: Some(vec![0xDE, 0xAD, 0xBE, 0xEF]),
897+
new_pin_enc: Some(vec![0xDE, 0xAD, 0xBE, 0xEF]),
898+
pin_hash_enc: Some(vec![0xDE, 0xAD, 0xBE, 0xEF]),
899+
permissions: Some(42),
900+
rp_id: Some("foobar".to_string()),
901+
};
902+
assert_canonical_cbor_encoding(&request);
903+
}
851904
}

src/ctap2/commands/credential_management.rs

+46
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,49 @@ impl PinUvAuthCommand for CredentialManagement {
455455
self.pin_uv_auth_param.as_ref()
456456
}
457457
}
458+
459+
#[cfg(test)]
460+
mod test {
461+
use super::*;
462+
use crate::ctap2::commands::assert_canonical_cbor_encoding;
463+
use crate::ctap2::server::Transport;
464+
465+
#[test]
466+
fn test_cbor_canonical() {
467+
for subcommand in [
468+
CredManagementCommand::GetCredsMetadata,
469+
CredManagementCommand::EnumerateRPsBegin,
470+
CredManagementCommand::EnumerateRPsGetNextRP,
471+
CredManagementCommand::EnumerateCredentialsBegin(RpIdHash([0u8; 32])),
472+
CredManagementCommand::EnumerateCredentialsGetNextCredential,
473+
CredManagementCommand::DeleteCredential(PublicKeyCredentialDescriptor {
474+
id: vec![0xDE, 0xAD, 0xBE, 0xEF],
475+
transports: vec![Transport::NFC],
476+
}),
477+
CredManagementCommand::UpdateUserInformation((
478+
PublicKeyCredentialDescriptor {
479+
id: vec![0xDE, 0xAD, 0xBE, 0xEF],
480+
transports: vec![Transport::NFC],
481+
},
482+
PublicKeyCredentialUserEntity {
483+
id: vec![0xDE, 0xAD, 0xBE, 0xEF],
484+
name: Some("foobar".to_string()),
485+
display_name: Some("foobar".to_string()),
486+
},
487+
)),
488+
] {
489+
let request = CredentialManagement {
490+
subcommand, // subCommand currently being requested
491+
pin_uv_auth_param: Some(PinUvAuthParam {
492+
pin_auth: vec![0xDE, 0xAD, 0xBE, 0xEF],
493+
pin_protocol: crate::crypto::PinUvAuthProtocol(Box::new(
494+
crate::crypto::PinUvAuth2 {},
495+
)),
496+
permissions: crate::ctap2::PinUvAuthTokenPermission::MakeCredential,
497+
}),
498+
use_legacy_preview: false,
499+
};
500+
assert_canonical_cbor_encoding(&request);
501+
}
502+
}
503+
}

src/ctap2/commands/get_assertion.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ pub mod test {
618618
use crate::ctap2::commands::get_info::{
619619
AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion,
620620
};
621-
use crate::ctap2::commands::RequestCtap1;
621+
use crate::ctap2::commands::{assert_canonical_cbor_encoding, RequestCtap1};
622622
use crate::ctap2::preflight::{
623623
do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2,
624624
};
@@ -660,6 +660,7 @@ pub mod test {
660660
},
661661
Default::default(),
662662
);
663+
assert_canonical_cbor_encoding(&assertion);
663664
let mut device = Device::new("commands/get_assertion").unwrap();
664665
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
665666
let mut cid = [0u8; 4];
@@ -859,6 +860,7 @@ pub mod test {
859860
},
860861
Default::default(),
861862
);
863+
assert_canonical_cbor_encoding(&assertion);
862864
let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
863865
// channel id
864866
device.downgrade_to_ctap1();
@@ -948,6 +950,7 @@ pub mod test {
948950
},
949951
Default::default(),
950952
);
953+
assert_canonical_cbor_encoding(&assertion);
951954

952955
let mut device = Device::new("commands/get_assertion").unwrap(); // not really used (all functions ignore it)
953956
// channel id
@@ -1128,6 +1131,7 @@ pub mod test {
11281131
},
11291132
Default::default(),
11301133
);
1134+
assert_canonical_cbor_encoding(&assertion);
11311135
let mut device = Device::new("commands/get_assertion").unwrap();
11321136
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
11331137
let mut cid = [0u8; 4];

src/ctap2/commands/make_credentials.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ pub mod test {
605605
AuthenticatorDataFlags, Signature,
606606
};
607607
use crate::ctap2::client_data::{Challenge, CollectedClientData, TokenBinding, WebauthnType};
608-
use crate::ctap2::commands::{RequestCtap1, RequestCtap2};
608+
use crate::ctap2::commands::{assert_canonical_cbor_encoding, RequestCtap1, RequestCtap2};
609609
use crate::ctap2::server::RpIdHash;
610610
use crate::ctap2::server::{
611611
AuthenticatorAttachment, PublicKeyCredentialParameters, PublicKeyCredentialUserEntity,
@@ -654,6 +654,7 @@ pub mod test {
654654
},
655655
Default::default(),
656656
);
657+
assert_canonical_cbor_encoding(&req);
657658

658659
let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it)
659660
assert_eq!(device.get_protocol(), FidoProtocol::CTAP2);
@@ -709,6 +710,7 @@ pub mod test {
709710
},
710711
Default::default(),
711712
);
713+
assert_canonical_cbor_encoding(&req);
712714

713715
let (req_serialized, _) = req
714716
.ctap1_format()

src/ctap2/commands/mod.rs

+64
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::ctap2::server::UserVerificationRequirement;
55
use crate::errors::AuthenticatorError;
66
use crate::transport::errors::{ApduErrorStatus, HIDError};
77
use crate::transport::{FidoDevice, VirtualFidoDevice};
8+
use serde::Serialize;
89
use serde_cbor::{error::Error as CborError, Value};
910
use serde_json as json;
1011
use std::error::Error as StdErrorT;
@@ -475,3 +476,66 @@ impl fmt::Display for CommandError {
475476
}
476477

477478
impl StdErrorT for CommandError {}
479+
480+
/// Asserts that a value encodes to canonical CBOR encoding according to CTAP2.1 spec (https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#ctap2-canonical-cbor-encoding-form). This function is used by submodules to verify the requests they define.
481+
fn assert_canonical_cbor_encoding<T: Serialize + fmt::Debug>(value: &T) {
482+
let bytes = serde_cbor::to_vec(value)
483+
.unwrap_or_else(|e| panic!("Failed to serialize value {:?}: {}", value, e));
484+
let opaque_value: serde_cbor::Value = serde_cbor::from_slice(&bytes)
485+
.unwrap_or_else(|e| panic!("Failed to deserialize value {:?}: {}", value, e));
486+
487+
// Below, each requirement is first quoted from the spec and then tested (or explained why testing is not necessary):
488+
489+
// > Integers MUST be encoded as small as possible.
490+
// > - 0 to 23 and -1 to -24 MUST be expressed in the same byte as the major type;
491+
// > - 24 to 255 and -25 to -256 MUST be expressed only with an additional uint8_t;
492+
// > - 256 to 65535 and -257 to -65536 MUST be expressed only with an additional uint16_t;
493+
// > - 65536 to 4294967295 and -65537 to -4294967296 MUST be expressed only with an additional uint32_t.
494+
// serde_cbor handles this as required, see https://github.com/pyfisch/cbor/blob/master/src/ser.rs#L140-L182
495+
496+
// > The representations of any floating-point values are not changed.
497+
// > Note: The size of a floating point value—16-, 32-, or 64-bits—is considered part of the value for the purpose of CTAP2. E.g., a 16-bit value of 1.5, say, has different semantic meaning than a 32-bit value of 1.5, and both can be canonical for their own meanings.
498+
// serde_cbor won't change u32 to u64 by itself, so this is fine, too
499+
500+
// > The expression of lengths in major types 2 through 5 MUST be as short as possible. The rules for these lengths follow the above rule for integers.
501+
// See above -- serde_cbor handles this for us.
502+
503+
// > Indefinite-length items MUST be made into definite-length items.
504+
// serde_cbor might serialize with indefinite length if serde doesn't know the length when beginning to serialize an array/a map.
505+
// We use a trick here: serde_cbor::Value manages arrays/maps as `Array`/`Map`. Both of these have iterators implementing `ExactIterator`, so serde_cbor does not serialize them with indefinite length (unless the length is larger than usize max value, which shouldn't happen in practice). So, just re-serialize and compare with the result of the first serialization.
506+
507+
let bytes_after_reserializing = serde_cbor::to_vec(&opaque_value)
508+
.unwrap_or_else(|e| panic!("Failed to reserialize value {:?}: {}", opaque_value, e));
509+
assert_eq!(bytes, bytes_after_reserializing, "Bytes aren't equal before and after reserializing, value is probably not in CTAP2 canonical CBOR encoding form");
510+
511+
// > The keys in every map MUST be sorted lowest value to highest. The sorting rules are:
512+
// > - If the major types are different, the one with the lower value in numerical order sorts earlier.
513+
// > - If two keys have different lengths, the shorter one sorts earlier;
514+
// > - If two keys have different lengths, the shorter one sorts earlier;
515+
// > - If two keys have the same length, the one with the lower value in (byte-wise) lexical order sorts earlier.
516+
// This is equivalent to the CBOR core canonicalization requirements (https://datatracker.ietf.org/doc/html/draft-ietf-cbor-7049bis-04#section-4.10), which is how the BTReeMap is sorted. Therefore, the reserialization trick from above also handles this case.
517+
518+
// > Tags as defined in Section 2.4 in [RFC8949] MUST NOT be present.
519+
fn assert_has_no_tags(value: &serde_cbor::Value) {
520+
match value {
521+
serde_cbor::Value::Array(vec) => {
522+
for entry in vec {
523+
assert_has_no_tags(entry);
524+
}
525+
}
526+
serde_cbor::Value::Map(map) => {
527+
for (key, entry) in map {
528+
assert_has_no_tags(key);
529+
assert_has_no_tags(entry);
530+
}
531+
}
532+
serde_cbor::Value::Tag(tag, value) => panic!(
533+
"Tags are not allowed inside canonical CBOR encoding, but found tag {} on value {:?}",
534+
tag, value
535+
),
536+
_ => {},
537+
}
538+
}
539+
540+
assert_has_no_tags(&opaque_value);
541+
}

src/transport/hid.rs

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub trait HIDDevice: FidoDevice + Read + Write {
9191
) -> io::Result<(HIDCmd, Vec<u8>)> {
9292
self.u2f_write(cmd.into(), send)?;
9393
debug!("sent to Device {:?} cmd={:?}: {:?}", self.id(), cmd, send);
94+
dbg!();
9495
loop {
9596
let (cmd, data) = self.u2f_read()?;
9697
if cmd != HIDCmd::Keepalive {
@@ -180,6 +181,7 @@ impl<T: HIDDevice + TestDevice> FidoDeviceIO for T {
180181
msg: &Req,
181182
keep_alive: &dyn Fn() -> bool,
182183
) -> Result<Req::Output, HIDError> {
184+
dbg!();
183185
debug!("sending {:?} to {:?}", msg, self);
184186
#[cfg(test)]
185187
{

0 commit comments

Comments
 (0)