summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator/src/transport/mod.rs
blob: 318934ed3608de4c758da9013287dd7d39a8650f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
use crate::crypto::{PinUvAuthProtocol, PinUvAuthToken, SharedSecret};
use crate::ctap2::commands::client_pin::{
    ClientPIN, ClientPinResponse, GetKeyAgreement, GetPinToken,
    GetPinUvAuthTokenUsingPinWithPermissions, GetPinUvAuthTokenUsingUvWithPermissions,
    PinUvAuthTokenPermission,
};
use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult};
use crate::ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorVersion, GetInfo};
use crate::ctap2::commands::get_version::{GetVersion, U2FInfo};
use crate::ctap2::commands::make_credentials::{
    dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsResult,
};
use crate::ctap2::commands::reset::Reset;
use crate::ctap2::commands::selection::Selection;
use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, StatusCode};
use crate::ctap2::preflight::CheckKeyHandle;
use crate::transport::device_selector::BlinkResult;
use crate::transport::errors::HIDError;

use crate::Pin;
use std::convert::TryFrom;
use std::fmt;

pub mod device_selector;
pub mod errors;
pub mod hid;

#[cfg(all(
    any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"),
    not(test)
))]
pub mod hidproto;

#[cfg(all(target_os = "linux", not(test)))]
#[path = "linux/mod.rs"]
pub mod platform;

#[cfg(all(target_os = "freebsd", not(test)))]
#[path = "freebsd/mod.rs"]
pub mod platform;

#[cfg(all(target_os = "netbsd", not(test)))]
#[path = "netbsd/mod.rs"]
pub mod platform;

#[cfg(all(target_os = "openbsd", not(test)))]
#[path = "openbsd/mod.rs"]
pub mod platform;

#[cfg(all(target_os = "macos", not(test)))]
#[path = "macos/mod.rs"]
pub mod platform;

#[cfg(all(target_os = "windows", not(test)))]
#[path = "windows/mod.rs"]
pub mod platform;

#[cfg(not(any(
    target_os = "linux",
    target_os = "freebsd",
    target_os = "openbsd",
    target_os = "netbsd",
    target_os = "macos",
    target_os = "windows",
    test
)))]
#[path = "stub/mod.rs"]
pub mod platform;

#[cfg(test)]
#[path = "mock/mod.rs"]
pub mod platform;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FidoProtocol {
    CTAP1,
    CTAP2,
}

pub trait FidoDeviceIO {
    fn send_msg<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
        &mut self,
        msg: &Req,
    ) -> Result<Out, HIDError> {
        self.send_msg_cancellable(msg, &|| true)
    }

    fn send_cbor<Req: RequestCtap2>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> {
        self.send_cbor_cancellable(msg, &|| true)
    }

    fn send_ctap1<Req: RequestCtap1>(&mut self, msg: &Req) -> Result<Req::Output, HIDError> {
        self.send_ctap1_cancellable(msg, &|| true)
    }

    fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>(
        &mut self,
        msg: &Req,
        keep_alive: &dyn Fn() -> bool,
    ) -> Result<Out, HIDError>;

    fn send_cbor_cancellable<Req: RequestCtap2>(
        &mut self,
        msg: &Req,
        keep_alive: &dyn Fn() -> bool,
    ) -> Result<Req::Output, HIDError>;

    fn send_ctap1_cancellable<Req: RequestCtap1>(
        &mut self,
        msg: &Req,
        keep_alive: &dyn Fn() -> bool,
    ) -> Result<Req::Output, HIDError>;
}

pub trait TestDevice {
    #[cfg(test)]
    fn skip_serialization(&self) -> bool;
    #[cfg(test)]
    fn send_ctap1_unserialized<Req: RequestCtap1>(
        &mut self,
        msg: &Req,
    ) -> Result<Req::Output, HIDError>;
    #[cfg(test)]
    fn send_ctap2_unserialized<Req: RequestCtap2>(
        &mut self,
        msg: &Req,
    ) -> Result<Req::Output, HIDError>;
}

