Skip to content

Commit 9b6822c

Browse files
docs(credmgmt): document persistent token usage and add example
1 parent bed6c79 commit 9b6822c

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ _Looking for the D-Bus API proposal?_ Check out [credentialsd][credentialsd].
3333
- 🟢 GetPinToken
3434
- 🟢 GetPinUvAuthTokenUsingPinWithPermissions
3535
- 🟢 GetPinUvAuthTokenUsingUvWithPermissions
36+
- 🟢 Persistent pinUvAuthToken for read-only credential management (pcmr, CTAP 2.2+)
3637
- [Passkey Authentication][passkeys]
3738
- 🟢 Discoverable credentials (resident keys)
3839
- 🟢 Hybrid transport (caBLE v2): QR-initiated transactions
@@ -93,6 +94,7 @@ $ cargo run --example change_pin_hid
9394
$ cargo run --example bio_enrollment_hid
9495
$ cargo run --example authenticator_config_hid
9596
$ cargo run --example cred_management_hid
97+
$ cargo run --example persistent_cred_management_hid
9698
```
9799

98100
## Contributing

libwebauthn/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,7 @@ path = "examples/management/authenticator_config_hid.rs"
176176
[[example]]
177177
name = "cred_management_hid"
178178
path = "examples/management/cred_management_hid.rs"
179+
180+
[[example]]
181+
name = "persistent_cred_management_hid"
182+
path = "examples/management/persistent_cred_management_hid.rs"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//! Read-only credential management backed by a persistent pinUvAuthToken (pcmr).
2+
//!
3+
//! A persistent token (CTAP 2.2+) lets a credential manager enumerate passkeys without
4+
//! re-prompting for the PIN on every launch or replug: the platform mints the token once
5+
//! and reuses it on later connections, until a PIN change or authenticator reset.
6+
//!
7+
//! The token is a long-lived bearer secret, so store it with confidentiality equivalent
8+
//! to other credential secrets (an OS keyring, or encrypted-at-rest with OS access
9+
//! control). This example uses the in-memory [`MemoryPersistentTokenStore`], which keeps
10+
//! records for the lifetime of the process and so demonstrates same-session reuse.
11+
12+
use std::sync::Arc;
13+
use std::time::Duration;
14+
15+
use libwebauthn::management::CredentialManagement;
16+
use libwebauthn::pin::persistent_token::{MemoryPersistentTokenStore, PersistentTokenStore};
17+
use libwebauthn::proto::ctap2::Ctap2;
18+
use libwebauthn::transport::hid::list_devices;
19+
use libwebauthn::transport::{Channel as _, Device};
20+
use libwebauthn::webauthn::Error as WebAuthnError;
21+
22+
#[path = "../common/mod.rs"]
23+
mod common;
24+
25+
const TIMEOUT: Duration = Duration::from_secs(10);
26+
27+
#[tokio::main]
28+
pub async fn main() -> Result<(), WebAuthnError> {
29+
common::setup_logging();
30+
31+
// In production, use a securely stored implementation. See the module docs.
32+
let store: Arc<dyn PersistentTokenStore> = Arc::new(MemoryPersistentTokenStore::new());
33+
34+
let devices = list_devices().await.unwrap();
35+
println!("Devices found: {:?}", devices);
36+
37+
for device in devices {
38+
// Attach the store to the device; every channel opened from it forwards the store.
39+
let mut device = device.with_persistent_token_store(store.clone());
40+
println!("Selected HID authenticator: {}", &device);
41+
42+
let mut channel = device.channel().await?;
43+
let state_recv = channel.get_ux_update_receiver();
44+
tokio::spawn(common::handle_uv_updates(state_recv));
45+
46+
let info = channel.ctap2_get_info().await?;
47+
if !info.supports_credential_management() {
48+
println!("This authenticator does not support credential management.");
49+
continue;
50+
}
51+
if !info.supports_persistent_credential_management_read_only() {
52+
println!(
53+
"This authenticator does not advertise perCredMgmtRO. Read-only credential \
54+
management will use an ordinary (ephemeral) pinUvAuthToken and prompt for \
55+
the PIN as usual."
56+
);
57+
}
58+
59+
// First pass: with an empty store the platform mints a persistent token, so a PIN
60+
// prompt is expected (a UV-capable authenticator may prompt for a touch instead).
61+
println!("\nFirst enumeration (expect a PIN prompt if nothing is cached yet):");
62+
print_metadata(&mut channel).await?;
63+
64+
// Second pass: the persistent token is recognized in the store and reused, so no
65+
// PIN prompt. With a durable store this also holds across restarts and replugs.
66+
println!("\nSecond enumeration (persistent token reused, no PIN prompt expected):");
67+
print_metadata(&mut channel).await?;
68+
69+
return Ok(());
70+
}
71+
72+
Ok(())
73+
}
74+
75+
async fn print_metadata(channel: &mut impl CredentialManagement) -> Result<(), WebAuthnError> {
76+
let metadata = channel.get_credential_metadata(TIMEOUT).await?;
77+
println!(
78+
"Discoverable credentials: {} (max remaining: {})",
79+
metadata.existing_resident_credentials_count,
80+
metadata.max_possible_remaining_resident_credentials_count,
81+
);
82+
Ok(())
83+
}

0 commit comments

Comments
 (0)