Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ path = "examples/features/webauthn_preflight_hid.rs"
name = "webauthn_prf_hid"
path = "examples/features/webauthn_prf_hid.rs"

[[example]]
name = "webauthn_prf_cable"
path = "examples/features/webauthn_prf_cable.rs"

[[example]]
name = "webauthn_related_origins_hid"
path = "examples/features/webauthn_related_origins_hid.rs"
Expand Down
199 changes: 199 additions & 0 deletions libwebauthn/examples/features/webauthn_prf_cable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! PRF extension over caBLE/hybrid, against a phone authenticator that
//! advertises the `prf` extension in getInfo.
//!
//! cargo run --example webauthn_prf_cable -- create
//! cargo run --example webauthn_prf_cable -- get [credential-id]
//!
//! `create` registers a discoverable credential with PRF enabled and an eval
//! at creation, then prints the credential ID. `get` asserts with PRF eval
//! salts. When a credential ID is given, it is added to the allow list with an
//! evalByCredential entry.

use std::collections::HashMap;
use std::error::Error;
use std::time::Duration;

use libwebauthn::ops::webauthn::{
GetAssertionRequest, GetAssertionRequestExtensions, JsonFormat, MakeCredentialPrfInput,
MakeCredentialRequest, MakeCredentialsRequestExtensions, PrfInput, PrfInputValue,
ResidentKeyRequirement, UserVerificationRequirement, WebAuthnIDLResponse as _,
};
use libwebauthn::proto::ctap2::{
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
Ctap2PublicKeyCredentialType, Ctap2PublicKeyCredentialUserEntity,
};
use libwebauthn::transport::cable::channel::CableChannel;
use libwebauthn::transport::cable::is_available;
use libwebauthn::transport::cable::qr_code_device::{
CableQrCodeDevice, CableTransports, QrCodeOperationHint,
};
use libwebauthn::transport::{Channel as _, ChannelSettings, Device};
use libwebauthn::webauthn::WebAuthn;
use qrcode::render::unicode;
use qrcode::QrCode;
use serde_bytes::ByteBuf;

#[path = "../common/mod.rs"]
mod common;

const TIMEOUT: Duration = Duration::from_secs(120);
const RP_ID: &str = "example.org";
const ORIGIN: &str = "https://example.org";

// Deterministic PRF inputs, so results can be cross-checked against another
// client holding the same credential.
const CREATE_EVAL_FIRST: &[u8] = b"example-prf-create-first";
const CREATE_EVAL_SECOND: &[u8] = b"example-prf-create-second";
const GET_EVAL_FIRST: &[u8] = b"example-prf-get-first";
const GET_EVAL_SECOND: &[u8] = b"example-prf-get-second";
const BY_CRED_FIRST: &[u8] = b"example-prf-bycred-first";
const BY_CRED_SECOND: &[u8] = b"example-prf-bycred-second";

#[tokio::main]
pub async fn main() -> Result<(), Box<dyn Error>> {
common::setup_logging();

let args: Vec<String> = std::env::args().collect();
let mode = args.get(1).map(String::as_str);

if !is_available().await {
eprintln!("No Bluetooth adapter found. Cable/Hybrid transport is unavailable.");
return Err("Cable transport not available".into());
}

match mode {
Some("create") => create().await,
Some("get") => get(args.get(2).map(String::as_str)).await,
_ => {
eprintln!("Usage: webauthn_prf_cable <create | get [credential-id-base64url]>");
Err("missing or unknown subcommand".into())
}
}
}

async fn connect(
hint: QrCodeOperationHint,
) -> Result<(CableQrCodeDevice, CableChannel), Box<dyn Error>> {
let mut device: CableQrCodeDevice =
CableQrCodeDevice::new_transient(hint, CableTransports::CloudAssistedOrLocal)?;

println!("Created QR code, awaiting advertisement.");
let qr_code = QrCode::new(device.qr_code.to_string()).unwrap();
let image = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Light)
.light_color(unicode::Dense1x2::Dark)
.build();
println!("{}", image);

let channel = device.channel(ChannelSettings::default()).await?;
println!("Channel established {:?}", channel);

let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_cable_updates(state_recv));
Ok((device, channel))
}