pub trait FidoDevice: FidoDeviceIO
where
    Self: Sized,
    Self: fmt::Debug,
{
    fn pre_init(&mut self) -> Result<(), HIDError>;
    fn initialized(&self) -> bool;

    // Check if the device is actually a token
    fn is_u2f(&mut self) -> bool;
    fn should_try_ctap2(&self) -> bool;
    fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>;
    fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
    fn refresh_authenticator_info(&mut self) -> Option<&AuthenticatorInfo> {
        let command = GetInfo::default();
        if let Ok(info) = self.send_cbor(&command) {
            debug!("Refreshed authenticator info: {:?}", info);
            self.set_authenticator_info(info);
        }
        self.get_authenticator_info()
    }

    // `get_protocol()` indicates whether we're using CTAP1 or CTAP2.
    // Prior to initializing the device, `get_protocol()` should return CTAP2 unless
    // there's a reason to believe that the device does not support CTAP2 (e.g. if
    // it's a HID device and it does not have the CBOR capability).
    fn get_protocol(&self) -> FidoProtocol;

    // We do not provide a generic `set_protocol(..)` function as this would have complicated
    // interactions with the AuthenticatorInfo state.
    fn downgrade_to_ctap1(&mut self);

    fn get_shared_secret(&self) -> Option<&SharedSecret>;
    fn set_shared_secret(&mut self, secret: SharedSecret);

    fn init(&mut self) -> Result<(), HIDError> {
        self.pre_init()?;

        if self.should_try_ctap2() {
            let command = GetInfo::default();
            if let Ok(info) = self.send_cbor(&command) {
                debug!("{:?}", info);
                if info.max_supported_version() == AuthenticatorVersion::U2F_V2 {
                    self.downgrade_to_ctap1();
                }
                self.set_authenticator_info(info);
                return Ok(());
            }
        }

        self.downgrade_to_ctap1();
        // We want to return an error here if this device doesn't support CTAP1,
        // so we send a U2F_VERSION command.
        let command = GetVersion::default();
        self.send_ctap1(&command)?;
        Ok(())
    }

    fn block_and_blink(&mut self, keep_alive: &dyn Fn() -> bool) -> BlinkResult {
        let supports_select_cmd = self.get_protocol() == FidoProtocol::CTAP2
            && self.get_authenticator_info().map_or(false, |i| {
                i.versions.contains(&AuthenticatorVersion::FIDO_2_1)
            });
        let resp = if supports_select_cmd {
            let msg = Selection {};
            self.send_cbor_cancellable(&msg, keep_alive)
        } else {
            // We need to fake a blink-request, because FIDO2.0 forgot to specify one
            // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential
            let msg = dummy_make_credentials_cmd();
            info!("Trying to blink: {:?}", &msg);
            // We don't care about the Ok-value, just if it is Ok or not
            self.send_msg_cancellable(&msg, keep_alive).map(|_| ())
        };

        match resp {
            // Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request,
            // but Yubikeys for example return PinAuthInvalid. A successful return is also possible
            // for CTAP1-tokens so we catch those here as well.
            Ok(_)
            | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)))
            | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)))
            | Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => {
                BlinkResult::DeviceSelected
            }
            // We cancelled the receive, because another device was selected.
            Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _)))
            | Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)))
            | Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => {
                // TODO: Repeat the request, if it is a UserActionTimeout?
                debug!("Device {:?} got cancelled", &self);
                BlinkResult::Cancelled
            }
            // Something unexpected happened, so we assume this device is not usable and
            // interpreting this equivalent to being cancelled.
            e => {
                info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e);
                BlinkResult::Cancelled
            }
        }
    }

    fn establish_shared_secret(
        &mut self,
        alive: &dyn Fn() -> bool,
    ) -> Result<SharedSecret, HIDError> {
        // CTAP1 devices don't support establishing a shared secret
        let info = match (self.get_protocol(), self.get_authenticator_info()) {
            (FidoProtocol::CTAP2, Some(info)) => info,
            _ => return Err(HIDError::UnsupportedCommand),
        };

        let pin_protocol = PinUvAuthProtocol::try_from(info)?;

        // Not reusing the shared secret here, if it exists, since we might start again
        // with a different PIN (e.g. if the last one was wrong)
        let pin_command = GetKeyAgreement::new(pin_protocol.clone());
        let resp = self.send_cbor_cancellable(&pin_command, alive)?;
        if let Some(device_key_agreement_key) = resp.key_agreement {
            let shared_secret = pin_protocol
                .encapsulate(&device_key_agreement_key)
                .map_err(CommandError::from)?;
            self.set_shared_secret(shared_secret.clone());
            Ok(shared_secret)
        } else {
            Err(HIDError::Command(CommandError::MissingRequiredField(
                "key_agreement",
            )))
        }
    }

    /// CTAP 2.0-only version:
    /// "Getting pinUvAuthToken using getPinToken (superseded)"
    fn get_pin_token(
        &mut self,
        pin: &Option<Pin>,
        alive: &dyn Fn() -> bool,
    ) -> Result<PinUvAuthToken, HIDError> {
        // Asking the user for PIN before establishing the shared secret
        let pin = pin
            .as_ref()
            .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;

        // Not reusing the shared secret here, if it exists, since we might start again
        // with a different PIN (e.g. if the last one was wrong)
        let shared_secret = self.establish_shared_secret(alive)?;

        let pin_command = GetPinToken::new(&shared_secret, pin);
        let resp = self.send_cbor_cancellable(&pin_command, alive)?;
        if let Some(encrypted_pin_token) = resp.pin_token {
            // CTAP 2.1 spec:
            // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions
            // of `mc` and `ga` (value 0x03) are granted for the returned pinUvAuthToken.
            let default_permissions = PinUvAuthTokenPermission::default();
            let pin_token = shared_secret
                .decrypt_pin_token(default_permissions, encrypted_pin_token.as_ref())
                .map_err(CommandError::from)?;
            Ok(pin_token)
        } else {
            Err(HIDError::Command(CommandError::MissingRequiredField(
                "pin_token",
            )))
        }
    }

    fn get_pin_uv_auth_token_using_uv_with_permissions(
        &mut self,
        permission: PinUvAuthTokenPermission,
        rp_id: Option<&String>,
        alive: &dyn Fn() -> bool,
    ) -> Result<PinUvAuthToken, HIDError> {
        // Explicitly not reusing the shared secret here
        let shared_secret = self.establish_shared_secret(alive)?;
        let pin_command = GetPinUvAuthTokenUsingUvWithPermissions::new(
            &shared_secret,
            permission,
            rp_id.cloned(),
        );

        let resp = self.send_cbor_cancellable(&pin_command, alive)?;

        if let Some(encrypted_pin_token) = resp.pin_token {
            let pin_token = shared_secret
                .decrypt_pin_token(permission, encrypted_pin_token.as_ref())
                .map_err(CommandError::from)?;
            Ok(pin_token)
        } else {
            Err(HIDError::Command(CommandError::MissingRequiredField(
                "pin_token",
            )))
        }
    }

    fn get_pin_uv_auth_token_using_pin_with_permissions(
        &mut self,
        pin: &Option<Pin>,
        permission: PinUvAuthTokenPermission,
        rp_id: Option<&String>,
        alive: &dyn Fn() -> bool,
    ) -> Result<PinUvAuthToken, HIDError> {
        // Asking the user for PIN before establishing the shared secret
        let pin = pin
            .as_ref()
            .ok_or(CommandError::StatusCode(StatusCode::PinRequired, None))?;

        // Not reusing the shared secret here, if it exists, since we might start again
        // with a different PIN (e.g. if the last one was wrong)
        let shared_secret = self.establish_shared_secret(alive)?;
        let pin_command = GetPinUvAuthTokenUsingPinWithPermissions::new(
            &shared_secret,
            pin,
            permission,
            rp_id.cloned(),
        );

        let resp = self.send_cbor_cancellable(&pin_command, alive)?;

        if let Some(encrypted_pin_token) = resp.pin_token {
            let pin_token = shared_secret
                .decrypt_pin_token(permission, encrypted_pin_token.as_ref())
                .map_err(CommandError::from)?;
            Ok(pin_token)
        } else {
            Err(HIDError::Command(CommandError::MissingRequiredField(
                "pin_token",
            )))
        }
    }
}

pub trait VirtualFidoDevice: FidoDevice {
    fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError>;
    fn client_pin(&self, req: &ClientPIN) -> Result<ClientPinResponse, HIDError>;
    fn get_assertion(&self, req: &GetAssertion) -> Result<Vec<GetAssertionResult>, HIDError>;
    fn get_info(&self) -> Result<AuthenticatorInfo, HIDError>;
    fn get_version(&self, req: &GetVersion) -> Result<U2FInfo, HIDError>;
    fn make_credentials(&self, req: &MakeCredentials) -> Result<MakeCredentialsResult, HIDError>;
    fn reset(&self, req: &Reset) -> Result<(), HIDError>;
    fn selection(&self, req: &Selection) -> Result<(), HIDError>;
}