Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1014412
[WIP] Web IDL support (make credentials); next: extension parsing
AlfioEmanueleFresta Jul 27, 2025
b86385d
[WIP] More progress
AlfioEmanueleFresta Jul 28, 2025
4e3ee42
Make Credential parsing seems working; added example
AlfioEmanueleFresta Aug 3, 2025
80ba702
Clean up warnings
AlfioEmanueleFresta Aug 8, 2025
c094136
GetAssertion IDL implementation
AlfioEmanueleFresta Aug 15, 2025
560cf98
Change back CTAP2 credential model to ByteBuf
AlfioEmanueleFresta Aug 24, 2025
56f3436
Fixes and test for MC
AlfioEmanueleFresta Aug 24, 2025
e12fe3f
Minor fix to test
AlfioEmanueleFresta Sep 7, 2025
294eed6
Rebase: Fix use of HmacOrPrf::None variant
AlfioEmanueleFresta Sep 7, 2025
fa44ab8
Adds basic parsing tests
AlfioEmanueleFresta Sep 7, 2025
9fc6f61
Add tests for get assertion
AlfioEmanueleFresta Sep 7, 2025
e948e2c
Update example to use JSON
AlfioEmanueleFresta Sep 7, 2025
437c6af
Address PR comments (AI assisted)
AlfioEmanueleFresta Dec 18, 2025
39cca52
Rebased
AlfioEmanueleFresta Dec 26, 2025
7933f7c
Fix: hmac_get_secret typo
AlfioEmanueleFresta Jan 24, 2026
acf8b7b
Feedback: From rather than Into
AlfioEmanueleFresta Jan 24, 2026
509a22f
Feedback: use rename_all=camelCase
AlfioEmanueleFresta Jan 24, 2026
93f6b3b
Feedback: Other minor fixes
AlfioEmanueleFresta Jan 24, 2026
f39c9ca
Feedback: Other minor fixex, simplfiied parsing chain
AlfioEmanueleFresta Jan 24, 2026
3da4043
Feedback: More rename_all
AlfioEmanueleFresta Jan 24, 2026
69794ff
Feedback: Fix UV field naming for JSON IDL conformance
AlfioEmanueleFresta Jan 24, 2026
9c51e1a
Feedback: Fix build.
AlfioEmanueleFresta Jan 24, 2026
66fefe5
Merge main into json
iinuwa Jan 26, 2026
8b08c6c
Pin nfc1 package to 0.6.0
iinuwa Jan 26, 2026
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
776 changes: 333 additions & 443 deletions libwebauthn/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ snow = { version = "0.10", features = ["use-p256"] }
ctap-types = { version = "0.4.0" }
btleplug = "0.11.7"
thiserror = "2.0.12"
serde_json = "1.0.141"
apdu-core = { version = "0.4.0", optional = true }
apdu = { version = "0.4.0", optional = true }
pcsc = { version = "2.9.0", optional = true }
Expand Down
9 changes: 5 additions & 4 deletions libwebauthn/examples/prf_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionHmacOrPrfInput, GetAssertionRequest, GetAssertionRequestExtensions, PRFValue,
UserVerificationRequirement,
PrfInput, UserVerificationRequirement,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::proto::ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType};
Expand Down Expand Up @@ -126,10 +126,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
});

let eval_by_credential = HashMap::new();
let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf {
let hmac_or_prf = GetAssertionHmacOrPrfInput::Prf(PrfInput {
eval,
eval_by_credential,
};
});