async fn create() -> Result<(), Box<dyn Error>> {
let (_device, mut channel) = connect(QrCodeOperationHint::MakeCredential).await?;

let extensions = MakeCredentialsRequestExtensions {
prf: Some(MakeCredentialPrfInput {
eval: Some(PrfInputValue {
first: CREATE_EVAL_FIRST.to_vec(),
second: Some(CREATE_EVAL_SECOND.to_vec()),
}),
}),
..Default::default()
};

let request = MakeCredentialRequest {
challenge: vec![0x11; 32],
origin: ORIGIN.to_owned(),
top_origin: None,
relying_party: Ctap2PublicKeyCredentialRpEntity::new(RP_ID, "Example Relying Party"),
user: Ctap2PublicKeyCredentialUserEntity::new(&[0x42; 16], "alice", "Alice"),
resident_key: Some(ResidentKeyRequirement::Required),
user_verification: UserVerificationRequirement::Preferred,
algorithms: vec![Ctap2CredentialType::default()],
exclude: None,
extensions: Some(extensions),
timeout: TIMEOUT,
};

let response = retry_user_errors!(channel.webauthn_make_credential(&request)).unwrap();

let response_json = response
.to_json_string(&request, JsonFormat::Prettified)
.expect("Failed to serialize MakeCredential response");
println!("WebAuthn MakeCredential response (JSON):\n{response_json}");

let credential: Ctap2PublicKeyCredentialDescriptor =
(&response.authenticator_data).try_into().unwrap();
println!(
"\nCredential ID (base64url): {}",
base64_url::encode(&credential.id)
);
println!("Next: run the `get` leg, e.g.");
println!(
"cargo run --example webauthn_prf_cable -- get {}",
base64_url::encode(&credential.id)
);
Ok(())
}

async fn get(credential_id: Option<&str>) -> Result<(), Box<dyn Error>> {
let (_device, mut channel) = connect(QrCodeOperationHint::GetAssertionRequest).await?;

let mut eval_by_credential = HashMap::new();
let allow = match credential_id {
Some(encoded) => {
eval_by_credential.insert(
encoded.to_owned(),
PrfInputValue {
first: BY_CRED_FIRST.to_vec(),
second: Some(BY_CRED_SECOND.to_vec()),
},
);
vec![Ctap2PublicKeyCredentialDescriptor {
r#type: Ctap2PublicKeyCredentialType::PublicKey,
id: ByteBuf::from(
base64_url::decode(encoded)
.map_err(|e| format!("invalid credential id: {e}"))?,
),
transports: None,
}]
}
None => vec![],
};

let request = GetAssertionRequest {
relying_party_id: RP_ID.to_owned(),
challenge: vec![0x22; 32],
origin: ORIGIN.to_owned(),
top_origin: None,
allow,
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
prf: Some(PrfInput {
eval: Some(PrfInputValue {
first: GET_EVAL_FIRST.to_vec(),
second: Some(GET_EVAL_SECOND.to_vec()),
}),
eval_by_credential,
}),
..Default::default()
}),
timeout: TIMEOUT,
};

let response = retry_user_errors!(channel.webauthn_get_assertion(&request)).unwrap();

for (num, assertion) in response.assertions.iter().enumerate() {
let assertion_json = assertion
.to_json_string(&request, JsonFormat::Prettified)
.expect("Failed to serialize GetAssertion response");
println!("Assertion {num} (JSON):\n{assertion_json}");
}
Ok(())
}
1 change: 1 addition & 0 deletions libwebauthn/src/ops/u2f.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for Regis
attestation_statement,
enterprise_attestation: None,
large_blob_key: None,
unsigned_extension_outputs: None,
};
Ok(resp.into_make_credential_output(request, None, None))
}
Expand Down
89 changes: 63 additions & 26 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::time::Duration;

