Skip to content
2 changes: 1 addition & 1 deletion libwebauthn/examples/webauthn_prf_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
let challenge: [u8; 32] = thread_rng().gen();

let extensions = MakeCredentialsRequestExtensions {
prf: Some(MakeCredentialPrfInput { _eval: None }),
prf: Some(MakeCredentialPrfInput { eval: None }),
..Default::default()
};

Expand Down
2 changes: 1 addition & 1 deletion libwebauthn/src/ops/u2f.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for Regis
enterprise_attestation: None,
large_blob_key: None,
};
Ok(resp.into_make_credential_output(request, None))
Ok(resp.into_make_credential_output(request, None, None))
}
}

Expand Down
8 changes: 6 additions & 2 deletions libwebauthn/src/ops/webauthn/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ use crate::{
use super::timeout::DEFAULT_TIMEOUT;
use super::{DowngradableRequest, RelyingPartyId, SignRequest, UserVerificationRequirement};

#[derive(Debug, Default, Clone, Serialize, PartialEq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct PRFValue {
#[serde(with = "serde_bytes")]
pub first: [u8; 32],
#[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes")]
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "serde_bytes"
)]
pub second: Option<[u8; 32]>,
}

Expand Down
61 changes: 52 additions & 9 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::time::Duration;

use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy;
use serde::{Deserialize, Serialize};
use serde_json::{self, Value as JsonValue};
use sha2::{Digest, Sha256};
use tracing::{debug, instrument, trace};

Expand Down Expand Up @@ -214,6 +213,7 @@ impl MakeCredentialsResponseUnsignedExtensions {
signed_extensions: &Option<Ctap2MakeCredentialsResponseExtensions>,
request: &MakeCredentialRequest,
info: Option<&Ctap2GetInfoResponse>,
auth_data: Option<&crate::transport::AuthTokenData>,
) -> MakeCredentialsResponseUnsignedExtensions {
let mut hmac_create_secret = None;
let mut prf = None;
Expand All @@ -225,8 +225,26 @@ impl MakeCredentialsResponseUnsignedExtensions {
hmac_create_secret = signed_extensions.hmac_secret;
}
if incoming_ext.prf.is_some() {
// Decrypt hmac-secret-mc output if available
let mc_results = signed_extensions.hmac_secret_mc.as_ref().and_then(|x| {
if let Some(auth_data) = auth_data {
let uv_proto = auth_data.protocol_version.create_protocol_object();
x.decrypt_output(&auth_data.shared_secret, &uv_proto)
} else {
None
}
});

let results = mc_results.map(|decrypted| {
super::PRFValue {
first: decrypted.output1,
second: decrypted.output2,
}
});

prf = Some(MakeCredentialPrfOutput {
enabled: signed_extensions.hmac_secret,
results,
});
}
}
Expand Down Expand Up @@ -444,19 +462,20 @@ impl WebAuthnIDL<MakeCredentialRequestParsingError> for MakeCredentialRequest {
type InnerModel = PublicKeyCredentialCreationOptionsJSON;
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct MakeCredentialPrfInput {
/// The `eval` field is parsed but not used during credential creation.
/// PRF evaluation only occurs during assertion (getAssertion), not registration.
/// We parse it here to accept valid WebAuthn JSON input without errors.
#[serde(rename = "eval")]
pub _eval: Option<JsonValue>,
/// PRF eval values for hmac-secret-mc (CTAP 2.2).
/// At MC time, only a single eval value is supported (no eval_by_credential).
#[serde(default)]
pub eval: Option<super::PRFValue>,
}

#[derive(Debug, Default, Clone, Serialize, PartialEq)]
pub struct MakeCredentialPrfOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub results: Option<super::PRFValue>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
Expand Down Expand Up @@ -765,17 +784,40 @@ mod tests {
let req_json = json_field_add(
REQUEST_BASE_JSON,
"extensions",
r#"{"prf": {"eval": {"first": "second"}}}"#,
r#"{"prf": {}}"#,
);

let req: MakeCredentialRequest =
MakeCredentialRequest::from_json(&rpid, &req_json).unwrap();
assert!(matches!(
req.extensions,
Some(MakeCredentialsRequestExtensions { prf: Some(_), .. })
Some(MakeCredentialsRequestExtensions {
prf: Some(MakeCredentialPrfInput { eval: None }),
..
})
));
}

#[test]
fn test_request_from_json_prf_extension_with_eval() {
let rpid = RelyingPartyId::try_from("example.org").unwrap();
// PRF eval values as JSON arrays of bytes (serde_bytes format)
let first_bytes = (1..=32)
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
let eval_json = format!(r#"{{"prf": {{"eval": {{"first": [{}]}}}}}}"#, first_bytes);
let req_json = json_field_add(REQUEST_BASE_JSON, "extensions", &eval_json);

let req: MakeCredentialRequest =
MakeCredentialRequest::from_json(&rpid, &req_json).unwrap();
let prf = req.extensions.unwrap().prf.unwrap();
assert!(prf.eval.is_some());
let eval = prf.eval.unwrap();
assert_eq!(eval.first, (1..=32).collect::<Vec<u8>>().as_slice());
assert!(eval.second.is_none());
}

#[test]
fn test_request_from_json_unknown_pub_key_cred_params() {
let rpid = RelyingPartyId::try_from("example.org").unwrap();
Expand Down Expand Up @@ -1023,6 +1065,7 @@ mod tests {
large_blob: None,
prf: Some(MakeCredentialPrfOutput {
enabled: Some(true),
results: None,
}),
};

Expand Down
52 changes: 5 additions & 47 deletions libwebauthn/src/proto/ctap2/model/get_assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ use crate::{
GetAssertionResponseUnsignedExtensions, HMACGetSecretInput, PRFValue,
},
pin::PinUvAuthProtocol,
proto::ctap2::cbor::Value,
proto::{
ctap2::cbor::Value,
extensions::prf::{prf_value_to_hmac_input, CalculatedHMACGetSecretInput},
},
transport::AuthTokenData,
webauthn::{Error, PlatformError},
};
Expand All @@ -17,11 +20,9 @@ use super::{
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialUserEntity,
Ctap2UserVerifiableRequest,
};
use cosey::PublicKey;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_indexed::{DeserializeIndexed, SerializeIndexed};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, HashMap};
use tracing::error;

Expand Down Expand Up @@ -321,57 +322,14 @@ impl Ctap2GetAssertionRequestExtensions {

// 5. If ev is not null:
if let Some(ev) = ev {
// SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || ev.first).
let mut prefix = String::from("WebAuthn PRF").into_bytes();
prefix.push(0x00);

let mut input = HMACGetSecretInput::default();
// 5.1 Let salt1 be the value of SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || ev.first).
let mut salt1_input = prefix.clone();
salt1_input.extend(ev.first);

let mut hasher = Sha256::default();
hasher.update(salt1_input);
let salt1_hash = hasher.finalize().to_vec();
input.salt1.copy_from_slice(&salt1_hash[..32]);

// 5.2 If ev.second is present, let salt2 be the value of SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || ev.second).
if let Some(second) = ev.second {
let mut salt2_input = prefix.clone();
salt2_input.extend(second);
let mut hasher = Sha256::default();
hasher.update(salt2_input);
let salt2_hash = hasher.finalize().to_vec();
let mut salt2 = [0u8; 32];
salt2.copy_from_slice(&salt2_hash[..32]);
input.salt2 = Some(salt2);
};

Ok(Some(input))
Ok(Some(prf_value_to_hmac_input(ev)))
} else {
// We don't have a usable PRF, so we don't do any HMAC
Ok(None)
}
}
}

#[derive(Debug, Clone, SerializeIndexed)]
pub struct CalculatedHMACGetSecretInput {
// keyAgreement(0x01): public key of platform key-agreement key.
#[serde(index = 0x01)]
pub public_key: PublicKey,
// saltEnc(0x02): Encryption of the one or two salts
#[serde(index = 0x02)]
pub salt_enc: ByteBuf,
// saltAuth(0x03): authenticate(shared secret, saltEnc)
#[serde(index = 0x03)]
pub salt_auth: ByteBuf,
// pinUvAuthProtocol(0x04): (optional) as selected when getting the shared secret. CTAP2.1 platforms MUST include this parameter if the value of pinUvAuthProtocol is not 1.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(index = 0x04)]
pub pin_auth_proto: Option<u32>,
}

#[derive(Debug, Clone, DeserializeIndexed)]
pub struct Ctap2GetAssertionResponse {
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
Loading
Loading