run_success_test(
&mut channel,
&credential,
Expand All @@ -155,7 +156,7 @@ async fn run_success_test(
allow: vec![credential.clone()],
user_verification: UserVerificationRequirement::Preferred,
extensions: Some(GetAssertionRequestExtensions {
hmac_or_prf,
hmac_or_prf: Some(hmac_or_prf),
..Default::default()
}),
timeout: TIMEOUT,
Expand Down
5 changes: 3 additions & 2 deletions libwebauthn/examples/webauthn_cable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use tokio::time::sleep;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement,
GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest,
ResidentKeyRequirement, UserVerificationRequirement,
};
use libwebauthn::proto::ctap2::{
Ctap2CredentialType, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
Expand Down Expand Up @@ -161,7 +162,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
hash: Vec::from(challenge),
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: None,
extensions: Some(GetAssertionRequestExtensions::default()),
timeout: TIMEOUT,
};

Expand Down
22 changes: 12 additions & 10 deletions libwebauthn/examples/webauthn_extensions_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
CredentialProtectionExtension, CredentialProtectionPolicy, GetAssertionHmacOrPrfInput,
GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput,
MakeCredentialHmacOrPrfInput, MakeCredentialLargeBlobExtension, MakeCredentialRequest,
GetAssertionRequest, GetAssertionRequestExtensions, HMACGetSecretInput, MakeCredentialRequest,
MakeCredentialsRequestExtensions, ResidentKeyRequirement, UserVerificationRequirement,
};
use libwebauthn::pin::PinRequestReason;
Expand Down Expand Up @@ -88,10 +87,11 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
policy: CredentialProtectionPolicy::UserVerificationRequired,
enforce_policy: true,
}),
cred_blob: Some(r"My own little blob".into()),
large_blob: MakeCredentialLargeBlobExtension::None,
cred_blob: Some("My own little blob".as_bytes().into()),
large_blob: None,
min_pin_length: Some(true),
hmac_or_prf: MakeCredentialHmacOrPrfInput::HmacGetSecret,
hmac_create_secret: Some(true),
prf: None,
cred_props: Some(true),
};

Expand Down Expand Up @@ -148,11 +148,13 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: Some(GetAssertionRequestExtensions {
cred_blob: Some(true),
hmac_or_prf: GetAssertionHmacOrPrfInput::HmacGetSecret(HMACGetSecretInput {
salt1: [1; 32],
salt2: None,
}),
cred_blob: true,
hmac_or_prf: Some(GetAssertionHmacOrPrfInput::HmacGetSecret(
HMACGetSecretInput {
salt1: [1; 32],
salt2: None,
},
)),
..Default::default()
}),
timeout: TIMEOUT,
Expand Down
5 changes: 3 additions & 2 deletions libwebauthn/examples/webauthn_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use tokio::sync::broadcast::Receiver;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement,
GetAssertionRequest, GetAssertionRequestExtensions, MakeCredentialRequest,
ResidentKeyRequirement, UserVerificationRequirement,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::proto::ctap2::{
Expand Down Expand Up @@ -128,7 +129,7 @@ pub async fn main() -> Result<(), Box<dyn Error>> {
hash: Vec::from(challenge),
allow: vec![credential],
user_verification: UserVerificationRequirement::Discouraged,
extensions: None,
extensions: Some(GetAssertionRequestExtensions::default()),
timeout: TIMEOUT,
};

Expand Down
166 changes: 166 additions & 0 deletions libwebauthn/examples/webauthn_json_hid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use std::error::Error;
use std::io::{self, Write};
use std::time::Duration;

use libwebauthn::UvUpdate;
use text_io::read;
use tokio::sync::broadcast::Receiver;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, MakeCredentialRequest, RelyingPartyId, WebAuthnIDL as _,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::transport::hid::list_devices;
use libwebauthn::transport::{Channel as _, Device};
use libwebauthn::webauthn::{Error as WebAuthnError, WebAuthn};

const TIMEOUT: Duration = Duration::from_secs(10);

fn setup_logging() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.without_time()
.init();
}

async fn handle_updates(mut state_recv: Receiver<UvUpdate>) {
while let Ok(update) = state_recv.recv().await {
match update {
UvUpdate::PresenceRequired => println!("Please touch your device!"),
UvUpdate::UvRetry { attempts_left } => {
print!("UV failed.");
if let Some(attempts_left) = attempts_left {
print!(" You have {attempts_left} attempts left.");
}
}
UvUpdate::PinRequired(update) => {
let mut attempts_str = String::new();
if let Some(attempts) = update.attempts_left {
attempts_str = format!(". You have {attempts} attempts left!");
};

match update.reason {
PinRequestReason::RelyingPartyRequest => println!("RP required a PIN."),
PinRequestReason::AuthenticatorPolicy => {
println!("Your device requires a PIN.")
}
PinRequestReason::FallbackFromUV => {
println!("UV failed too often and is blocked. Falling back to PIN.")
}
}
print!("PIN: Please enter the PIN for your authenticator{attempts_str}: ");
io::stdout().flush().unwrap();
let pin_raw: String = read!("{}\n");

if pin_raw.is_empty() {
println!("PIN: No PIN provided, cancelling operation.");
update.cancel();
} else {
let _ = update.send_pin(&pin_raw);
}
}
}
}
}

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

