No problem, here’s a full working example with passkey (using a client-side passkey with the Rust passkeys library):
use std::collections::HashMap;
use anyhow::{Context, Result, anyhow};
use async_trait::async_trait;
use coset::iana;
use mfkdf2::definitions::{MFKDF2Options, factor::MFKDF2Factor};
use mfkdf2::setup::factors::{
passkey::PasskeyOptions,
totp::TOTPOptions,
uuid::{UUIDOptions, Uuid},
};
use passkey::{
authenticator::{Authenticator, UiHint, UserCheck, UserValidationMethod},
client::Client,
types::{
Bytes, Passkey,
ctap2::{Aaguid, Ctap2Error},
rand::random_vec,
webauthn::*,
},
};
use passkey_authenticator::extensions::HmacSecretConfig;
use url::Url;
const RECOVERY_CODE: &str = "550e8400-e29b-41d4-a716-446655440000";
// Application-specific PRF salt: the authenticator computes HMAC-SHA256(CredRandom, SHA256("WebAuthn PRF\0" || salt))
const PRF_EVAL_SALT: &[u8] = b"mfkdf2-passkey-salt-v1";
// Stub user-validation that approves every request (simulates a platform authenticator unlock).
struct AlwaysApprove;
#[async_trait]
impl UserValidationMethod for AlwaysApprove {
type PasskeyItem = Passkey;
async fn check_user<'a>(
&self,
_hint: UiHint<'a, Self::PasskeyItem>,
presence: bool,
verification: bool,
) -> Result<UserCheck, Ctap2Error> {
Ok(UserCheck { presence, verification })
}
fn is_verification_enabled(&self) -> Option<bool> { Some(true) }
fn is_presence_enabled(&self) -> bool { true }
}
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn setup_factors(prf: [u8; 32]) -> Result<Vec<MFKDF2Factor>> {
Ok(vec![
mfkdf2::setup::factors::passkey::passkey(prf, PasskeyOptions::default())?,
mfkdf2::setup::factors::totp(TOTPOptions {
id: Some("totp".to_string()),
secret: Some(b"abcdefghijklmnopqrst".to_vec()),
time: Some(1),
..Default::default()
})?,
mfkdf2::setup::factors::uuid::uuid(UUIDOptions {
id: Some("recovery_code".to_string()),
uuid: Some(Uuid::parse_str(RECOVERY_CODE)?),
})?,
])
}
// Build a CredentialRequestOptions with PRF eval for the given credential.
fn prf_auth_request(credential_id: Bytes) -> CredentialRequestOptions {
CredentialRequestOptions {
public_key: PublicKeyCredentialRequestOptions {
challenge: random_vec(32).into(),
timeout: None,
rp_id: Some("example.com".to_string()),
allow_credentials: Some(vec![PublicKeyCredentialDescriptor {
ty: PublicKeyCredentialType::PublicKey,
id: credential_id,
transports: None,
}]),
user_verification: UserVerificationRequirement::Required,
hints: None,
attestation: AttestationConveyancePreference::None,
attestation_formats: None,
extensions: Some(AuthenticationExtensionsClientInputs {
prf: Some(AuthenticationExtensionsPrfInputs {
eval: Some(AuthenticationExtensionsPrfValues {
first: PRF_EVAL_SALT.to_vec().into(),
second: None,
}),
eval_by_credential: None,
}),
..Default::default()
}),
},
}
}
fn extract_prf(auth_result: AuthenticatedPublicKeyCredential) -> Result<[u8; 32]> {
let prf_bytes: Vec<u8> = auth_result
.client_extension_results
.prf
.context("PRF extension absent from auth result")?
.results
.context("PRF results absent")?
.first
.into();
prf_bytes.try_into().map_err(|_| anyhow!("PRF output was not 32 bytes"))
}
#[tokio::main]
async fn main() -> Result<()> {
let origin = Url::parse("https://example.com").unwrap();
// Build a software authenticator with hmac-secret (PRF) support
let authenticator = Authenticator::new(Aaguid::new_empty(), None::<Passkey>, AlwaysApprove)
.hmac_secret(HmacSecretConfig::new_with_uv_only());
let mut client = Client::new(authenticator);
// ── Registration ──────────────────────────────────────────────────────────
let reg = client
.register(
&origin,
CredentialCreationOptions {
public_key: PublicKeyCredentialCreationOptions {
rp: PublicKeyCredentialRpEntity { id: None, name: "example.com".into() },
user: PublicKeyCredentialUserEntity {
id: random_vec(32).into(),
display_name: "Test User".into(),
name: "test@example.com".into(),
},
challenge: random_vec(32).into(),
pub_key_cred_params: vec![PublicKeyCredentialParameters {
ty: PublicKeyCredentialType::PublicKey,
alg: iana::Algorithm::ES256,
}],
timeout: None,
exclude_credentials: None,
authenticator_selection: None,
hints: None,
attestation: AttestationConveyancePreference::None,
attestation_formats: None,
// Enable PRF during registration (no eval output needed yet)
extensions: Some(AuthenticationExtensionsClientInputs {
prf: Some(AuthenticationExtensionsPrfInputs {
eval: None,
eval_by_credential: None,
}),
..Default::default()
}),
},
},
None,
)
.await
.map_err(|e| anyhow!("{e:?}"))?;
let credential_id = reg.raw_id.clone();
println!("Registered passkey (id: {}...)", &to_hex(&credential_id)[..16]);
// ── Setup: authenticate → get PRF bytes → build MFKDF2 policy ─────────────
let auth_setup = client
.authenticate(&origin, prf_auth_request(credential_id.clone()), None)
.await
.map_err(|e| anyhow!("{e:?}"))?;
let prf_setup = extract_prf(auth_setup)?;
println!("PRF output: {}", to_hex(&prf_setup));
let setup = mfkdf2::setup::key(
&setup_factors(prf_setup)?,
MFKDF2Options { threshold: Some(1), ..MFKDF2Options::default() },
)?;
let setup_hex = to_hex(&setup.key);
println!("Setup key: {}", setup_hex);
// ── Derive with passkey (re-authenticate → same PRF output) ───────────────
let auth_derive = client
.authenticate(&origin, prf_auth_request(credential_id.clone()), None)
.await
.map_err(|e| anyhow!("{e:?}"))?;
let prf_derive = extract_prf(auth_derive)?;
let from_passkey = mfkdf2::derive::key(
&setup.policy,
HashMap::from([("passkey".to_string(), mfkdf2::derive::factors::passkey(prf_derive)?)]),
true,
false,
)?;
let passkey_hex = to_hex(&from_passkey.key);
println!("Passkey: {} (match: {})", passkey_hex, passkey_hex == setup_hex);
// ── Derive with TOTP only ─────────────────────────────────────────────────
let from_totp = mfkdf2::derive::key(
&setup.policy,
HashMap::from([(
"totp".to_string(),
mfkdf2::derive::factors::totp(
241063,
Some(mfkdf2::derive::factors::totp::TOTPDeriveOptions {
time: Some(30001),
..Default::default()
}),
)?,
)]),
true,
false,
)?;
let totp_hex = to_hex(&from_totp.key);
println!("TOTP: {} (match: {})", totp_hex, totp_hex == setup_hex);
// ── Derive with recovery code only ────────────────────────────────────────
let from_recovery = mfkdf2::derive::key(
&setup.policy,
HashMap::from([(
"recovery_code".to_string(),
mfkdf2::derive::factors::uuid(Uuid::parse_str(RECOVERY_CODE)?)?,
)]),
true,
false,
)?;
let recovery_hex = to_hex(&from_recovery.key);
println!("Recovery: {} (match: {})", recovery_hex, recovery_hex == setup_hex);
Ok(())
}
Output:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
Running `target\debug\zcash_example.exe`
Registered passkey (id: 847144ce499acb66...)
PRF output: 470b9a3e74a92b093f57b0b48cf95cea4704d7f639d4ce9a24fa1f2812356272
Setup key: 1ff03acb598feed517c61582b5becd0c28379e4bf966d56be7d30843083d1c87
Passkey: 1ff03acb598feed517c61582b5becd0c28379e4bf966d56be7d30843083d1c87 (match: true)
TOTP: 1ff03acb598feed517c61582b5becd0c28379e4bf966d56be7d30843083d1c87 (match: true)
Recovery: 1ff03acb598feed517c61582b5becd0c28379e4bf966d56be7d30843083d1c87 (match: true)