use async_trait::async_trait;
Expand Down Expand Up @@ -25,10 +26,11 @@ use crate::{
proto::{
ctap1::{Ctap1RegisteredKey, Ctap1Version},
ctap2::{
cbor, cose, Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier,
Ctap2CredentialType, Ctap2GetInfoResponse, Ctap2MakeCredentialsResponseExtensions,
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
Ctap2PublicKeyCredentialUserEntity,
cbor, cbor::Value, cose, parse_unsigned_prf, Ctap2AttestationStatement,
Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType, Ctap2GetInfoResponse,
Ctap2MakeCredentialsResponseExtensions, Ctap2PublicKeyCredentialDescriptor,
Ctap2PublicKeyCredentialRpEntity, Ctap2PublicKeyCredentialUserEntity,
UnsignedPrfOutput,
},
},
transport::AuthTokenData,
Expand Down Expand Up @@ -203,35 +205,44 @@ impl MakeCredentialsResponseUnsignedExtensions {

pub fn from_signed_extensions(
signed_extensions: &Option<Ctap2MakeCredentialsResponseExtensions>,
unsigned_outputs: Option<&BTreeMap<Value, Value>>,
request: &MakeCredentialRequest,
info: Option<&Ctap2GetInfoResponse>,
auth_data: Option<&AuthTokenData>,
) -> MakeCredentialsResponseUnsignedExtensions {
let mut hmac_create_secret = None;
let mut prf = None;
if let Some(signed_extensions) = signed_extensions {
if let Some(incoming_ext) = &request.extensions {
if incoming_ext.hmac_create_secret.is_some() {
hmac_create_secret = signed_extensions.hmac_secret;
}
if incoming_ext.prf.is_some() {
let results = signed_extensions
.hmac_secret_mc
.as_ref()
.zip(auth_data)
.and_then(|(out, auth)| {
let uv_proto = auth.protocol_version.create_protocol_object();
out.decrypt_output(&auth.shared_secret, uv_proto.as_ref())
})
.map(|decrypted| PrfOutputValue {
first: decrypted.output1,
second: decrypted.output2,
});
prf = Some(MakeCredentialPrfOutput {
enabled: signed_extensions.hmac_secret,
results,
// Native `prf` outputs arrive in unsignedExtensionOutputs, not authData.
let unsigned_prf = unsigned_outputs.and_then(parse_unsigned_prf);
if let Some(incoming_ext) = &request.extensions {
if incoming_ext.hmac_create_secret.is_some() {
hmac_create_secret = signed_extensions.as_ref().and_then(|s| s.hmac_secret);
}
if incoming_ext.prf.is_some() && (signed_extensions.is_some() || unsigned_prf.is_some())
{
let decrypted_results = signed_extensions
.as_ref()
.and_then(|s| s.hmac_secret_mc.as_ref())
.zip(auth_data)
.and_then(|(out, auth)| {
let uv_proto = auth.protocol_version.create_protocol_object();
out.decrypt_output(&auth.shared_secret, uv_proto.as_ref())
})
.map(|decrypted| PrfOutputValue {
first: decrypted.output1,
second: decrypted.output2,
});
}
let UnsignedPrfOutput {
enabled: unsigned_enabled,
results: unsigned_results,
} = unsigned_prf.unwrap_or_default();
prf = Some(MakeCredentialPrfOutput {
enabled: signed_extensions
.as_ref()
.and_then(|s| s.hmac_secret)
.or(unsigned_enabled),
results: decrypted_results.or(unsigned_results),
});
}
}

Expand Down Expand Up @@ -1448,6 +1459,32 @@ mod tests {
);
}

#[test]
fn prf_output_serialized_into_client_extension_results() {
let mut response = create_test_response();
response.unsigned_extensions_output = MakeCredentialsResponseUnsignedExtensions {
prf: Some(MakeCredentialPrfOutput {
enabled: Some(true),
results: Some(PrfOutputValue {
first: [0xAB; 32],
second: Some([0xCD; 32]),
}),
}),
..Default::default()
};

let results = serde_json::to_value(response.build_client_extension_results()).unwrap();
assert_eq!(results["prf"]["enabled"], serde_json::json!(true));
assert_eq!(
results["prf"]["results"]["first"],
serde_json::json!(base64_url::encode(&[0xAB; 32]))
);
assert_eq!(
results["prf"]["results"]["second"],
serde_json::json!(base64_url::encode(&[0xCD; 32]))
);
}

#[test]
fn test_response_to_idl_model() {
let response = create_test_response();
Expand Down
5 changes: 4 additions & 1 deletion libwebauthn/src/proto/ctap2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub use model::{
Ctap2UserVerificationOperation, FidoU2fAttestationStmt,
};

pub(crate) use model::{parse_unsigned_prf, UnsignedPrfOutput};
pub use model::{
Ctap2AuthenticatorConfigCommand, Ctap2AuthenticatorConfigParams,
Ctap2AuthenticatorConfigRequest,
Expand All @@ -34,10 +35,12 @@ pub use model::{
};
pub use model::{
Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions,
Ctap2PrfGetAssertionInput, Ctap2PrfSalts,
};
pub use model::{Ctap2LargeBlobsRequest, Ctap2LargeBlobsResponse};
pub use model::{
Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, Ctap2MakeCredentialsResponseExtensions,
Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse,
Ctap2MakeCredentialsResponseExtensions, Ctap2PrfMakeCredentialInput,
};
pub mod preflight;
pub use protocol::Ctap2;
6 changes: 4 additions & 2 deletions libwebauthn/src/proto/ctap2/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ pub use client_pin::{
mod make_credential;
pub use make_credential::{
Ctap2MakeCredentialOptions, Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse,
Ctap2MakeCredentialsResponseExtensions,
Ctap2MakeCredentialsResponseExtensions, Ctap2PrfMakeCredentialInput,
};
mod get_assertion;
pub(crate) use get_assertion::{parse_unsigned_prf, UnsignedPrfOutput};
pub use get_assertion::{
Ctap2AttestationStatement, Ctap2GetAssertionOptions, Ctap2GetAssertionRequest,
Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, FidoU2fAttestationStmt,
Ctap2GetAssertionResponse, Ctap2GetAssertionResponseExtensions, Ctap2PrfGetAssertionInput,
Ctap2PrfSalts, FidoU2fAttestationStmt,
};
mod credential_management;
pub use credential_management::{
Expand Down
Loading
Loading