authenticator_ctap2_2021/ctap2/commands/
mod.rs

1use crate::crypto;
2use crate::ctap2::client_data::{ClientDataHash, CollectedClientData};
3use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError};
4use crate::errors::AuthenticatorError;
5use crate::transport::errors::{ApduErrorStatus, HIDError};
6use crate::transport::FidoDevice;
7use serde_cbor::{error::Error as CborError, Value};
8use serde_json as json;
9use std::error::Error as StdErrorT;
10use std::fmt;
11use std::io::{Read, Write};
12
13pub(crate) mod client_pin;
14pub(crate) mod get_assertion;
15pub(crate) mod get_info;
16pub(crate) mod get_next_assertion;
17pub(crate) mod get_version;
18pub(crate) mod make_credentials;
19pub(crate) mod reset;
20pub(crate) mod selection;
21
22pub trait Request<T>
23where
24    Self: fmt::Debug,
25    Self: RequestCtap1<Output = T>,
26    Self: RequestCtap2<Output = T>,
27{
28    fn is_ctap2_request(&self) -> bool;
29}
30
31/// Retryable wraps an error type and may ask manager to retry sending a
32/// command, this is useful for ctap1 where token will reply with "condition not
33/// sufficient" because user needs to press the button.
34#[derive(Debug)]
35pub enum Retryable<T> {
36    Retry,
37    Error(T),
38}
39
40impl<T> Retryable<T> {
41    pub fn is_retry(&self) -> bool {
42        matches!(*self, Retryable::Retry)
43    }
44
45    pub fn is_error(&self) -> bool {
46        !self.is_retry()
47    }
48}
49
50impl<T> From<T> for Retryable<T> {
51    fn from(e: T) -> Self {
52        Retryable::Error(e)
53    }
54}
55
56pub trait RequestCtap1: fmt::Debug {
57    type Output;
58
59    fn apdu_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
60    where
61        Dev: FidoDevice + Read + Write + fmt::Debug;
62
63    fn handle_response_ctap1(
64        &self,
65        status: Result<(), ApduErrorStatus>,
66        input: &[u8],
67    ) -> Result<Self::Output, Retryable<HIDError>>;
68}
69
70pub trait RequestCtap2: fmt::Debug {
71    type Output;
72
73    fn command() -> Command;
74
75    fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
76    where
77        Dev: FidoDevice + Read + Write + fmt::Debug;
78
79    fn handle_response_ctap2<Dev>(
80        &self,
81        dev: &mut Dev,
82        input: &[u8],
83    ) -> Result<Self::Output, HIDError>
84    where
85        Dev: FidoDevice + Read + Write + fmt::Debug;
86}
87
88pub(crate) trait PinAuthCommand {
89    fn pin(&self) -> &Option<Pin>;
90    fn set_pin(&mut self, pin: Option<Pin>);
91    fn pin_auth(&self) -> &Option<PinAuth>;
92    fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>);
93    fn client_data(&self) -> &CollectedClientData;
94    fn unset_uv_option(&mut self);
95    fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
96        if !dev.supports_ctap2() {
97            self.set_pin_auth(None);
98            return Ok(());
99        }
100
101        let client_data_hash = self
102            .client_data()
103            .hash()
104            .map_err(|e| AuthenticatorError::HIDError(HIDError::Command(CommandError::Json(e))))?;
105        let pin_auth = match calculate_pin_auth(dev, &client_data_hash, &self.pin()) {
106            Ok(pin_auth) => pin_auth,
107            Err(e) => {
108                return Err(repackage_pin_errors(dev, e));
109            }
110        };
111        self.set_pin_auth(pin_auth);
112        Ok(())
113    }
114}
115
116pub(crate) fn repackage_pin_errors<D: FidoDevice>(
117    dev: &mut D,
118    error: AuthenticatorError,
119) -> AuthenticatorError {
120    match error {
121        AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
122            StatusCode::PinInvalid,
123            _,
124        ))) => {
125            // If the given PIN was wrong, determine no. of left retries
126            let cmd = GetRetries::new();
127            let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
128            return AuthenticatorError::PinError(PinError::InvalidPin(retries));
129        }
130        AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
131            StatusCode::PinAuthBlocked,
132            _,
133        ))) => {
134            return AuthenticatorError::PinError(PinError::PinAuthBlocked);
135        }
136        AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
137            StatusCode::PinBlocked,
138            _,
139        ))) => {
140            return AuthenticatorError::PinError(PinError::PinBlocked);
141        }
142        AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
143            StatusCode::PinRequired,
144            _,
145        ))) => {
146            return AuthenticatorError::PinError(PinError::PinRequired);
147        }
148        // TODO(MS): Add "PinNotSet"
149        // TODO(MS): Add "PinPolicyViolated"
150        err => {
151            return err;
152        }
153    }
154}
155
156// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api
157// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api
158#[repr(u8)]
159#[derive(Debug)]
160pub enum Command {
161    MakeCredentials = 0x01,
162    GetAssertion = 0x02,
163    GetInfo = 0x04,
164    ClientPin = 0x06,
165    Reset = 0x07,
166    GetNextAssertion = 0x08,
167    Selection = 0x0B,
168}
169
170impl Command {
171    #[cfg(test)]
172    pub fn from_u8(v: u8) -> Option<Command> {
173        match v {
174            0x01 => Some(Command::MakeCredentials),
175            0x02 => Some(Command::GetAssertion),
176            0x04 => Some(Command::GetInfo),
177            0x06 => Some(Command::ClientPin),
178            0x07 => Some(Command::Reset),
179            0x08 => Some(Command::GetNextAssertion),
180            _ => None,
181        }
182    }
183}
184
185#[derive(Debug)]
186pub enum StatusCode {
187    /// Indicates successful response.
188    OK,
189    /// The command is not a valid CTAP command.
190    InvalidCommand,
191    /// The command included an invalid parameter.
192    InvalidParameter,
193    /// Invalid message or item length.
194    InvalidLength,
195    /// Invalid message sequencing.
196    InvalidSeq,
197    /// Message timed out.
198    Timeout,
199    /// Channel busy.
200    ChannelBusy,
201    /// Command requires channel lock.
202    LockRequired,
203    /// Command not allowed on this cid.
204    InvalidChannel,
205    /// Invalid/unexpected CBOR error.
206    CBORUnexpectedType,
207    /// Error when parsing CBOR.
208    InvalidCBOR,
209    /// Missing non-optional parameter.
210    MissingParameter,
211    /// Limit for number of items exceeded.
212    LimitExceeded,
213    /// Unsupported extension.
214    UnsupportedExtension,
215    /// Valid credential found in the exclude list.
216    CredentialExcluded,
217    /// Processing (Lengthy operation is in progress).
218    Processing,
219    /// Credential not valid for the authenticator.
220    InvalidCredential,
221    /// Authentication is waiting for user interaction.
222    UserActionPending,
223    /// Processing, lengthy operation is in progress.
224    OperationPending,
225    /// No request is pending.
226    NoOperations,
227    /// Authenticator does not support requested algorithm.
228    UnsupportedAlgorithm,
229    /// Not authorized for requested operation.
230    OperationDenied,
231    /// Internal key storage is full.
232    KeyStoreFull,
233    /// No outstanding operations.
234    NoOperationPending,
235    /// Unsupported option.
236    UnsupportedOption,
237    /// Not a valid option for current operation.
238    InvalidOption,
239    /// Pending keep alive was cancelled.
240    KeepaliveCancel,
241    /// No valid credentials provided.
242    NoCredentials,
243    /// Timeout waiting for user interaction.
244    UserActionTimeout,
245    /// Continuation command, such as, authenticatorGetNextAssertion not
246    /// allowed.
247    NotAllowed,
248    /// PIN Invalid.
249    PinInvalid,
250    /// PIN Blocked.
251    PinBlocked,
252    /// PIN authentication,pinAuth, verification failed.
253    PinAuthInvalid,
254    /// PIN authentication,pinAuth, blocked. Requires power recycle to reset.
255    PinAuthBlocked,
256    /// No PIN has been set.
257    PinNotSet,
258    /// PIN is required for the selected operation.
259    PinRequired,
260    /// PIN policy violation. Currently only enforces minimum length.
261    PinPolicyViolation,
262    /// pinToken expired on authenticator.
263    PinTokenExpired,
264    /// Authenticator cannot handle this request due to memory constraints.
265    RequestTooLarge,
266    /// The current operation has timed out.
267    ActionTimeout,
268    /// User presence is required for the requested operation.
269    UpRequired,
270
271    /// Unknown status.
272    Unknown(u8),
273}
274
275impl StatusCode {
276    fn is_ok(&self) -> bool {
277        matches!(*self, StatusCode::OK)
278    }
279
280    fn device_busy(&self) -> bool {
281        matches!(*self, StatusCode::ChannelBusy)
282    }
283}
284
285impl From<u8> for StatusCode {
286    fn from(value: u8) -> StatusCode {
287        match value {
288            0x00 => StatusCode::OK,
289            0x01 => StatusCode::InvalidCommand,
290            0x02 => StatusCode::InvalidParameter,
291            0x03 => StatusCode::InvalidLength,
292            0x04 => StatusCode::InvalidSeq,
293            0x05 => StatusCode::Timeout,
294            0x06 => StatusCode::ChannelBusy,
295            0x0A => StatusCode::LockRequired,
296            0x0B => StatusCode::InvalidChannel,
297            0x11 => StatusCode::CBORUnexpectedType,
298            0x12 => StatusCode::InvalidCBOR,
299            0x14 => StatusCode::MissingParameter,
300            0x15 => StatusCode::LimitExceeded,
301            0x16 => StatusCode::UnsupportedExtension,
302            0x19 => StatusCode::CredentialExcluded,
303            0x21 => StatusCode::Processing,
304            0x22 => StatusCode::InvalidCredential,
305            0x23 => StatusCode::UserActionPending,
306            0x24 => StatusCode::OperationPending,
307            0x25 => StatusCode::NoOperations,
308            0x26 => StatusCode::UnsupportedAlgorithm,
309            0x27 => StatusCode::OperationDenied,
310            0x28 => StatusCode::KeyStoreFull,
311            0x2A => StatusCode::NoOperationPending,
312            0x2B => StatusCode::UnsupportedOption,
313            0x2C => StatusCode::InvalidOption,
314            0x2D => StatusCode::KeepaliveCancel,
315            0x2E => StatusCode::NoCredentials,
316            0x2f => StatusCode::UserActionTimeout,
317            0x30 => StatusCode::NotAllowed,
318            0x31 => StatusCode::PinInvalid,
319            0x32 => StatusCode::PinBlocked,
320            0x33 => StatusCode::PinAuthInvalid,
321            0x34 => StatusCode::PinAuthBlocked,
322            0x35 => StatusCode::PinNotSet,
323            0x36 => StatusCode::PinRequired,
324            0x37 => StatusCode::PinPolicyViolation,
325            0x38 => StatusCode::PinTokenExpired,
326            0x39 => StatusCode::RequestTooLarge,
327            0x3A => StatusCode::ActionTimeout,
328            0x3B => StatusCode::UpRequired,
329
330            othr => StatusCode::Unknown(othr),
331        }
332    }
333}
334
335#[cfg(test)]
336impl Into<u8> for StatusCode {
337    fn into(self) -> u8 {
338        match self {
339            StatusCode::OK => 0x00,
340            StatusCode::InvalidCommand => 0x01,
341            StatusCode::InvalidParameter => 0x02,
342            StatusCode::InvalidLength => 0x03,
343            StatusCode::InvalidSeq => 0x04,
344            StatusCode::Timeout => 0x05,
345            StatusCode::ChannelBusy => 0x06,
346            StatusCode::LockRequired => 0x0A,
347            StatusCode::InvalidChannel => 0x0B,
348            StatusCode::CBORUnexpectedType => 0x11,
349            StatusCode::InvalidCBOR => 0x12,
350            StatusCode::MissingParameter => 0x14,
351            StatusCode::LimitExceeded => 0x15,
352            StatusCode::UnsupportedExtension => 0x16,
353            StatusCode::CredentialExcluded => 0x19,
354            StatusCode::Processing => 0x21,
355            StatusCode::InvalidCredential => 0x22,
356            StatusCode::UserActionPending => 0x23,
357            StatusCode::OperationPending => 0x24,
358            StatusCode::NoOperations => 0x25,
359            StatusCode::UnsupportedAlgorithm => 0x26,
360            StatusCode::OperationDenied => 0x27,
361            StatusCode::KeyStoreFull => 0x28,
362            StatusCode::NoOperationPending => 0x2A,
363            StatusCode::UnsupportedOption => 0x2B,
364            StatusCode::InvalidOption => 0x2C,
365            StatusCode::KeepaliveCancel => 0x2D,
366            StatusCode::NoCredentials => 0x2E,
367            StatusCode::UserActionTimeout => 0x2f,
368            StatusCode::NotAllowed => 0x30,
369            StatusCode::PinInvalid => 0x31,
370            StatusCode::PinBlocked => 0x32,
371            StatusCode::PinAuthInvalid => 0x33,
372            StatusCode::PinAuthBlocked => 0x34,
373            StatusCode::PinNotSet => 0x35,
374            StatusCode::PinRequired => 0x36,
375            StatusCode::PinPolicyViolation => 0x37,
376            StatusCode::PinTokenExpired => 0x38,
377            StatusCode::RequestTooLarge => 0x39,
378            StatusCode::ActionTimeout => 0x3A,
379            StatusCode::UpRequired => 0x3B,
380
381            StatusCode::Unknown(othr) => othr,
382        }
383    }
384}
385
386#[derive(Debug)]
387pub enum CommandError {
388    InputTooSmall,
389    MissingRequiredField(&'static str),
390    Deserializing(CborError),
391    Serializing(CborError),
392    StatusCode(StatusCode, Option<Value>),
393    Json(json::Error),
394    Crypto(crypto::CryptoError),
395    UnsupportedPinProtocol,
396}
397
398impl fmt::Display for CommandError {
399    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
400        match *self {
401            CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
402            CommandError::MissingRequiredField(field) => {
403                write!(f, "CommandError: Missing required field {}", field)
404            }
405            CommandError::Deserializing(ref e) => {
406                write!(f, "CommandError: Error while parsing: {}", e)
407            }
408            CommandError::Serializing(ref e) => {
409                write!(f, "CommandError: Error while serializing: {}", e)
410            }
411            CommandError::StatusCode(ref code, ref value) => {
412                write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value)
413            }
414            CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e),
415            CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e),
416            CommandError::UnsupportedPinProtocol => {
417                write!(f, "CommandError: Pin protocol is not supported")
418            }
419        }
420    }
421}
422
423impl StdErrorT for CommandError {}
424
425pub(crate) fn calculate_pin_auth<Dev>(
426    dev: &mut Dev,
427    client_data_hash: &ClientDataHash,
428    pin: &Option<Pin>,
429) -> Result<Option<PinAuth>, AuthenticatorError>
430where
431    Dev: FidoDevice,
432{
433    // Not reusing the shared secret here, if it exists, since we might start again
434    // with a different PIN (e.g. if the last one was wrong)
435    let (shared_secret, info) = dev.establish_shared_secret()?;
436
437    // TODO(MS): What to do if token supports client_pin, but none has been set: Some(false)
438    //           AND a Pin is not None?
439    let pin_auth = if info.options.client_pin == Some(true) {
440        let pin = pin
441            .as_ref()
442            .ok_or(HIDError::Command(CommandError::StatusCode(
443                StatusCode::PinRequired,
444                None,
445            )))?;
446
447        let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?;
448        let pin_token = dev.send_cbor(&pin_command)?;
449
450        Some(
451            pin_token
452                .auth(client_data_hash.as_ref())
453                .map_err(CommandError::Crypto)?,
454        )
455    } else {
456        None
457    };
458
459    Ok(pin_auth)
460}