let devices = list_devices().await.unwrap();
println!("Devices found: {:?}", devices);

for mut device in devices {
println!("Selected HID authenticator: {}", &device);
let mut channel = device.channel().await?;
channel.wink(TIMEOUT).await?;

// Relying
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "// Relying" on line 79 appears to be incomplete. It should probably say "// Relying Party ID" or similar.

Suggested change
// Relying
// Relying Party ID

Copilot uses AI. Check for mistakes.
let rpid = RelyingPartyId("example.org".to_owned());
let request_json = r#"
{
"rp": {
"id": "example.org",
"name": "Example Relying Party"
},
"user": {
"id": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg",
"name": "Mario Rossi",
"displayName": "Mario Rossi"
},
"challenge": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg",
"pubKeyCredParams": [
{"type": "public-key", "alg": -7}
],
"timeout": 60000,
"excludeCredentials": [],
"authenticatorSelection": {
"residentKey": "discouraged",
"userVerification": "preferred"
},
"attestation": "none"
}
"#;
let make_credentials_request: MakeCredentialRequest =
MakeCredentialRequest::from_json(&rpid, request_json)
.expect("Failed to parse request JSON");
println!(
"WebAuthn MakeCredential request: {:?}",
make_credentials_request
);

let state_recv = channel.get_ux_update_receiver();
tokio::spawn(handle_updates(state_recv));

let response = loop {
match channel
.webauthn_make_credential(&make_credentials_request)
.await
{
Ok(response) => break Ok(response),
Err(WebAuthnError::Ctap(ctap_error)) => {
if ctap_error.is_retryable_user_error() {
println!("Oops, try again! Error: {}", ctap_error);
continue;
}
break Err(WebAuthnError::Ctap(ctap_error));
}
Err(err) => break Err(err),
};
}
.unwrap();
println!("WebAuthn MakeCredential response: {:?}", response);

let request_json = r#"
{
"challenge": "Y3JlZGVudGlhbHMtZm9yLWxpbnV4L2xpYndlYmF1dGhu",
"timeout": 30000,
"rpId": "example.org",
"userVerification": "discouraged"
}
"#;
let get_assertion: GetAssertionRequest =
GetAssertionRequest::from_json(&rpid, request_json)
.expect("Failed to parse request JSON");
println!("WebAuthn GetAssertion request: {:?}", get_assertion);

let response = loop {
match channel.webauthn_get_assertion(&get_assertion).await {
Ok(response) => break Ok(response),
Err(WebAuthnError::Ctap(ctap_error)) => {
if ctap_error.is_retryable_user_error() {
println!("Oops, try again! Error: {}", ctap_error);
continue;
}
break Err(WebAuthnError::Ctap(ctap_error));
}
Err(err) => break Err(err),
};
}
.unwrap();
println!("WebAuthn GetAssertion response: {:?}", response);
}

Ok(())
}
6 changes: 3 additions & 3 deletions libwebauthn/examples/webauthn_preflight_hid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use tokio::sync::broadcast::Receiver;
use tracing_subscriber::{self, EnvFilter};

use libwebauthn::ops::webauthn::{
GetAssertionRequest, GetAssertionResponse, MakeCredentialRequest, ResidentKeyRequirement,
UserVerificationRequirement,
GetAssertionRequest, GetAssertionRequestExtensions, GetAssertionResponse,
MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement,
};
use libwebauthn::pin::PinRequestReason;
use libwebauthn::proto::ctap2::{
Expand Down Expand Up @@ -202,7 +202,7 @@ async fn get_assertion_call(
hash: Vec::from(challenge),
allow: allow_list,
user_verification: UserVerificationRequirement::Discouraged,
extensions: None,
extensions: Some(GetAssertionRequestExtensions::default()),
timeout: TIMEOUT,
};

Expand Down
Loading
Loading