Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ _Looking for the D-Bus API proposal?_ Check out [credentialsd][credentialsd].
- 🟢 GetPinToken
- 🟢 GetPinUvAuthTokenUsingPinWithPermissions
- 🟢 GetPinUvAuthTokenUsingUvWithPermissions
- 🟢 Persistent pinUvAuthToken for read-only credential management (pcmr, CTAP 2.2+)
- [Passkey Authentication][passkeys]
- 🟢 Discoverable credentials (resident keys)
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
Expand Down Expand Up @@ -98,6 +99,7 @@ $ cargo run --example change_pin_hid
$ cargo run --example bio_enrollment_hid
$ cargo run --example authenticator_config_hid
$ cargo run --example cred_management_hid
$ cargo run --example persistent_cred_management_hid
```

## Contributing
Expand Down
4 changes: 4 additions & 0 deletions libwebauthn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,7 @@ path = "examples/management/authenticator_config_hid.rs"
[[example]]
name = "cred_management_hid"
path = "examples/management/cred_management_hid.rs"

[[example]]
name = "persistent_cred_management_hid"
path = "examples/management/persistent_cred_management_hid.rs"
86 changes: 86 additions & 0 deletions libwebauthn/examples/management/persistent_cred_management_hid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Read-only credential management backed by a persistent pinUvAuthToken (pcmr).
//!
//! A persistent token (CTAP 2.2+) lets a credential manager enumerate passkeys without
//! re-prompting for the PIN on every launch or replug: the platform mints the token once
//! and reuses it on later connections, until a PIN change or authenticator reset.
//!
//! The token is a long-lived bearer secret, so store it with confidentiality equivalent
//! to other credential secrets (an OS keyring, or encrypted-at-rest with OS access
//! control). This example uses the in-memory [`MemoryPersistentTokenStore`], which keeps
//! records for the lifetime of the process and so demonstrates same-session reuse.

use std::sync::Arc;
use std::time::Duration;

use libwebauthn::management::CredentialManagement;
use libwebauthn::pin::persistent_token::{MemoryPersistentTokenStore, PersistentTokenStore};
use libwebauthn::proto::ctap2::Ctap2;
use libwebauthn::transport::hid::list_devices;
use libwebauthn::transport::{Channel as _, ChannelSettings, Device};
use libwebauthn::webauthn::Error as WebAuthnError;

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

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

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

// In production, use a securely stored implementation. See the module docs.
let store: Arc<dyn PersistentTokenStore> = Arc::new(MemoryPersistentTokenStore::new());

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

for mut device in devices {
println!("Selected HID authenticator: {}", &device);

// Pass the store via ChannelSettings; the channel reuses or mints a persistent
// token through it. The same settings apply to any transport.
let settings = ChannelSettings {
persistent_token_store: Some(store.clone()),
};
let mut channel = device.channel(settings).await?;
let state_recv = channel.get_ux_update_receiver();
tokio::spawn(common::handle_uv_updates(state_recv));

let info = channel.ctap2_get_info().await?;
if !info.supports_credential_management() {
println!("This authenticator does not support credential management.");
continue;
}
if !info.supports_persistent_credential_management_read_only() {
println!(
"This authenticator does not advertise perCredMgmtRO. Read-only credential \
management will use an ordinary (ephemeral) pinUvAuthToken and prompt for \
the PIN as usual."
);
}

// First pass: with an empty store the platform mints a persistent token, so a PIN
// prompt is expected (a UV-capable authenticator may prompt for a touch instead).
println!("\nFirst enumeration (expect a PIN prompt if nothing is cached yet):");
print_metadata(&mut channel).await?;

// Second pass: the persistent token is recognized in the store and reused, so no
// PIN prompt. With a durable store this also holds across restarts and replugs.
println!("\nSecond enumeration (persistent token reused, no PIN prompt expected):");
print_metadata(&mut channel).await?;

return Ok(());
}

Ok(())
}

async fn print_metadata(channel: &mut impl CredentialManagement) -> Result<(), WebAuthnError> {
let metadata = channel.get_credential_metadata(TIMEOUT).await?;
println!(
"Discoverable credentials: {} (max remaining: {})",
metadata.existing_resident_credentials_count,
metadata.max_possible_remaining_resident_credentials_count,
);
Ok(())
}
Loading