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
|
use super::client_data::ClientDataHash;
use super::commands::get_assertion::{GetAssertion, GetAssertionOptions};
use super::commands::{CommandError, PinUvAuthCommand, RequestCtap1, Retryable, StatusCode};
use crate::authenticatorservice::GetAssertionExtensions;
use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED};
use crate::crypto::PinUvAuthToken;
use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper};
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::FidoDevice;
use crate::u2ftypes::CTAP1RequestAPDU;
use sha2::{Digest, Sha256};
/// This command is used to check which key_handle is valid for this
/// token. This is sent before a GetAssertion command, to determine which
/// is valid for a specific token and which key_handle GetAssertion
/// should send to the token. Or before a MakeCredential command, to determine
/// if this token is already registered or not.
#[derive(Debug)]
pub(crate) struct CheckKeyHandle<'assertion> {
pub(crate) key_handle: &'assertion [u8],
pub(crate) client_data_hash: &'assertion [u8],
pub(crate) rp: &'assertion RelyingPartyWrapper,
}
impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
type Output = ();
type AdditionalInfo = ();
fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
// In theory, we only need to do this for up=true, for up=false, we could
// use U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN instead and use the answer directly.
// But that would involve another major refactoring to implement, and so we accept
// that we will send the final request twice to the authenticator. Once with
// U2F_CHECK_IS_REGISTERED followed by U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN.
let flags = U2F_CHECK_IS_REGISTERED;
let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len());
auth_data.extend_from_slice(self.client_data_hash);
auth_data.extend_from_slice(self.rp.hash().as_ref());
auth_data.extend_from_slice(&[self.key_handle.len() as u8]);
auth_data.extend_from_slice(self.key_handle);
let cmd = U2F_AUTHENTICATE;
let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
Ok((apdu, ()))
}
fn handle_response_ctap1(
&self,
status: Result<(), ApduErrorStatus>,
_input: &[u8],
_add_info: &Self::AdditionalInfo,
) -> Result<Self::Output, Retryable<HIDError>> {
// From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register
// if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to
// simply check whether the provided key handle was originally created by this token,
// and whether it was created for the provided application parameter. If so, the U2F
// token MUST respond with an authentication response
// message:error:test-of-user-presence-required (note that despite the name this
// signals a success condition). If the key handle was not created by this U2F
// token, or if it was created for a different application parameter, the token MUST
// respond with an authentication response message:error:bad-key-handle.
match status {
Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()),
Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))),
}
}
}
/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or
/// authenticatorGetAssertion's allowList contain credential IDs that are already
/// present on an authenticator, a platform typically invokes authenticatorGetAssertion
/// with the "up" option key set to false and optionally pinUvAuthParam one or more times.
/// For CTAP1, the resulting list will always be of length 1.
pub(crate) fn do_credential_list_filtering_ctap1<Dev: FidoDevice>(
dev: &mut Dev,
cred_list: &[PublicKeyCredentialDescriptor],
rp: &RelyingPartyWrapper,
client_data_hash: &ClientDataHash,
) -> Option<PublicKeyCredentialDescriptor> {
let key_handle = cred_list
.iter()
// key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2.
// Filter out key-handles that are too long (can happen if this is a CTAP2-request,
// but the token only speaks CTAP1).
.filter(|key_handle| key_handle.id.len() < 256)
.find_map(|key_handle| {
let check_command = CheckKeyHandle {
key_handle: key_handle.id.as_ref(),
client_data_hash: client_data_hash.as_ref(),
rp,
};
let res = dev.send_ctap1(&check_command);
match res {
Ok(_) => Some(key_handle.clone()),
_ => None,
}
});
key_handle
}
/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or
/// authenticatorGetAssertion's allowList contain credential IDs that are already
/// present on an authenticator, a platform typically invokes authenticatorGetAssertion
/// with the "up" option key set to false and optionally pinUvAuthParam one or more times.
pub(crate) fn do_credential_list_filtering_ctap2<Dev: FidoDevice>(
dev: &mut Dev,
cred_list: &[PublicKeyCredentialDescriptor],
rp: &RelyingPartyWrapper,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<Vec<PublicKeyCredentialDescriptor>, AuthenticatorError> {
let info = dev
.get_authenticator_info()
.ok_or(HIDError::DeviceNotInitialized)?;
let mut cred_list = cred_list.to_vec();
// Step 1.0: Find out how long the exclude_list/allow_list is allowed to be
// If the token doesn't tell us, we assume a length of 1
let mut chunk_size = match info.max_credential_count_in_list {
// Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means
// falling back to a chunk size of 1 as the bare minimum.
None | Some(0) => 1,
Some(x) => x,
};
// Step 1.1: The device only supports keys up to a certain length.
// Filter out all keys that are longer, because they can't be
// from this device anyways.
match info.max_credential_id_length {
None => { /* no-op */ }
// Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means
// falling back to a chunk size of 1 as the bare minimum.
Some(0) => {
chunk_size = 1;
}
Some(max_key_length) => {
cred_list.retain(|k| k.id.len() <= max_key_length);
}
}
// Step 1.2: Return early, if we only have one chunk anyways
if cred_list.len() <= chunk_size {
return Ok(cred_list);
}
let chunked_list = cred_list.chunks(chunk_size);
// Step 2: If we have more than one chunk: Loop over all, doing GetAssertion
// and if one of them comes back with a success, use only that chunk.
let mut final_list = Vec::new();
for chunk in chunked_list {
let mut silent_assert = GetAssertion::new(
ClientDataHash(Sha256::digest("").into()),
rp.clone(),
chunk.to_vec(),
GetAssertionOptions {
user_verification: if pin_uv_auth_token.is_some() {
None
} else {
Some(false)
},
user_presence: Some(false),
},
GetAssertionExtensions::default(),
None,
None,
);
silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?;
let res = dev.send_msg(&silent_assert);
match res {
Ok(response) => {
// This chunk contains a key_handle that is already known to the device.
// Filter out all credentials the device returned. Those are valid.
let credential_ids = response
.0
.iter()
.filter_map(|a| a.credentials.clone())
.collect();
// Replace credential_id_list with the valid credentials
final_list = credential_ids;
break;
}
Err(HIDError::Command(CommandError::StatusCode(StatusCode::NoCredentials, _))) => {
// No-op: Go to next chunk.
}
Err(e) => {
// Some unexpected error
return Err(e.into());
}
}
}
// Step 3: Now ExcludeList/AllowList is either empty or has one batch with a 'known' credential.
// Send it as a normal Request and expect a "CredentialExcluded"-error in case of
// MakeCredential or a Success in case of GetAssertion
Ok(final_list)
}
|