Skip to content

Commit 3286922

Browse files
test(ctap2): pin canonical CBOR and COSE vectors (#279)
There was no test pinning the exact wire bytes the stack emits, so a change in CBOR ordering or a serialization dependency could break interoperability silently. This pins golden vectors for a make-credential and a get-assertion request and for a COSE public key. Part of #259.
1 parent ee077e7 commit 3286922

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

libwebauthn/src/proto/ctap2/cbor/request.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,80 @@ impl TryFrom<&Ctap2LargeBlobsRequest> for CborRequest {
117117
})
118118
}
119119
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
// To refresh after an intentional wire change, run the test and copy the actual (left) hex from the assert_eq! panic.
124+
use super::*;
125+
use crate::proto::ctap2::model::{
126+
Ctap2CredentialType, Ctap2GetAssertionOptions, Ctap2MakeCredentialOptions,
127+
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
128+
Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity,
129+
};
130+
use serde_bytes::ByteBuf;
131+
132+
// Deterministic, hand-pickable inputs: fixed byte fills and example.com.
133+
fn fixed_make_credential_request() -> Ctap2MakeCredentialRequest {
134+
Ctap2MakeCredentialRequest {
135+
hash: ByteBuf::from([0x01u8; 32].to_vec()),
136+
relying_party: Ctap2PublicKeyCredentialRpEntity {
137+
id: "example.com".to_string(),
138+
name: Some("Example".to_string()),
139+
},
140+
user: Ctap2PublicKeyCredentialUserEntity {
141+
id: ByteBuf::from([0x02u8; 16].to_vec()),
142+
name: Some("alice".to_string()),
143+
display_name: Some("Alice".to_string()),
144+
},
145+
algorithms: vec![Ctap2CredentialType::default()],
146+
exclude: Some(vec![Ctap2PublicKeyCredentialDescriptor {
147+
id: ByteBuf::from([0x03u8; 16].to_vec()),
148+
r#type: Ctap2PublicKeyCredentialType::PublicKey,
149+
transports: None,
150+
}]),
151+
extensions: None,
152+
options: Some(Ctap2MakeCredentialOptions {
153+
require_resident_key: Some(true),
154+
deprecated_require_user_verification: None,
155+
}),
156+
pin_auth_param: None,
157+
pin_auth_proto: None,
158+
enterprise_attestation: None,
159+
}
160+
}
161+
162+
fn fixed_get_assertion_request() -> Ctap2GetAssertionRequest {
163+
Ctap2GetAssertionRequest {
164+
relying_party_id: "example.com".to_string(),
165+
client_data_hash: ByteBuf::from([0x04u8; 32].to_vec()),
166+
allow: vec![Ctap2PublicKeyCredentialDescriptor {
167+
id: ByteBuf::from([0x05u8; 16].to_vec()),
168+
r#type: Ctap2PublicKeyCredentialType::PublicKey,
169+
transports: None,
170+
}],
171+
extensions: None,
172+
options: Some(Ctap2GetAssertionOptions {
173+
require_user_presence: true,
174+
require_user_verification: false,
175+
}),
176+
pin_auth_param: None,
177+
pin_auth_proto: None,
178+
}
179+
}
180+
181+
#[test]
182+
fn make_credential_request_golden_cbor() {
183+
let cbor = CborRequest::try_from(&fixed_make_credential_request()).unwrap();
184+
assert_eq!(cbor.command, Ctap2CommandCode::AuthenticatorMakeCredential);
185+
// Canonical indexed map, keys 0x01,0x02,0x03,0x04,0x05,0x07 in order.
186+
assert_eq!(hex::encode(&cbor.encoded_data), "a6015820010101010101010101010101010101010101010101010101010101010101010102a26269646b6578616d706c652e636f6d646e616d65674578616d706c6503a36269645002020202020202020202020202020202646e616d6565616c6963656b646973706c61794e616d6565416c6963650481a263616c672664747970656a7075626c69632d6b65790581a2626964500303030303030303030303030303030364747970656a7075626c69632d6b657907a162726bf5");
187+
}
188+
189+
#[test]
190+
fn get_assertion_request_golden_cbor() {
191+
let cbor = CborRequest::try_from(&fixed_get_assertion_request()).unwrap();
192+
assert_eq!(cbor.command, Ctap2CommandCode::AuthenticatorGetAssertion);
193+
// Canonical indexed map with keys 0x01,0x02,0x03,0x05 in order, uv omitted.
194+
assert_eq!(hex::encode(&cbor.encoded_data), "a4016b6578616d706c652e636f6d02582004040404040404040404040404040404040404040404040404040404040404040381a2626964500505050505050505050505050505050564747970656a7075626c69632d6b657905a1627570f5");
195+
}
196+
}

libwebauthn/src/proto/ctap2/cose.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,4 +550,18 @@ mod tests {
550550
.unwrap();
551551
assert!(to_spki(&bytes).is_err());
552552
}
553+
554+
// To refresh after an intentional wire change, run the test and copy the actual (left) hex from the assert_eq! panic.
555+
/// Pin the exact COSE_Key bytes cosey emits for a fixed P-256 key.
556+
#[test]
557+
fn p256_public_key_golden_cose() {
558+
use cosey::{Bytes, P256PublicKey, PublicKey};
559+
let key = PublicKey::P256Key(P256PublicKey {
560+
x: Bytes::from_slice(&[0x06u8; 32]).unwrap(),
561+
y: Bytes::from_slice(&[0x07u8; 32]).unwrap(),
562+
});
563+
let bytes = cbor::to_vec(&key).unwrap();
564+
// {1: 2(EC2), 3: -7(ES256), -1: 1(P256), -2: x[32], -3: y[32]}.
565+
assert_eq!(hex::encode(&bytes), "a501020326200121582006060606060606060606060606060606060606060606060606060606060606062258200707070707070707070707070707070707070707070707070707070707070707");
566+
}
553567
}

0 commit comments

Comments
 (0)