Skip to content

Commit b4616ab

Browse files
committed
Separate signed/unsigned extensions for GetAssertion
1 parent c782d8b commit b4616ab

File tree

6 files changed

+99
-80
lines changed

6 files changed

+99
-80
lines changed

libwebauthn/examples/prf_test.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use tokio::sync::mpsc::Receiver;
1313
use tracing_subscriber::{self, EnvFilter};
1414

1515
use libwebauthn::ops::webauthn::{
16-
GetAssertionHmacOrPrfInput, GetAssertionHmacOrPrfOutput, GetAssertionRequest,
17-
GetAssertionRequestExtensions, PRFValue, UserVerificationRequirement,
16+
GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, PRFValue,
17+
UserVerificationRequirement,
1818
};
1919
use libwebauthn::pin::PinRequestReason;
2020
use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType};
@@ -178,15 +178,15 @@ async fn run_success_test(
178178
println!(
179179
"{num}. result of {printoutput}: {:?}",
180180
assertion
181-
.authenticator_data
182-
.extensions
181+
.unsigned_extensions_output
183182
.as_ref()
184-
.map(|e| match &e.hmac_or_prf {
185-
GetAssertionHmacOrPrfOutput::None => String::from("ERROR: No PRF output"),
186-
GetAssertionHmacOrPrfOutput::HmacGetSecret(..) =>
187-
String::from("ERROR: Got HMAC instead of PRF output"),
188-
GetAssertionHmacOrPrfOutput::Prf { enabled: _, result } =>
189-
hex::encode(result.first),
183+
.map(|e| if let Some(prf) = &e.prf {
184+
let results = prf.results.as_ref().map(|r| hex::encode(r.first)).unwrap();
185+
format!("Found PRF results: {}", results)
186+
} else if e.hmac_get_secret.is_some() {
187+
String::from("ERROR: Got HMAC instead of PRF output")
188+
} else {
189+
String::from("ERROR: No PRF output")
190190
})
191191
.unwrap_or(String::from("ERROR: No extensions returned"))
192192
);

libwebauthn/src/ops/u2f.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ impl UpgradableResponse<GetAssertionResponse, SignRequest> for SignResponse {
194194
credentials_count: None,
195195
user_selected: None,
196196
large_blob_key: None,
197-
unsigned_extension_outputs: None,
198197
enterprise_attestation: None,
199198
attestation_statement: None,
200199
};

libwebauthn/src/ops/webauthn.rs

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
use std::{
2-
collections::{BTreeMap, HashMap},
3-
time::Duration,
4-
};
1+
use std::{collections::HashMap, time::Duration};
52

63
use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy;
74
use serde::{Deserialize, Serialize};
8-
use serde_cbor::Value;
95
use sha2::{Digest, Sha256};
106
use tracing::{debug, error, instrument, trace};
117

@@ -16,8 +12,9 @@ use crate::{
1612
ctap1::{Ctap1RegisteredKey, Ctap1Version},
1713
ctap2::{
1814
Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType,
19-
Ctap2MakeCredentialsResponseExtensions, Ctap2PublicKeyCredentialDescriptor,
20-
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity,
15+
Ctap2GetAssertionResponseExtensions, Ctap2MakeCredentialsResponseExtensions,
16+
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
17+
Ctap2PublicKeyCredentialUserEntity,
2118
},
2219
},
2320
webauthn::CtapError,
@@ -97,7 +94,9 @@ pub struct MakeCredentialRequest {
9794

9895
#[derive(Debug, Default, Clone, Serialize)]
9996
pub struct PRFValue {
97+
#[serde(with = "serde_bytes")]
10098
pub first: [u8; 32],
99+
#[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes")]
101100
pub second: Option<[u8; 32]>,
102101
}
103102

@@ -243,17 +242,10 @@ pub enum GetAssertionHmacOrPrfInput {
243242
},
244243
}
245244

246-
#[derive(Debug, Default, Clone)]
247-
pub enum GetAssertionHmacOrPrfOutput {
248-
#[default]
249-
None,
250-
HmacGetSecret(HMACGetSecretOutput),
251-
Prf {
252-
enabled: bool,
253-
// The spec tells us this should be a Vec<PRFValue>, but doesn't
254-
// explain how it could hold more than 1 value
255-
result: PRFValue,
256-
},
245+
#[derive(Debug, Default, Clone, Serialize)]
246+
pub struct GetAssertionPrfOutput {
247+
#[serde(skip_serializing_if = "Option::is_none")]
248+
pub results: Option<PRFValue>,
257249
}
258250

259251
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -271,6 +263,15 @@ pub enum GetAssertionLargeBlobExtension {
271263
// Write(Vec<u8>),
272264
}
273265

266+
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
267+
pub struct GetAssertionLargeBlobExtensionOutput {
268+
#[serde(skip_serializing_if = "Option::is_none")]
269+
pub blob: Option<Vec<u8>>,
270+
// Not yet supported
271+
// #[serde(skip_serializing_if = "Option::is_none")]
272+
// pub written: Option<bool>,
273+
}
274+
274275
#[derive(Debug, Default, Clone)]
275276
pub struct GetAssertionRequestExtensions {
276277
pub cred_blob: Option<bool>,
@@ -286,7 +287,7 @@ pub struct HMACGetSecretOutput {
286287
pub output2: Option<[u8; 32]>,
287288
}
288289

289-
#[derive(Clone, Debug, Default, Deserialize)]
290+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
290291
#[serde(transparent)]
291292
pub struct Ctap2HMACGetSecretOutput {
292293
// We get this from the device, but have to decrypt it, and
@@ -297,7 +298,7 @@ pub struct Ctap2HMACGetSecretOutput {
297298

298299
impl Ctap2HMACGetSecretOutput {
299300
pub(crate) fn decrypt_output(
300-
self,
301+
&self,
301302
shared_secret: &[u8],
302303
uv_proto: &Box<dyn PinUvAuthProtocol>,
303304
) -> Option<HMACGetSecretOutput> {
@@ -313,7 +314,7 @@ impl Ctap2HMACGetSecretOutput {
313314
res.output1.copy_from_slice(&output);
314315
} else if output.len() == 64 {
315316
let (o1, o2) = output.split_at(32);
316-
res.output1.copy_from_slice(&o1);
317+
res.output1.copy_from_slice(o1);
317318
res.output2 = Some(o2.try_into().unwrap());
318319
} else {
319320
error!("Failed to split HMAC Secret outputs. Unexpected output length: {}. Skipping HMAC extension", output.len());
@@ -324,11 +325,17 @@ impl Ctap2HMACGetSecretOutput {
324325
}
325326
}
326327

327-
#[derive(Debug, Default, Clone)]
328-
pub struct GetAssertionResponseExtensions {
329-
// Stored credBlob
330-
pub cred_blob: Option<Vec<u8>>,
331-
pub hmac_or_prf: GetAssertionHmacOrPrfOutput,
328+
pub type GetAssertionResponseExtensions = Ctap2GetAssertionResponseExtensions;
329+
330+
#[derive(Debug, Default, Clone, Serialize)]
331+
#[serde(rename_all = "camelCase")]
332+
pub struct GetAssertionResponseUnsignedExtensions {
333+
#[serde(skip_serializing_if = "Option::is_none")]
334+
pub hmac_get_secret: Option<HMACGetSecretOutput>,
335+
#[serde(skip_serializing_if = "Option::is_none")]
336+
pub large_blob: Option<GetAssertionLargeBlobExtensionOutput>,
337+
#[serde(skip_serializing_if = "Option::is_none")]
338+
pub prf: Option<GetAssertionPrfOutput>,
332339
}
333340

334341
#[derive(Debug, Clone)]
@@ -345,7 +352,7 @@ pub struct Assertion {
345352
pub credentials_count: Option<u32>,
346353
pub user_selected: Option<bool>,
347354
pub large_blob_key: Option<Vec<u8>>,
348-
pub unsigned_extension_outputs: Option<BTreeMap<Value, Value>>,
355+
pub unsigned_extensions_output: Option<GetAssertionResponseUnsignedExtensions>,
349356
pub enterprise_attestation: Option<bool>,
350357
pub attestation_statement: Option<Ctap2AttestationStatement>,
351358
}

libwebauthn/src/proto/ctap2/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ pub use model::{
2525
Ctap2CredentialData, Ctap2CredentialManagementMetadata, Ctap2CredentialManagementRequest,
2626
Ctap2CredentialManagementResponse, Ctap2RPData,
2727
};
28-
pub use model::{Ctap2GetAssertionRequest, Ctap2GetAssertionResponse};
28+
pub use model::{
29+
Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions,
30+
};
2931
pub use model::{
3032
Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, Ctap2MakeCredentialsResponseExtensions,
3133
};

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub use make_credential::{
3030
mod get_assertion;
3131
pub use get_assertion::{
3232
Ctap2AttestationStatement, Ctap2GetAssertionOptions, Ctap2GetAssertionRequest,
33-
Ctap2GetAssertionResponse, FidoU2fAttestationStmt,
33+
Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, FidoU2fAttestationStmt,
3434
};
3535
mod credential_management;
3636
pub use credential_management::{

libwebauthn/src/proto/ctap2/model/get_assertion.rs

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use crate::{
22
fido::AuthenticatorData,
33
ops::webauthn::{
44
Assertion, Ctap2HMACGetSecretOutput, GetAssertionHmacOrPrfInput,
5-
GetAssertionHmacOrPrfOutput, GetAssertionLargeBlobExtension, GetAssertionRequest,
6-
GetAssertionRequestExtensions, GetAssertionResponseExtensions, HMACGetSecretInput,
7-
PRFValue,
5+
GetAssertionLargeBlobExtension, GetAssertionLargeBlobExtensionOutput,
6+
GetAssertionPrfOutput, GetAssertionRequest, GetAssertionRequestExtensions,
7+
GetAssertionResponseUnsignedExtensions, HMACGetSecretInput, PRFValue,
88
},
99
pin::PinUvAuthProtocol,
1010
transport::AuthTokenData,
@@ -383,9 +383,6 @@ pub struct Ctap2GetAssertionResponse {
383383
#[serde(skip_serializing_if = "Option::is_none")]
384384
pub large_blob_key: Option<ByteBuf>,
385385

386-
#[serde(skip_serializing_if = "Option::is_none")]
387-
pub unsigned_extension_outputs: Option<BTreeMap<Value, Value>>,
388-
389386
#[serde(skip_serializing_if = "Option::is_none")]
390387
pub enterprise_attestation: Option<bool>,
391388

@@ -438,32 +435,27 @@ impl Ctap2GetAssertionResponse {
438435
request: &GetAssertionRequest,
439436
auth_data: Option<&AuthTokenData>,
440437
) -> Assertion {
441-
let authenticator_data = AuthenticatorData::<GetAssertionResponseExtensions> {
442-
rp_id_hash: self.authenticator_data.rp_id_hash,
443-
flags: self.authenticator_data.flags,
444-
signature_count: self.authenticator_data.signature_count,
445-
attested_credential: self.authenticator_data.attested_credential,
446-
extensions: self
447-
.authenticator_data
448-
.extensions
449-
.map(|x| x.into_output(request, auth_data)),
450-
};
438+
let unsigned_extensions_output = self
439+
.authenticator_data
440+
.extensions
441+
.as_ref()
442+
.map(|x| x.to_unsigned_extensions(request, &self, auth_data));
451443
Assertion {
452444
credential_id: self.credential_id,
453-
authenticator_data,
445+
authenticator_data: self.authenticator_data,
454446
signature: self.signature.into_vec(),
455447
user: self.user,
456448
credentials_count: self.credentials_count,
457449
user_selected: self.user_selected,
458450
large_blob_key: self.large_blob_key.map(ByteBuf::into_vec),
459-
unsigned_extension_outputs: self.unsigned_extension_outputs,
451+
unsigned_extensions_output,
460452
enterprise_attestation: self.enterprise_attestation,
461453
attestation_statement: self.attestation_statement,
462454
}
463455
}
464456
}
465457

466-
#[derive(Debug, Default, Clone, Deserialize)]
458+
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
467459
#[serde(rename_all = "camelCase")]
468460
pub struct Ctap2GetAssertionResponseExtensions {
469461
// Stored credBlob
@@ -480,14 +472,15 @@ pub struct Ctap2GetAssertionResponseExtensions {
480472
}
481473

482474
impl Ctap2GetAssertionResponseExtensions {
483-
pub(crate) fn into_output(
484-
self,
475+
pub(crate) fn to_unsigned_extensions(
476+
&self,
485477
request: &GetAssertionRequest,
478+
response: &Ctap2GetAssertionResponse,
486479
auth_data: Option<&AuthTokenData>,
487-
) -> GetAssertionResponseExtensions {
488-
let hmac_or_prf = if let Some(orig_ext) = &request.extensions {
480+
) -> GetAssertionResponseUnsignedExtensions {
481+
let (hmac_get_secret, prf) = if let Some(orig_ext) = &request.extensions {
489482
// Decrypt the raw HMAC extension
490-
let decrypted_hmac = self.hmac_secret.and_then(|x| {
483+
let decrypted_hmac = self.hmac_secret.as_ref().and_then(|x| {
491484
if let Some(auth_data) = auth_data {
492485
let uv_proto = auth_data.protocol_version.create_protocol_object();
493486
x.decrypt_output(&auth_data.shared_secret, &uv_proto)
@@ -498,28 +491,46 @@ impl Ctap2GetAssertionResponseExtensions {
498491
if let Some(decrypted) = decrypted_hmac {
499492
// Repackaging it into output
500493
match &orig_ext.hmac_or_prf {
501-
GetAssertionHmacOrPrfInput::None => GetAssertionHmacOrPrfOutput::None,
502-
GetAssertionHmacOrPrfInput::HmacGetSecret(..) => {
503-
GetAssertionHmacOrPrfOutput::HmacGetSecret(decrypted)
504-
}
505-
GetAssertionHmacOrPrfInput::Prf { .. } => GetAssertionHmacOrPrfOutput::Prf {
506-
enabled: true,
507-
result: PRFValue {
508-
first: decrypted.output1,
509-
second: decrypted.output2,
510-
},
511-
},
494+
GetAssertionHmacOrPrfInput::None => (None, None),
495+
GetAssertionHmacOrPrfInput::HmacGetSecret(..) => (Some(decrypted), None),
496+
GetAssertionHmacOrPrfInput::Prf { .. } => (
497+
None,
498+
Some(GetAssertionPrfOutput {
499+
results: Some(PRFValue {
500+
first: decrypted.output1,
501+
second: decrypted.output2,
502+
}),
503+
}),
504+
),
512505
}
513506
} else {
514-
GetAssertionHmacOrPrfOutput::None
507+
(None, None)
515508
}
516509
} else {
517-
GetAssertionHmacOrPrfOutput::None
510+
(None, None)
518511
};
519512

520-
GetAssertionResponseExtensions {
521-
cred_blob: self.cred_blob,
522-
hmac_or_prf,
513+
// LargeBlobs was requested
514+
let large_blob = request
515+
.extensions
516+
.as_ref()
517+
.filter(|x| x.large_blob != GetAssertionLargeBlobExtension::None)
518+
.map(|_| {
519+
Some(GetAssertionLargeBlobExtensionOutput {
520+
blob: response
521+
.large_blob_key
522+
.as_ref()
523+
.map(|x| x.clone().into_vec()),
524+
// Not yet supported
525+
// written: None,
526+
})
527+
})
528+
.unwrap_or_default();
529+
530+
GetAssertionResponseUnsignedExtensions {
531+
hmac_get_secret,
532+
large_blob,
533+
prf,
523534
}
524535
}
525536
}

0 commit comments

Comments
 (0)