@@ -6,6 +6,7 @@ use super::{Ctap2GetAssertionRequest, Ctap2PublicKeyCredentialDescriptor};
66use crate :: {
77 proto:: ctap2:: { model:: Ctap2GetAssertionOptions , Ctap2 } ,
88 transport:: Channel ,
9+ webauthn:: error:: { CtapError , Error } ,
910} ;
1011
1112/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pre-flight
@@ -23,7 +24,7 @@ pub async fn ctap2_preflight<C: Channel>(
2324 credentials : & [ Ctap2PublicKeyCredentialDescriptor ] ,
2425 client_data_hash : & [ u8 ] ,
2526 rp : & str ,
26- ) -> Vec < Ctap2PublicKeyCredentialDescriptor > {
27+ ) -> Result < Vec < Ctap2PublicKeyCredentialDescriptor > , Error > {
2728 ctap2_preflight_with_appid ( channel, credentials, client_data_hash, rp, None ) . await
2829}
2930
@@ -38,12 +39,12 @@ pub async fn ctap2_preflight_with_appid<C: Channel>(
3839 client_data_hash : & [ u8 ] ,
3940 rp : & str ,
4041 appid_exclude : Option < & str > ,
41- ) -> Vec < Ctap2PublicKeyCredentialDescriptor > {
42+ ) -> Result < Vec < Ctap2PublicKeyCredentialDescriptor > , Error > {
4243 info ! ( "Credential list BEFORE preflight: {credentials:?}" ) ;
4344 let mut filtered_list = Vec :: new ( ) ;
4445 for credential in credentials {
4546 // Test against the canonical rpId first.
46- if let Some ( matched) = preflight_one ( channel, credential, client_data_hash, rp) . await {
47+ if let Some ( matched) = preflight_one ( channel, credential, client_data_hash, rp) . await ? {
4748 debug ! ( "Pre-flight: Found already known credential under rpId {credential:?}" ) ;
4849 filtered_list. push ( matched) ;
4950 continue ;
@@ -54,7 +55,8 @@ pub async fn ctap2_preflight_with_appid<C: Channel>(
5455 // the AppID URL as the rpId here; the authenticator hashes it the
5556 // same way the U2F device hashed the original AppID.
5657 if let Some ( appid) = appid_exclude {
57- if let Some ( matched) = preflight_one ( channel, credential, client_data_hash, appid) . await
58+ if let Some ( matched) =
59+ preflight_one ( channel, credential, client_data_hash, appid) . await ?
5860 {
5961 debug ! (
6062 "Pre-flight: Found already known credential under appidExclude {credential:?}"
@@ -66,15 +68,15 @@ pub async fn ctap2_preflight_with_appid<C: Channel>(
6668 debug ! ( "Pre-flight: Filtering out {credential:?}" ) ;
6769 }
6870 info ! ( "Credential list AFTER preflight: {filtered_list:?}" ) ;
69- filtered_list
71+ Ok ( filtered_list)
7072}
7173
7274async fn preflight_one < C : Channel > (
7375 channel : & mut C ,
7476 credential : & Ctap2PublicKeyCredentialDescriptor ,
7577 client_data_hash : & [ u8 ] ,
7678 rp : & str ,
77- ) -> Option < Ctap2PublicKeyCredentialDescriptor > {
79+ ) -> Result < Option < Ctap2PublicKeyCredentialDescriptor > , Error > {
7880 let preflight_request = Ctap2GetAssertionRequest {
7981 relying_party_id : rp. to_string ( ) ,
8082 client_data_hash : ByteBuf :: from ( client_data_hash) ,
@@ -105,11 +107,95 @@ async fn preflight_one<C: Channel>(
105107 // 3. Neither, which is allowed, if the allow_list was of length 1, then
106108 // we have to copy it ourselfs from the input
107109 . unwrap_or ( credential. clone ( ) ) ;
108- Some ( id)
110+ Ok ( Some ( id) )
109111 }
112+ // Only CTAP2_ERR_NO_CREDENTIALS proves the credential is absent.
113+ Err ( Error :: Ctap ( CtapError :: NoCredentials ) ) => {
114+ debug ! ( "Pre-flight: Not found under {rp:?}" ) ;
115+ Ok ( None )
116+ }
117+ // Any other error is transient or unexpected, not absence: propagate it.
110118 Err ( e) => {
111- debug ! ( "Pre-flight: Not found under {rp:?}: {e:?}" ) ;
112- None
119+ debug ! ( "Pre-flight: Error testing under {rp:?}: {e:?}" ) ;
120+ Err ( e)
121+ }
122+ }
123+ }
124+
125+ #[ cfg( test) ]
126+ mod tests {
127+ use serde_bytes:: ByteBuf ;
128+
129+ use super :: ctap2_preflight;
130+ use crate :: proto:: ctap2:: cbor:: { CborRequest , CborResponse } ;
131+ use crate :: proto:: ctap2:: model:: Ctap2GetAssertionOptions ;
132+ use crate :: proto:: ctap2:: {
133+ Ctap2GetAssertionRequest , Ctap2PublicKeyCredentialDescriptor , Ctap2PublicKeyCredentialType ,
134+ } ;
135+ use crate :: transport:: mock:: channel:: MockChannel ;
136+ use crate :: webauthn:: error:: { CtapError , Error } ;
137+
138+ fn credential ( id : & [ u8 ] ) -> Ctap2PublicKeyCredentialDescriptor {
139+ Ctap2PublicKeyCredentialDescriptor {
140+ r#type : Ctap2PublicKeyCredentialType :: PublicKey ,
141+ id : ByteBuf :: from ( id) ,
142+ transports : None ,
113143 }
114144 }
145+
146+ fn expected_preflight_request (
147+ cred : & Ctap2PublicKeyCredentialDescriptor ,
148+ hash : & [ u8 ] ,
149+ rp : & str ,
150+ ) -> CborRequest {
151+ let request = Ctap2GetAssertionRequest {
152+ relying_party_id : rp. to_string ( ) ,
153+ client_data_hash : ByteBuf :: from ( hash) ,
154+ allow : vec ! [ cred. clone( ) ] ,
155+ extensions : None ,
156+ options : Some ( Ctap2GetAssertionOptions {
157+ require_user_presence : false ,
158+ require_user_verification : false ,
159+ } ) ,
160+ pin_auth_param : None ,
161+ pin_auth_proto : None ,
162+ } ;
163+ ( & request) . try_into ( ) . expect ( "encode preflight request" )
164+ }
165+
166+ #[ tokio:: test]
167+ async fn preflight_propagates_non_no_credentials_error ( ) {
168+ let cred = credential ( & [ 1 , 2 , 3 , 4 ] ) ;
169+ let hash = [ 0u8 ; 32 ] ;
170+ let mut channel = MockChannel :: new ( ) ;
171+ channel. push_command_pair (
172+ expected_preflight_request ( & cred, & hash, "example.org" ) ,
173+ CborResponse {
174+ status_code : CtapError :: OperationDenied ,
175+ data : None ,
176+ } ,
177+ ) ;
178+
179+ let result = ctap2_preflight ( & mut channel, & [ cred] , & hash, "example.org" ) . await ;
180+ assert_eq ! ( result. err( ) , Some ( Error :: Ctap ( CtapError :: OperationDenied ) ) ) ;
181+ }
182+
183+ #[ tokio:: test]
184+ async fn preflight_treats_no_credentials_as_absence ( ) {
185+ let cred = credential ( & [ 1 , 2 , 3 , 4 ] ) ;
186+ let hash = [ 0u8 ; 32 ] ;
187+ let mut channel = MockChannel :: new ( ) ;
188+ channel. push_command_pair (
189+ expected_preflight_request ( & cred, & hash, "example.org" ) ,
190+ CborResponse {
191+ status_code : CtapError :: NoCredentials ,
192+ data : None ,
193+ } ,
194+ ) ;
195+
196+ let filtered = ctap2_preflight ( & mut channel, & [ cred] , & hash, "example.org" )
197+ . await
198+ . expect ( "preflight should succeed on NoCredentials" ) ;
199+ assert ! ( filtered. is_empty( ) ) ;
200+ }
115201}
0 commit comments