Skip to content

Commit 5bb01a1

Browse files
feat(credmgmt): request pcmr for read-only subcommands
1 parent 01a3997 commit 5bb01a1

4 files changed

Lines changed: 84 additions & 1 deletion

File tree

libwebauthn/src/management/credential_management.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,12 @@ impl Ctap2UserVerifiableRequest for Ctap2CredentialManagementRequest {
295295
}
296296

297297
fn permissions(&self) -> Ctap2AuthTokenPermissionRole {
298-
Ctap2AuthTokenPermissionRole::CREDENTIAL_MANAGEMENT
298+
if self.use_persistent_token {
299+
// pcmr MUST be the sole permission requested (CTAP 2.3-PS 6.5.5.7).
300+
Ctap2AuthTokenPermissionRole::PERSISTENT_CREDENTIAL_MANAGEMENT_READ_ONLY
301+
} else {
302+
Ctap2AuthTokenPermissionRole::CREDENTIAL_MANAGEMENT
303+
}
299304
}
300305

301306
fn permissions_rpid(&self) -> Option<&str> {
@@ -322,4 +327,16 @@ impl Ctap2UserVerifiableRequest for Ctap2CredentialManagementRequest {
322327
fn needs_shared_secret(&self, _get_info_response: &Ctap2GetInfoResponse) -> bool {
323328
false
324329
}
330+
331+
fn set_persistent_token_use(&mut self, info: &Ctap2GetInfoResponse, store_available: bool) {
332+
self.use_persistent_token = store_available
333+
&& info.supports_persistent_credential_management_read_only()
334+
&& self
335+
.subcommand
336+
.is_some_and(|subcommand| subcommand.is_read_only());
337+
}
338+
339+
fn wants_persistent_token(&self) -> bool {
340+
self.use_persistent_token
341+
}
325342
}

libwebauthn/src/proto/ctap2/model.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,15 @@ pub trait Ctap2UserVerifiableRequest {
310310
fn handle_legacy_preview(&mut self, info: &Ctap2GetInfoResponse);
311311
/// We need to establish a shared secret, even if no PIN or UV is set on the device
312312
fn needs_shared_secret(&self, info: &Ctap2GetInfoResponse) -> bool;
313+
/// Decide, and cache on the request, whether to acquire a persistent (pcmr) token.
314+
/// Called once from the UV flow with whether a persistent token store is available.
315+
/// Default: never request one.
316+
fn set_persistent_token_use(&mut self, _info: &Ctap2GetInfoResponse, _store_available: bool) {}
317+
/// Whether this request will reuse or mint a persistent (pcmr) token, per the cached
318+
/// decision from [`Self::set_persistent_token_use`]. Default false.
319+
fn wants_persistent_token(&self) -> bool {
320+
false
321+
}
313322
}
314323

315324
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

libwebauthn/src/proto/ctap2/model/credential_management.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ pub struct Ctap2CredentialManagementRequest {
3131

3232
#[serde(skip)]
3333
pub use_legacy_preview: bool,
34+
35+
/// Cached gate: request a persistent (pcmr) token instead of an ephemeral `cm` one.
36+
/// Set from getInfo and store availability before `permissions()` is read.
37+
#[serde(skip)]
38+
pub use_persistent_token: bool,
3439
}
3540

3641
#[repr(u32)]
@@ -45,6 +50,21 @@ pub enum Ctap2CredentialManagementSubcommand {
4550
UpdateUserInformation = 0x07,
4651
}
4752

53+
impl Ctap2CredentialManagementSubcommand {
54+
/// Read-only subcommands can be authorized by a persistent (pcmr) token; the write
55+
/// subcommands (deleteCredential, updateUserInformation) cannot.
56+
pub fn is_read_only(self) -> bool {
57+
matches!(
58+
self,
59+
Self::GetCredsMetadata
60+
| Self::EnumerateRPsBegin
61+
| Self::EnumerateRPsGetNextRP
62+
| Self::EnumerateCredentialsBegin
63+
| Self::EnumerateCredentialsGetNextCredential
64+
)
65+
}
66+
}
67+
4868
#[derive(Debug, Clone, SerializeIndexed)]
4969
pub struct Ctap2CredentialManagementParams {
5070
// rpIDHash (0x01) Byte String RP ID SHA-256 hash
@@ -129,6 +149,7 @@ impl Ctap2CredentialManagementRequest {
129149
protocol: None,
130150
uv_auth_param: None,
131151
use_legacy_preview: false,
152+
use_persistent_token: false,
132153
}
133154
}
134155

@@ -139,6 +160,7 @@ impl Ctap2CredentialManagementRequest {
139160
protocol: None,
140161
uv_auth_param: None,
141162
use_legacy_preview: false,
163+
use_persistent_token: false,
142164
}
143165
}
144166

@@ -149,6 +171,7 @@ impl Ctap2CredentialManagementRequest {
149171
protocol: None,
150172
uv_auth_param: None,
151173
use_legacy_preview: false,
174+
use_persistent_token: false,
152175
}
153176
}
154177

@@ -163,6 +186,7 @@ impl Ctap2CredentialManagementRequest {
163186
protocol: None,
164187
uv_auth_param: None,
165188
use_legacy_preview: false,
189+
use_persistent_token: false,
166190
}
167191
}
168192

@@ -175,6 +199,7 @@ impl Ctap2CredentialManagementRequest {
175199
protocol: None,
176200
uv_auth_param: None,
177201
use_legacy_preview: false,
202+
use_persistent_token: false,
178203
}
179204
}
180205

@@ -189,6 +214,7 @@ impl Ctap2CredentialManagementRequest {
189214
protocol: None,
190215
uv_auth_param: None,
191216
use_legacy_preview: false,
217+
use_persistent_token: false,
192218
}
193219
}
194220

@@ -206,6 +232,7 @@ impl Ctap2CredentialManagementRequest {
206232
protocol: None,
207233
uv_auth_param: None,
208234
use_legacy_preview: false,
235+
use_persistent_token: false,
209236
}
210237
}
211238
}
@@ -268,3 +295,32 @@ impl Ctap2RPData {
268295
Self { rp, rp_id_hash }
269296
}
270297
}
298+
299+
#[cfg(test)]
300+
mod test {
301+
use super::Ctap2CredentialManagementSubcommand as Sub;
302+
303+
#[test]
304+
fn read_only_classification() {
305+
// Read-only: authorizable by a pcmr token.
306+
for subcommand in [
307+
Sub::GetCredsMetadata,
308+
Sub::EnumerateRPsBegin,
309+
Sub::EnumerateRPsGetNextRP,
310+
Sub::EnumerateCredentialsBegin,
311+
Sub::EnumerateCredentialsGetNextCredential,
312+
] {
313+
assert!(
314+
subcommand.is_read_only(),
315+
"{subcommand:?} should be read-only"
316+
);
317+
}
318+
// Writes: never pcmr.
319+
for subcommand in [Sub::DeleteCredential, Sub::UpdateUserInformation] {
320+
assert!(
321+
!subcommand.is_read_only(),
322+
"{subcommand:?} should be a write"
323+
);
324+
}
325+
}
326+
}

libwebauthn/src/proto/ctap2/protocol.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ mod tests {
468468
protocol: None,
469469
uv_auth_param: None,
470470
use_legacy_preview: false,
471+
use_persistent_token: false,
471472
};
472473
let expected_request: CborRequest = (&request).try_into().unwrap();
473474
channel.push_command_pair(expected_request, error_response(CtapError::PINRequired));

0 commit comments

Comments
 (0)