Skip to content

Commit 90c1c28

Browse files
authored
Merge pull request #49 from complexspaces/configurable-credid-length
Add configuration option for new credential ID length
2 parents 9e21b64 + 2dda67b commit 90c1c28

File tree

4 files changed

+102
-8
lines changed

4 files changed

+102
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## Unreleased
4+
- Added: support for controlling generated credential's ID length to passkey-authenticator ([#49](https://github.com/1Password/passkey-rs/pull/49))
45

56
## Passkey v0.3.0
67
### passkey-authenticator v0.3.0

passkey-authenticator/src/authenticator.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,59 @@ mod make_credential;
1313

1414
use extensions::Extensions;
1515

16+
/// The length of credentialId that should be randomly generated during a credential creation operation.
17+
///
18+
/// The value has a maximum of `64` per the [webauthn specification]. The minimum is a library enforced as `16`.
19+
///
20+
/// It is recommended to randomize this if possible to avoid authenticator fingerprinting.
21+
///
22+
/// [webauthn specification]: https://www.w3.org/TR/webauthn-3/#user-handle
23+
#[derive(Debug, Clone, Copy)]
24+
#[repr(transparent)]
25+
pub struct CredentialIdLength(u8);
26+
27+
impl CredentialIdLength {
28+
/// The default length of a credentialId to generate.
29+
///
30+
/// This value is the same as [`Self::default`], but available in
31+
/// `const` contexts.
32+
pub const DEFAULT: Self = Self(Self::MIN);
33+
34+
const MIN: u8 = 16;
35+
36+
// "A user handle is an opaque byte sequence with a maximum size of 64 bytes..."
37+
// Ref: https://www.w3.org/TR/webauthn-3/#user-handle
38+
const MAX: u8 = 64;
39+
40+
/// Generates and returns a uniformly random [CredentialIdLength].
41+
pub fn randomized(rng: &mut impl rand::Rng) -> Self {
42+
let length = rng.gen_range(Self::MIN..=Self::MAX);
43+
Self(length)
44+
}
45+
}
46+
47+
impl Default for CredentialIdLength {
48+
fn default() -> Self {
49+
Self::DEFAULT
50+
}
51+
}
52+
53+
impl From<u8> for CredentialIdLength {
54+
fn from(value: u8) -> Self {
55+
// Clamp to the specification's maximum.
56+
let value = core::cmp::min(Self::MAX, value);
57+
// Round values less then what we support up to the default.
58+
let value = core::cmp::max(Self::MIN, value);
59+
Self(value)
60+
}
61+
}
62+
63+
impl From<CredentialIdLength> for usize {
64+
fn from(value: CredentialIdLength) -> Self {
65+
usize::from(value.0)
66+
}
67+
}
68+
1669
/// A virtual authenticator with all the necessary state and information.
1770
pub struct Authenticator<S, U> {
1871
/// The authenticator's AAGUID
@@ -35,6 +88,9 @@ pub struct Authenticator<S, U> {
3588
/// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
3689
make_credentials_with_signature_counter: bool,
3790

91+
/// The length of the credentialId made during a creation operation.
92+
credential_id_length: CredentialIdLength,
93+
3894
/// Supported authenticator extensions
3995
extensions: Extensions,
4096
}
@@ -57,6 +113,7 @@ where
57113
],
58114
user_validation: user,
59115
make_credentials_with_signature_counter: false,
116+
credential_id_length: CredentialIdLength::default(),
60117
extensions: Extensions::default(),
61118
}
62119
}
@@ -84,6 +141,16 @@ where
84141
self.make_credentials_with_signature_counter
85142
}
86143

144+
/// Set the length of credentialId to generate when creating a new credential.
145+
pub fn set_make_credential_id_length(&mut self, length: CredentialIdLength) {
146+
self.credential_id_length = length;
147+
}
148+
149+
/// Get the current length of credential that will be generated when making a new credential.
150+
pub fn make_credential_id_length(&self) -> CredentialIdLength {
151+
self.credential_id_length
152+
}
153+
87154
/// Access the [`CredentialStore`] to look into what is stored.
88155
pub fn store(&self) -> &S {
89156
&self.store
@@ -186,7 +253,7 @@ where
186253
mod tests {
187254
use passkey_types::ctap2::{Aaguid, Flags};
188255

189-
use crate::{Authenticator, MockUserValidationMethod, UserCheck};
256+
use crate::{Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck};
190257

191258
#[tokio::test]
192259
async fn check_user_does_not_check_up_or_uv_when_not_requested() {
@@ -440,4 +507,35 @@ mod tests {
440507
// Assert
441508
assert_eq!(result, Flags::UP | Flags::UV);
442509
}
510+
511+
#[test]
512+
fn credential_id_lengths_validate() {
513+
for num in 0..u8::MAX {
514+
let length = CredentialIdLength::from(num);
515+
if !(16..=64).contains(&num) {
516+
if num < 16 {
517+
// Lower values should be rounded up.
518+
assert_eq!(length.0, CredentialIdLength::DEFAULT.0);
519+
} else {
520+
// Higher values should be clamped
521+
assert_eq!(length.0, 64);
522+
}
523+
}
524+
}
525+
526+
assert_eq!(
527+
CredentialIdLength::DEFAULT.0,
528+
CredentialIdLength::default().0
529+
);
530+
}
531+
532+
#[test]
533+
fn credential_id_generation() {
534+
let mut rng = rand::thread_rng();
535+
let valid_range = 0..=64;
536+
for _ in 0..=100 {
537+
let length = CredentialIdLength::randomized(&mut rng).0;
538+
assert!(valid_range.contains(&length));
539+
}
540+
}
443541
}

passkey-authenticator/src/authenticator/make_credential.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,7 @@ where
9191
// error.
9292

9393
// 9. Generate a new credential key pair for the algorithm specified.
94-
let credential_id: Vec<u8> = {
95-
use rand::RngCore;
96-
let mut data = vec![0u8; 16];
97-
rand::thread_rng().fill_bytes(&mut data);
98-
data
99-
};
94+
let credential_id = passkey_types::rand::random_vec(self.credential_id_length.into());
10095

10196
let private_key = {
10297
let mut rng = rand::thread_rng();

passkey-authenticator/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use p256::{
4242
use passkey_types::{ctap2::Ctap2Error, Bytes};
4343

4444
pub use self::{
45-
authenticator::{extensions, Authenticator},
45+
authenticator::{extensions, Authenticator, CredentialIdLength},
4646
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
4747
ctap2::Ctap2Api,
4848
u2f::U2fApi,

0 commit comments

Comments
 (0)