Skip to content

Commit

Permalink
Merge pull request #49 from complexspaces/configurable-credid-length
Browse files Browse the repository at this point in the history
Add configuration option for new credential ID length
  • Loading branch information
Progdrasil authored Oct 24, 2024
2 parents 9e21b64 + 2dda67b commit 90c1c28
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

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

## Passkey v0.3.0
### passkey-authenticator v0.3.0
Expand Down
100 changes: 99 additions & 1 deletion passkey-authenticator/src/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,59 @@ mod make_credential;

use extensions::Extensions;

/// The length of credentialId that should be randomly generated during a credential creation operation.
///
/// The value has a maximum of `64` per the [webauthn specification]. The minimum is a library enforced as `16`.
///
/// It is recommended to randomize this if possible to avoid authenticator fingerprinting.
///
/// [webauthn specification]: https://www.w3.org/TR/webauthn-3/#user-handle
#[derive(Debug, Clone, Copy)]
#[repr(transparent)]
pub struct CredentialIdLength(u8);

impl CredentialIdLength {
/// The default length of a credentialId to generate.
///
/// This value is the same as [`Self::default`], but available in
/// `const` contexts.
pub const DEFAULT: Self = Self(Self::MIN);

const MIN: u8 = 16;

// "A user handle is an opaque byte sequence with a maximum size of 64 bytes..."
// Ref: https://www.w3.org/TR/webauthn-3/#user-handle
const MAX: u8 = 64;

/// Generates and returns a uniformly random [CredentialIdLength].
pub fn randomized(rng: &mut impl rand::Rng) -> Self {
let length = rng.gen_range(Self::MIN..=Self::MAX);
Self(length)
}
}

impl Default for CredentialIdLength {
fn default() -> Self {
Self::DEFAULT
}
}

impl From<u8> for CredentialIdLength {
fn from(value: u8) -> Self {
// Clamp to the specification's maximum.
let value = core::cmp::min(Self::MAX, value);
// Round values less then what we support up to the default.
let value = core::cmp::max(Self::MIN, value);
Self(value)
}
}

impl From<CredentialIdLength> for usize {
fn from(value: CredentialIdLength) -> Self {
usize::from(value.0)
}
}

/// A virtual authenticator with all the necessary state and information.
pub struct Authenticator<S, U> {
/// The authenticator's AAGUID
Expand All @@ -35,6 +88,9 @@ pub struct Authenticator<S, U> {
/// with the distributed nature of synced keys. It can also cause issues with backup and restore functionality.
make_credentials_with_signature_counter: bool,

/// The length of the credentialId made during a creation operation.
credential_id_length: CredentialIdLength,

/// Supported authenticator extensions
extensions: Extensions,
}
Expand All @@ -57,6 +113,7 @@ where
],
user_validation: user,
make_credentials_with_signature_counter: false,
credential_id_length: CredentialIdLength::default(),
extensions: Extensions::default(),
}
}
Expand Down Expand Up @@ -84,6 +141,16 @@ where
self.make_credentials_with_signature_counter
}

/// Set the length of credentialId to generate when creating a new credential.
pub fn set_make_credential_id_length(&mut self, length: CredentialIdLength) {
self.credential_id_length = length;
}

/// Get the current length of credential that will be generated when making a new credential.
pub fn make_credential_id_length(&self) -> CredentialIdLength {
self.credential_id_length
}

/// Access the [`CredentialStore`] to look into what is stored.
pub fn store(&self) -> &S {
&self.store
Expand Down Expand Up @@ -186,7 +253,7 @@ where
mod tests {
use passkey_types::ctap2::{Aaguid, Flags};

use crate::{Authenticator, MockUserValidationMethod, UserCheck};
use crate::{Authenticator, CredentialIdLength, MockUserValidationMethod, UserCheck};

#[tokio::test]
async fn check_user_does_not_check_up_or_uv_when_not_requested() {
Expand Down Expand Up @@ -440,4 +507,35 @@ mod tests {
// Assert
assert_eq!(result, Flags::UP | Flags::UV);
}

#[test]
fn credential_id_lengths_validate() {
for num in 0..u8::MAX {
let length = CredentialIdLength::from(num);
if !(16..=64).contains(&num) {
if num < 16 {
// Lower values should be rounded up.
assert_eq!(length.0, CredentialIdLength::DEFAULT.0);
} else {
// Higher values should be clamped
assert_eq!(length.0, 64);
}
}
}

assert_eq!(
CredentialIdLength::DEFAULT.0,
CredentialIdLength::default().0
);
}

#[test]
fn credential_id_generation() {
let mut rng = rand::thread_rng();
let valid_range = 0..=64;
for _ in 0..=100 {
let length = CredentialIdLength::randomized(&mut rng).0;
assert!(valid_range.contains(&length));
}
}
}
7 changes: 1 addition & 6 deletions passkey-authenticator/src/authenticator/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,7 @@ where
// error.

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

let private_key = {
let mut rng = rand::thread_rng();
Expand Down
2 changes: 1 addition & 1 deletion passkey-authenticator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use p256::{
use passkey_types::{ctap2::Ctap2Error, Bytes};

pub use self::{
authenticator::{extensions, Authenticator},
authenticator::{extensions, Authenticator, CredentialIdLength},
credential_store::{CredentialStore, DiscoverabilitySupport, MemoryStore, StoreInfo},
ctap2::Ctap2Api,
u2f::U2fApi,
Expand Down

0 comments on commit 90c1c28

Please sign in to comment.