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
use std::time::Duration;
use cosey as cose;
use serde_bytes::ByteBuf;
use sha2::{Digest, Sha256};
use tracing::{error, trace};
use x509_parser::nom::AsBytes;
use super::webauthn::MakeCredentialRequest;
use crate::fido::{AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags};
use crate::ops::webauthn::{
GetAssertionRequest, GetAssertionResponse, MakeCredentialResponse, UserVerificationRequirement,
};
use crate::proto::ctap1::{Ctap1RegisterRequest, Ctap1SignRequest};
use crate::proto::ctap1::{Ctap1RegisterResponse, Ctap1SignResponse};
use crate::proto::ctap2::cbor;
use crate::proto::ctap2::{
Ctap2AttestationStatement, Ctap2GetAssertionResponse, Ctap2MakeCredentialResponse,
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, FidoU2fAttestationStmt,
};
use crate::webauthn::{CtapError, Error, PlatformError};
// FIDO U2F operations can be aliased to CTAP1 requests, as they have no other representation.
pub type RegisterRequest = Ctap1RegisterRequest;
pub type RegisterResponse = Ctap1RegisterResponse;
pub type SignRequest = Ctap1SignRequest;
pub type SignResponse = Ctap1SignResponse;
impl SignRequest {
pub fn new_upgraded(
rp_id_hash: &[u8],
challenge: &[u8],
key_handle: &[u8],
timeout: Duration,
) -> Self {
Self {
app_id_hash: Vec::from(rp_id_hash),
challenge: Vec::from(challenge),
key_handle: Vec::from(key_handle),
timeout,
require_user_presence: true,
}
}
}
pub trait UpgradableResponse<T, R> {
fn try_upgrade(&self, request: &R) -> Result<T, Error>;
}
impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for RegisterResponse {
fn try_upgrade(
&self,
request: &MakeCredentialRequest,
) -> Result<MakeCredentialResponse, Error> {
// Let x9encodedUserPublicKeybe the user public key returned in the U2F registration response message [U2FRawMsgs].
// Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value
// from ANS X9.62 / Sec-1 v2 uncompressed curve point representation [SEC1V2]
// to COSE_Key representation ([RFC8152] Section 7).
let Ok(encoded_point) = p256::EncodedPoint::from_bytes(&self.public_key) else {
error!(?self.public_key, "Failed to parse public key as SEC-1 v2 encoded point");
return Err(Error::Ctap(CtapError::Other));
};
let x_bytes = encoded_point.x().ok_or_else(|| {
error!("Public key is the identity point");
Error::Platform(PlatformError::CryptoError(
"public key is the identity point".into(),
))
})?;
let y_bytes = encoded_point.y().ok_or_else(|| {
error!("Public key is identity or compressed");
Error::Platform(PlatformError::CryptoError(
"public key is identity or compressed".into(),
))
})?;
let x: heapless::Vec<u8, 32> =
heapless::Vec::from_slice(x_bytes.as_bytes()).map_err(|_| {
Error::Platform(PlatformError::CryptoError(
"x coordinate exceeds 32 bytes".into(),
))
})?;
let y: heapless::Vec<u8, 32> =
heapless::Vec::from_slice(y_bytes.as_bytes()).map_err(|_| {
Error::Platform(PlatformError::CryptoError(
"y coordinate exceeds 32 bytes".into(),
))
})?;
let cose_public_key = cose::PublicKey::P256Key(cose::P256PublicKey {
x: x.into(),
y: y.into(),
});
let cose_encoded_public_key = cbor::to_vec(&cose_public_key)?;
// Canonical CBOR encoding of the COSE P-256 key is 77 bytes for the
// fields we set; return a typed error if a future encoder change
// produces a different length rather than `assert!`-panicking.
if cose_encoded_public_key.len() != 77 {
error!(
len = cose_encoded_public_key.len(),
"COSE-encoded P-256 public key is not 77 bytes"
);
return Err(Error::Platform(PlatformError::CryptoError(
"unexpected COSE-encoded public key length".into(),
)));
}
// Let attestedCredData be a byte string with following structure:
//
// Length (in bytes) Description Value
// -------------------------------------------------------------------------------------------------------------
// 16 The AAGUID of the authenticator. Initialized with all zeros.
// 2 Byte length L of Credential ID Initialized with credentialIdLength bytes.
// credentialIdLength Credential ID. Initialized with credentialId bytes.
// 77 The credential public key. Initialized with coseEncodedCredentialPublicKey bytes.
let attested_cred_data = AttestedCredentialData {
aaguid: [0u8; 16], // aaguid zeros
credential_id: self.key_handle.clone(),
credential_public_key: cose_encoded_public_key,
};
// Initialize authenticatorData:
// Let flags be a byte whose zeroth bit (bit 0, UP) is set, and whose sixth bit (bit 6, AT) is set,
// and all other bits are zero (bit zero is the least significant bit)
let flags =
AuthenticatorDataFlags::USER_PRESENT | AuthenticatorDataFlags::ATTESTED_CREDENTIALS;
// Let signCount be a 4-byte unsigned integer initialized to zero.
let signature_count: u32 = 0;
// Let authenticatorData be a byte string with the following structure:
//
// Length (in bytes) Description Value
// -------------------------------------------------------------------------------------------------------------
// 32 SHA-256 hash of the rp.id. Initialized with rpIdHash bytes.
// 1 Flags Initialized with flags' value.
// 4 Signature counter (signCount). Initialized with signCount bytes.
// Variable Length Attested credential data. Initialized with attestedCredData’s value.
let mut hasher = Sha256::default();
hasher.update(request.relying_party.id.as_bytes());
let rp_id_hash = hasher.finalize().into();
let authenticator_data = AuthenticatorData {
rp_id_hash,
flags,
signature_count,
attested_credential: Some(attested_cred_data),
extensions: None,
raw: None,
};
// Let attestationStatement be a CBOR map (see "attStmtTemplate" in Generating an Attestation Object [WebAuthn])
// with the following keys, whose values are as follows:
// * Set "x5c" as an array of the one attestation cert extracted from CTAP1/U2F response.
// * Set "sig" to be the "signature" bytes from the U2F registration response message [U2FRawMsgs].
// Note: An ASN.1-encoded ECDSA signature value ranges over 8–72 bytes in length. [U2FRawMsgs] incorrectly
// states a different length range.
let attestation_statement = Ctap2AttestationStatement::FidoU2F(FidoU2fAttestationStmt {
signature: ByteBuf::from(self.signature.clone()),
certificates: vec![ByteBuf::from(self.attestation.clone())],
});
// Let attestationObject be a CBOR map (see "attObj" in Generating an Attestation Object [WebAuthn]) with the
// following keys, whose values are as follows:
// * Set "authData" to authenticatorData.
// * Set "fmt" to "fido-u2f".
// * Set "attStmt" to attestationStatement.
let resp = Ctap2MakeCredentialResponse {
format: String::from("fido-u2f"),
authenticator_data,
attestation_statement,
enterprise_attestation: None,
large_blob_key: None,
unsigned_extension_outputs: None,
};
Ok(resp.into_make_credential_output(request, None, None))
}
}
impl UpgradableResponse<GetAssertionResponse, SignRequest> for SignResponse {
fn try_upgrade(&self, request: &SignRequest) -> Result<GetAssertionResponse, Error> {
// Generate authenticatorData from the U2F authentication response message received from the authenticator:
// Copy bits 0 (the UP bit) and bit 1 from the CTAP2/U2F response user presence byte to bits 0 and 1 of the
// CTAP2 flags, respectively. Set all other bits of flags to zero. Note: bit zero is the least significant bit.
// See also Authenticator Data section of [WebAuthn].
// up always set
// bit 1 is unused, ignoring
let flags = AuthenticatorDataFlags::USER_PRESENT;
// Let signCount be a 4-byte unsigned integer initialized with CTAP1/U2F response counter field.
let signature_count = self.counter;
// Let authenticatorData is a byte string of following structure:
// Length (in bytes) Description Value
// -------------------------------------------------------------------------------------------------------------
// 32 SHA-256 hash of the rp.id. Initialized with rpIdHash bytes.
// 1 Flags Initialized with flags' value.
// 4 Signature counter (signCount) Initialized with signCount bytes.
let authenticator_data = AuthenticatorData {
rp_id_hash: request.app_id_hash.clone().try_into().map_err(|_| {
error!("app_id_hash has invalid length, expected 32 bytes");
Error::Platform(PlatformError::InvalidDeviceResponse)
})?,
flags,
signature_count,
attested_credential: None,
extensions: None,
raw: None,
};
// Let authenticatorGetAssertionResponse be a CBOR map with the following keys whose values are as follows: [..]
let response = Ctap2GetAssertionResponse {
credential_id: Some(Ctap2PublicKeyCredentialDescriptor {
r#type: Ctap2PublicKeyCredentialType::PublicKey,
id: ByteBuf::from(request.key_handle.clone()),
transports: None,
}),
authenticator_data,
signature: ByteBuf::from(self.signature.clone()),
user: None,
credentials_count: None,
user_selected: None,
large_blob_key: None,
unsigned_extension_outputs: None,
};
// This isn't great, but we have no access to the original request, and need to construct
// something like that here. In reality, we only need `extensions: None` currently.
let orig_request = GetAssertionRequest {
relying_party_id: String::new(), // We don't have access to that info here, but we don't need it either
challenge: Vec::new(), // U2F path doesn't use client_data for response serialization
origin: String::new(),
top_origin: None,
allow: vec![Ctap2PublicKeyCredentialDescriptor {
r#type: Ctap2PublicKeyCredentialType::PublicKey,
id: request.key_handle.clone().into(),
transports: None,
}],
extensions: None,
user_verification: if request.require_user_presence {
UserVerificationRequirement::Required
} else {
UserVerificationRequirement::Preferred
},
timeout: request.timeout,
};
let upgraded_response = [response.into_assertion_output(&orig_request, None)]
.as_slice()
.into();
trace!(?upgraded_response);
Ok(upgraded_response)
}
}