forked from kanidm/webauthn-rs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add fake credential generator interfaces
- Loading branch information
Showing
5 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,323 @@ | ||
//! Fake `CredentialID` generator. See [WebauthnFakeCredentialGenerator] for more details. | ||
|
||
use openssl::{hash, pkey, sign}; | ||
use rand::prelude::*; | ||
use rand_chacha::ChaCha8Rng; | ||
|
||
use crate::error::WebauthnError; | ||
use crate::proto::CredentialID; | ||
|
||
/// A trait for implementing custom `CredentialID` distributions. You *must* use the provided | ||
/// rng for generating `CredentialID`s to ensure that the outputs are deterministic. | ||
pub trait FakeCredentialIDDistribution { | ||
/// Given the provided rng, generate fake `CredentialID`s. | ||
fn generate<R: RngCore>(seeded_rng: &mut R) -> Vec<CredentialID>; | ||
} | ||
|
||
// Median number of credentials on an account. Due to synced credentials a lot of consumers will | ||
// only have a single credential contrast to security key users who tend to use 2 or more. | ||
const CREDENTIAL_MEDIAN: u8 = 1; | ||
|
||
// Various credential type lengths, taken from observation. | ||
|
||
/// Size of the `CredentialID` returned by a Yubikey Series 5 with ECDSA. | ||
pub const YUBIKEY_CRED_LEN: usize = 64; | ||
/// Size of the `CredentialID` returned by a Yubikey Series 5 with EDDSA. | ||
pub const YUBIKEY_EDDSA_CRED_LEN: usize = 128; | ||
/// Size of the `CredentialID` returned by a TPM, including Windows Hello. | ||
pub const TPM_CRED_LEN: usize = 32; | ||
/// Size of the `CredentialID` returned by a Google Pixel. | ||
pub const G_PIXEL_CRED_LEN: usize = 65; | ||
/// Size of the `CredentialID` returned by Apple Keychain on MacOS and iOS. | ||
pub const APPLE_CRED_LEN: usize = 20; | ||
/// Size of the `CredentialID` returned by Bitwarden Password Manager | ||
pub const BITWARDEN_CRED_LEN: usize = 16; | ||
|
||
// Solokeys EDDSA. | ||
// const SK_EDDSA_CRED_LEN_1: usize = 268; | ||
// const SK_EDDSA_CRED_LEN_1: usize = 211; | ||
// const SK_EDDSA_CRED_LEN_1: usize = 195; | ||
|
||
// These are the values for std distribution of a u32. We use this for | ||
// helping create believable ranges of credential's on accounts etc. | ||
const SD_02: u32 = 94489281; | ||
const SD_15: u32 = 678604833; | ||
// const SD_50: u32 = 2147483647; | ||
const SD_84: u32 = 3612067496; | ||
const SD_98: u32 = 4200478015; | ||
|
||
// ================================================= | ||
|
||
// These numbers are open to discussion. | ||
|
||
// If you only have one cred, we want to "fake" a synced credential. Today that's most likely | ||
// an iphone or android, which is split 70/30 android/ios. We also want to consider third | ||
// party password managers - these tend to be only used by tech communities rather than the | ||
// general population. then factor this so that 98% of these are mobile keychains, 2% are | ||
// pw managers | ||
|
||
const CRED_SINGLE_68: u32 = 2946347564; | ||
const CRED_SINGLE_98: u32 = 4200478014; | ||
|
||
// For multiple credentials, we assume a broader range of possible credentials. | ||
// https://gs.statcounter.com/os-market-share/ | ||
// | ||
// Android 41.64% - g-pixel | ||
// Windows 29.25% - tpm | ||
// iOS 17.71% - apple kc | ||
// OS X 6.57% - apple kc | ||
// Unknown 1.93% - n/a | ||
// Linux 1.54% - n/a | ||
// | ||
// Similar to above, we assume that pw managers and security keys are limited in the | ||
// general consumer populace. For now I'm going to assume this at about 4% as an | ||
// arbitrary number. Especially for unknown + linux, these rely on sk/pw manager anyway | ||
// | ||
// I'm also going to reduce the windows tpm values to 1/4 - windows hello probably has | ||
// less adoption than anything else, both due to tpm requirements, but also general | ||
// consumer rejection of ms related tech. This means we should distribute and inflate the | ||
// other values. | ||
// | ||
// With that done, we end up with roughly. | ||
// | ||
// Android 50.61% - g-pixel | ||
// Windows 8.68% - tpm | ||
// Apl 33.25% - apple kc | ||
// Other 7.46% - n/a | ||
// | ||
// Within "other" we assume: | ||
// | ||
// 50% pw manager | ||
// 50% yk | ||
|
||
const CRED_MULTI_ANDROID: u32 = 2173682947; | ||
const CRED_MULTI_WINDOWS: u32 = CRED_MULTI_ANDROID + 372803161; | ||
const CRED_MULTI_APL: u32 = CRED_MULTI_WINDOWS + 1428076625; | ||
const CRED_MULTI_YK: u32 = CRED_MULTI_APL + 160202280; | ||
|
||
/// A distribution for service providers which have passkeys as an authentication option parallel | ||
/// to passwords and other MFA types. This type assumes that not all users may have registered | ||
/// passkeys to their accounts. | ||
pub struct FakePasskeyDistribution; | ||
|
||
impl FakeCredentialIDDistribution for FakePasskeyDistribution { | ||
/// Given the provided rng, generate fake `CredentialID`s. | ||
fn generate<R: RngCore>(seeded_rng: &mut R) -> Vec<CredentialID> { | ||
// How many credentials should we create? | ||
let cred_dist = seeded_rng.next_u32(); | ||
|
||
let creds_to_generate = if cred_dist < SD_02 { | ||
// -2 | ||
CREDENTIAL_MEDIAN.saturating_sub(2) | ||
} else if cred_dist < SD_15 { | ||
// -1 | ||
CREDENTIAL_MEDIAN.saturating_sub(1) | ||
} else if cred_dist < SD_84 { | ||
// -0 | ||
CREDENTIAL_MEDIAN | ||
} else if cred_dist < SD_98 { | ||
// +1 | ||
CREDENTIAL_MEDIAN.saturating_add(1) | ||
} else { | ||
// +2 | ||
CREDENTIAL_MEDIAN.saturating_add(2) | ||
}; | ||
|
||
let mut credentials = Vec::with_capacity(creds_to_generate as usize); | ||
|
||
if creds_to_generate == 1 { | ||
let type_dist = seeded_rng.next_u32(); | ||
|
||
let cred_len = if type_dist < CRED_SINGLE_68 { | ||
// Android | ||
G_PIXEL_CRED_LEN | ||
} else if type_dist < CRED_SINGLE_98 { | ||
// iOS | ||
APPLE_CRED_LEN | ||
} else { | ||
// pw manager | ||
BITWARDEN_CRED_LEN | ||
}; | ||
|
||
let mut cred = vec![0; cred_len]; | ||
seeded_rng.fill_bytes(&mut cred); | ||
|
||
credentials.push(cred.into()); | ||
} else { | ||
for _i in 0..creds_to_generate { | ||
let type_dist = seeded_rng.next_u32(); | ||
|
||
let cred_len = if type_dist < CRED_MULTI_ANDROID { | ||
// Android | ||
G_PIXEL_CRED_LEN | ||
} else if type_dist < CRED_MULTI_WINDOWS { | ||
// Windows | ||
TPM_CRED_LEN | ||
} else if type_dist < CRED_MULTI_APL { | ||
// Apple | ||
APPLE_CRED_LEN | ||
} else if type_dist < CRED_MULTI_YK { | ||
// Other - yk | ||
YUBIKEY_CRED_LEN | ||
} else { | ||
BITWARDEN_CRED_LEN | ||
}; | ||
|
||
let mut cred = vec![0; cred_len]; | ||
seeded_rng.fill_bytes(&mut cred); | ||
|
||
credentials.push(cred.into()); | ||
} | ||
} | ||
|
||
credentials | ||
} | ||
} | ||
|
||
/// A fake `CredentialID` generator. This allows RP's to implement account enumeration defences if | ||
/// they so choose. Since webauthn requires the RP to send `CredentialID`s in challenges, when a | ||
/// user does not exist this will either provide an error or an empty list of IDs. This becomes a | ||
/// signal that an account with IDs associated to it in challenges must exist. | ||
/// | ||
/// Account enumeration defence is a very subjective matter. For example on a website for a | ||
/// psychologists or drug addiction service, account enumeration prevention is important for | ||
/// privacy of the users of the service. For a service such as a business it may be less important | ||
/// as corporate directories may be available. | ||
/// | ||
/// Because of this some considerations have been made in this api. | ||
/// | ||
/// The only input we have from the account enumeration is the username. | ||
/// | ||
/// Two users of this library should not have the same generated credentials given the same username. | ||
/// | ||
/// `CredentialID`s that are generated should be believable and appear to come from legitimate services | ||
/// or credential sources. This includes realistic distribution of the types of devices that users | ||
/// may be using. | ||
/// | ||
/// This api must be consistent - the same inputs will yield the same outputs. | ||
/// | ||
/// To achieve this, the generator uses a seeded CSPRNG for each operation. The CSPRNG is seeded | ||
/// from the HMAC of the username. The HMAC is keyed from an input that the site provides. | ||
/// | ||
/// The HMAC key should not be disclosed, as knowledge of the HMAC key will allow an external | ||
/// party to determine which IDs are generated and which are not. | ||
pub struct WebauthnFakeCredentialGenerator<D> | ||
where | ||
D: FakeCredentialIDDistribution, | ||
{ | ||
// hmac key | ||
hmac_key: pkey::PKey<pkey::Private>, | ||
distribution: std::marker::PhantomData<D>, | ||
} | ||
|
||
impl<D> WebauthnFakeCredentialGenerator<D> | ||
where | ||
D: FakeCredentialIDDistribution, | ||
{ | ||
/// Create a new HMAC keyed fake credential generator. You should associate a distribution type | ||
/// using type annotations. | ||
pub fn new(hmac_key: &[u8]) -> Result<Self, WebauthnError> { | ||
let hmac_key = pkey::PKey::hmac(hmac_key).map_err(WebauthnError::OpenSSLError)?; | ||
|
||
Ok(WebauthnFakeCredentialGenerator { | ||
hmac_key, | ||
distribution: std::marker::PhantomData, | ||
}) | ||
} | ||
|
||
/// Given a username as a byte slice, generate a set of deterministic `CredentialID`s. | ||
pub fn generate(&self, username: &[u8]) -> Result<Vec<CredentialID>, WebauthnError> { | ||
// hmac the username | ||
let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &self.hmac_key) | ||
.map_err(WebauthnError::OpenSSLError)?; | ||
|
||
let mut seed = [0; 32]; | ||
let buf = signer | ||
.sign_oneshot_to_vec(username) | ||
.map_err(WebauthnError::OpenSSLError)?; | ||
|
||
seed.copy_from_slice(&buf); | ||
|
||
// Seed the rng | ||
let mut seeded_rng = ChaCha8Rng::from_seed(seed); | ||
|
||
let credentials = D::generate(&mut seeded_rng); | ||
|
||
Ok(credentials) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::{FakePasskeyDistribution, WebauthnFakeCredentialGenerator}; | ||
use crate::proto::Base64UrlSafeData; | ||
|
||
#[test] | ||
fn test_fake_credential_generator() { | ||
let _ = tracing_subscriber::fmt::try_init(); | ||
|
||
let cred_gen: WebauthnFakeCredentialGenerator<FakePasskeyDistribution> = | ||
WebauthnFakeCredentialGenerator::new(&[0, 1, 2, 3]).unwrap(); | ||
|
||
let cred_a = cred_gen.generate("a".as_bytes()).unwrap(); | ||
assert!(cred_a.is_empty()); | ||
|
||
let cred_b = cred_gen.generate("b".as_bytes()).unwrap(); | ||
assert_eq!( | ||
cred_b, | ||
vec![Base64UrlSafeData(vec![ | ||
77, 7, 210, 37, 212, 90, 185, 162, 81, 110, 242, 185, 204, 84, 84, 123, 155, 139, | ||
146, 230 | ||
])] | ||
); | ||
|
||
let cred_c = cred_gen.generate("c".as_bytes()).unwrap(); | ||
assert!(cred_c.is_empty()); | ||
|
||
let cred_d = cred_gen.generate("d".as_bytes()).unwrap(); | ||
assert_eq!( | ||
cred_d, | ||
vec![Base64UrlSafeData(vec![ | ||
203, 174, 48, 43, 223, 223, 211, 78, 99, 88, 240, 25, 90, 42, 86, 186, 239, 57, | ||
123, 81, 177, 173, 236, 214, 204, 222, 224, 134, 233, 143, 143, 144, 127, 23, 26, | ||
145, 217, 217, 110, 194, 235, 76, 2, 59, 56, 98, 47, 236, 103, 98, 235, 239, 195, | ||
140, 199, 239, 201, 11, 132, 227, 181, 7, 188, 240, 168 | ||
])] | ||
); | ||
|
||
let cred_e = cred_gen.generate("e".as_bytes()).unwrap(); | ||
assert_eq!( | ||
cred_e, | ||
vec![ | ||
Base64UrlSafeData(vec![ | ||
207, 79, 70, 16, 136, 39, 65, 40, 104, 116, 214, 85, 66, 12, 175, 99, 203, 228, | ||
60, 249, 118, 169, 28, 217, 161, 132, 3, 217, 119, 66, 235, 151, 138, 15, 26, | ||
76, 161, 44, 225, 120, 34, 131, 48, 195, 116, 81, 178, 0, 218, 96, 167, 1, 70, | ||
183, 20, 94, 115, 63, 12, 235, 189, 105, 104, 60, 77 | ||
]), | ||
Base64UrlSafeData(vec![ | ||
175, 118, 205, 177, 121, 39, 194, 157, 251, 53, 216, 180, 38, 22, 44, 155, 132, | ||
155, 204, 68, 171, 98, 97, 114, 50, 58, 218, 238, 44, 154, 27, 140, 95, 90, | ||
127, 210, 221, 177, 194, 44, 231, 178, 238, 239, 79, 222, 127, 164, 115, 65, | ||
160, 6, 55, 150, 30, 140, 18, 159, 229, 159, 78, 216, 120, 27, 122 | ||
]) | ||
] | ||
); | ||
|
||
// Demonstrate that re-keying the generator yields different generated credential results. | ||
let alt_cred_gen: WebauthnFakeCredentialGenerator<FakePasskeyDistribution> = | ||
WebauthnFakeCredentialGenerator::new(&[3, 2, 1, 0]).unwrap(); | ||
|
||
let alt_cred_a = alt_cred_gen.generate("a".as_bytes()).unwrap(); | ||
assert!(alt_cred_a != cred_a); | ||
assert_eq!( | ||
alt_cred_a, | ||
vec![Base64UrlSafeData(vec![ | ||
44, 141, 39, 252, 47, 212, 48, 123, 96, 131, 15, 213, 21, 149, 95, 147, 188, 152, | ||
201, 171, 245, 103, 22, 246, 211, 172, 143, 86, 97, 96, 109, 246, 23, 54, 13, 127, | ||
167, 107, 72, 235, 151, 144, 162, 200, 251, 93, 137, 8, 211, 197, 47, 115, 108, | ||
210, 62, 232, 246, 206, 36, 202, 94, 179, 254, 81, 59 | ||
])] | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ mod constants; | |
|
||
pub mod attestation; | ||
pub mod crypto; | ||
pub mod fake; | ||
|
||
mod core; | ||
pub mod error; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters