diff --git a/Cargo.lock b/Cargo.lock index cdd164d..e574693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -655,7 +655,7 @@ dependencies = [ "const-oid", "der_derive", "flagset", - "pem-rfc7468", + "pem-rfc7468 0.6.0", "zeroize", ] @@ -666,6 +666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468 0.7.0", "zeroize", ] @@ -787,7 +788,7 @@ dependencies = [ "generic-array", "group 0.12.1", "hkdf", - "pem-rfc7468", + "pem-rfc7468 0.6.0", "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", @@ -802,14 +803,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct 0.2.0", + "base64ct", "crypto-bigint 0.5.5", "digest", "ff 0.13.0", "generic-array", "group 0.13.0", + "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", + "serde_json", "serdect", "subtle", "zeroize", @@ -821,7 +825,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.100", @@ -904,9 +908,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1124,6 +1128,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1521,9 +1531,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2009,6 +2019,60 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "passkey-authenticator" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b065ce31354bcf23a333003c77f0d71f00eb95761b3390a069546e078a7a5b" +dependencies = [ + "async-trait", + "coset", + "log", + "p256 0.13.2", + "passkey-types", + "rand 0.8.5", +] + +[[package]] +name = "passkey-client" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5080bfafe23d139ae8be8b907453aee0b8af3ca7cf25d1f8d7bfcf7b079d3412" +dependencies = [ + "ciborium", + "coset", + "idna", + "passkey-authenticator", + "passkey-types", + "public-suffix", + "serde", + "serde_json", + "typeshare", + "url", +] + +[[package]] +name = "passkey-types" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77144664f6aac5f629d7efa815f5098a054beeeca6ccafee5ec453fd2b0c53f9" +dependencies = [ + "bitflags 2.9.0", + "ciborium", + "coset", + "data-encoding", + "getrandom 0.2.15", + "hmac", + "indexmap 2.9.0", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "strum", + "typeshare", + "zeroize", +] + [[package]] name = "pem-rfc7468" version = "0.6.0" @@ -2018,11 +2082,20 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -2208,7 +2281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck", + "heck 0.5.0", "itertools", "log", "multimap", @@ -2226,7 +2299,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck", + "heck 0.5.0", "itertools", "log", "multimap", @@ -2284,6 +2357,12 @@ dependencies = [ "prost 0.13.5", ] +[[package]] +name = "public-suffix" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51315bca45305dd8aa64b831b33e71abac528ca8058c0651346a39b8d3009498" + [[package]] name = "quinn" version = "0.11.8" @@ -2785,6 +2864,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ + "indexmap 2.9.0", "itoa", "memchr", "ryu", @@ -2987,6 +3067,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3370,7 +3472,7 @@ dependencies = [ name = "turnkey_codegen" version = "0.1.0" dependencies = [ - "heck", + "heck 0.5.0", "prettyplease", "proc-macro2", "prost-build 0.12.6", @@ -3401,8 +3503,10 @@ dependencies = [ name = "turnkey_examples" version = "0.0.1" dependencies = [ + "coset", "dotenvy", "hex", + "passkey-types", "reqwest", "serde", "serde_json", @@ -3411,6 +3515,8 @@ dependencies = [ "turnkey_client", "turnkey_enclave_encrypt", "turnkey_proofs", + "turnkey_webauthn_stamper", + "url", ] [[package]] @@ -3441,12 +3547,50 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "turnkey_webauthn_stamper" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "coset", + "hex", + "passkey-authenticator", + "passkey-client", + "passkey-types", + "tokio", + "turnkey_client", + "url", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typeshare" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" +dependencies = [ + "quote", + "syn 2.0.100", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3477,13 +3621,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8dc1209..317d13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ members = [ "examples", # Contains utilities to work with Turnkey proofs (boot proofs, app proofs) "proofs", + # Crate to simulate stamps with WebAuthn credentials + "webauthn_stamper", ] resolver = "2" @@ -79,6 +81,12 @@ walkdir = { version = "2.5", default-features = false } # Environment and configuration dotenvy = { version = "0.15.0", default-features = false } +# Webauthn stamper +passkey-authenticator = "0.4" +passkey-client = "0.4" +passkey-types = "0.4" +url = "2.5.7" + # Development dependencies tempfile = { version = "3.19.1", default-features = false } signature = "2" @@ -92,3 +100,4 @@ turnkey_api_key_stamper = { path = "api_key_stamper", version = "0.5.0" } turnkey_client = { path = "client", version = "0.5.0" } turnkey_enclave_encrypt = { path = "enclave_encrypt", version = "0.5.0" } turnkey_proofs = { path = "proofs", version = "0.5.0" } +turnkey_webauthn_stamper = { path = "webauthn_stamper", version = "0.1.0" } \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml index ea51efe..b01900f 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,8 +7,10 @@ description = "Examples using the Turnkey Rust SDK" publish = false [dependencies] +coset.workspace = true dotenvy.workspace = true hex.workspace = true +passkey-types.workspace = true reqwest.workspace = true serde_json.workspace = true serde = { workspace = true, features = ["derive"] } @@ -17,3 +19,5 @@ turnkey_api_key_stamper.workspace = true turnkey_enclave_encrypt.workspace = true turnkey_client.workspace = true turnkey_proofs.workspace = true +turnkey_webauthn_stamper.workspace = true +url.workspace = true diff --git a/examples/src/bin/sub_organization_passkey.rs b/examples/src/bin/sub_organization_passkey.rs new file mode 100644 index 0000000..82fd5a6 --- /dev/null +++ b/examples/src/bin/sub_organization_passkey.rs @@ -0,0 +1,122 @@ +use coset::iana; +use passkey_types::{ + crypto::sha256, + webauthn::{ + PublicKeyCredentialParameters, PublicKeyCredentialType, PublicKeyCredentialUserEntity, + }, +}; +use std::error::Error; +use std::{env, vec}; +use turnkey_client::generated::immutable::activity::v1::DeleteSubOrganizationIntent; +use turnkey_client::generated::{ + immutable::common::v1::{AddressFormat, Curve, PathFormat}, + CreateSubOrganizationIntentV7, RootUserParamsV4, WalletAccountParams, WalletParams, +}; +use turnkey_client::generated::{AuthenticatorParamsV2, DeleteSubOrganizationRequest}; +use turnkey_examples::load_api_key_from_env; +use turnkey_webauthn_stamper::WebAuthnStamper; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let api_key = load_api_key_from_env()?; + + let origin_url = Url::parse("https://example.com")?; + let mut webauthn_stamper = WebAuthnStamper::new(origin_url); + let challenge = sha256(b"passkey example").to_vec(); + let credential_params = PublicKeyCredentialParameters { + ty: PublicKeyCredentialType::PublicKey, + alg: iana::Algorithm::ES256, + }; + let user_entity = PublicKeyCredentialUserEntity { + id: b"user".to_vec().into(), + name: "passkey user".into(), + display_name: "User".into(), + }; + let passkey = webauthn_stamper + .create_passkey(&challenge, credential_params, user_entity) + .await + .unwrap(); + + // Create the request body for our create_sub_organization request + let organization_id = + env::var("TURNKEY_ORGANIZATION_ID").expect("cannot load TURNKEY_ORGANIZATION_ID"); + + let client: turnkey_client::TurnkeyClient<_> = turnkey_client::TurnkeyClient::builder() + .api_key(api_key) + .build()?; + let intent: CreateSubOrganizationIntentV7 = CreateSubOrganizationIntentV7 { + sub_organization_name: "New sub-organization".to_string(), + root_users: vec![RootUserParamsV4 { + user_name: "Root User".to_string(), + api_keys: vec![], + user_email: None, + user_phone_number: None, + authenticators: vec![AuthenticatorParamsV2 { + authenticator_name: "Test software passkey".to_string(), + challenge: passkey.encoded_challenge, + attestation: Some(passkey.attestation), + }], + oauth_providers: vec![], + }], + root_quorum_threshold: 1, + wallet: Some(WalletParams { + wallet_name: "New wallet".to_string(), + accounts: vec![WalletAccountParams { + curve: Curve::Secp256k1, + path_format: PathFormat::Bip32, + path: "m/44'/60'/0'/0".to_string(), + address_format: AddressFormat::Ethereum, + }], + mnemonic_length: None, // Let that be the default + }), + // Defaults + disable_email_recovery: None, + disable_email_auth: None, + disable_sms_auth: None, + disable_otp_email_auth: None, + verification_token: None, + }; + + let create_res = client + .create_sub_organization(organization_id, client.current_timestamp(), intent) + .await?; + + assert_eq!(create_res.root_user_ids.len(), 1); + + println!( + "New sub-organization created: {} (root user ID: {})", + create_res.sub_organization_id, + create_res.root_user_ids.first().unwrap() + ); + + // delete the created sub-organization to clean up + let url = String::from("https://api.turnkey.com/public/v1/submit/delete_sub_organization"); + let request: DeleteSubOrganizationRequest = DeleteSubOrganizationRequest { + r#type: "ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION".to_string(), + timestamp_ms: client.current_timestamp().to_string(), + parameters: Some(DeleteSubOrganizationIntent { + delete_without_export: Some(true), + }), + organization_id: create_res.sub_organization_id, + }; + let request_str = serde_json::to_string(&request)?; + let stamp = webauthn_stamper + .stamp(request_str.as_bytes()) + .await + .unwrap(); + + let client = reqwest::Client::new(); + let res = client + .post(&url) + .header("X-Stamp-Webauthn", serde_json::to_string(&stamp)?) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&request)?) + .send() + .await?; + assert!(res.status().is_success()); + + println!("Deleted the created sub-organization"); + + Ok(()) +} diff --git a/webauthn_stamper/Cargo.toml b/webauthn_stamper/Cargo.toml new file mode 100644 index 0000000..4468141 --- /dev/null +++ b/webauthn_stamper/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "turnkey_webauthn_stamper" +version = "0.1.0" +edition = "2024" + +[dependencies] +async-trait = "0.1.89" +base64.workspace = true +hex.workspace = true +passkey-authenticator.workspace = true +passkey-client.workspace = true +passkey-types.workspace = true +turnkey_client.workspace = true +url.workspace = true + +[dev-dependencies] +coset.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/webauthn_stamper/src/lib.rs b/webauthn_stamper/src/lib.rs new file mode 100644 index 0000000..276777b --- /dev/null +++ b/webauthn_stamper/src/lib.rs @@ -0,0 +1,229 @@ +use async_trait::async_trait; +use base64::Engine; +use base64::prelude::BASE64_URL_SAFE_NO_PAD; +use passkey_authenticator::{Authenticator, UserCheck, UserValidationMethod}; +use passkey_client::{Client, DefaultClientData, WebauthnError}; +use passkey_types::{crypto::sha256, ctap2::*, webauthn::*}; +use turnkey_client::generated::Attestation; +use turnkey_client::generated::external::webauthn::v1::WebAuthnStamp; +use turnkey_client::generated::immutable::webauthn::v1::AuthenticatorTransport; +use url::Url; + +/// A user-verification stub that always approves +pub struct AutoUserValidation; +#[async_trait] +impl UserValidationMethod for AutoUserValidation { + type PasskeyItem = passkey_types::Passkey; + + async fn check_user<'a>( + &self, + _hint: Option<&'a Self::PasskeyItem>, + presence: bool, + verification: bool, + ) -> Result { + Ok(UserCheck { + presence, + verification, + }) + } + + fn is_verification_enabled(&self) -> Option { + Some(true) + } + fn is_presence_enabled(&self) -> bool { + true + } +} + +pub struct WebAuthnStamper { + pub aaguid: Aaguid, + pub origin_url: Url, + pub store: Option, +} + +#[derive(Debug)] +pub struct Passkey { + pub encoded_challenge: String, + pub attestation: Attestation, +} + +impl WebAuthnStamper { + pub fn new(origin_url: Url) -> Self { + let aaguid = Aaguid::new_empty(); + + WebAuthnStamper { + aaguid, + origin_url, + store: None, + } + } + + pub async fn create_passkey( + &mut self, + challenge_bytes: &[u8], + credential_params: PublicKeyCredentialParameters, + user_entity: PublicKeyCredentialUserEntity, + ) -> Result { + let authenticator = self.get_authenticator(); + let mut client = Client::new(authenticator); + + let credential_request = CredentialCreationOptions { + public_key: PublicKeyCredentialCreationOptions { + rp: PublicKeyCredentialRpEntity { + id: None, + name: user_entity.clone().name, + }, + user: user_entity, + challenge: challenge_bytes.into(), + pub_key_cred_params: vec![credential_params], + timeout: None, + exclude_credentials: None, + authenticator_selection: None, + hints: None, + attestation: AttestationConveyancePreference::None, + attestation_formats: None, + extensions: None, + }, + }; + + let credential = client + .register(&self.origin_url, credential_request, DefaultClientData) + .await?; + + self.store = client.authenticator().store().clone(); + + Ok(Passkey { + encoded_challenge: BASE64_URL_SAFE_NO_PAD.encode(challenge_bytes), + attestation: Attestation { + credential_id: BASE64_URL_SAFE_NO_PAD.encode(credential.raw_id.as_slice()), + client_data_json: BASE64_URL_SAFE_NO_PAD + .encode(credential.response.client_data_json.as_slice()), + attestation_object: BASE64_URL_SAFE_NO_PAD + .encode(credential.response.attestation_object.as_slice()), + transports: credential + .response + .transports + .unwrap_or_default() + .iter() + .map(|t| match t { + passkey_types::webauthn::AuthenticatorTransport::Usb => { + AuthenticatorTransport::Usb + } + passkey_types::webauthn::AuthenticatorTransport::Nfc => { + AuthenticatorTransport::Nfc + } + passkey_types::webauthn::AuthenticatorTransport::Ble => { + AuthenticatorTransport::Ble + } + passkey_types::webauthn::AuthenticatorTransport::Hybrid => { + AuthenticatorTransport::Hybrid + } + passkey_types::webauthn::AuthenticatorTransport::Internal => { + AuthenticatorTransport::Internal + } + }) + .collect(), + }, + }) + } + + pub async fn stamp(&self, body: &[u8]) -> Result { + let request_digest = sha256(body); + let hex_digest = hex::encode(request_digest); + let challenge_bytes = hex_digest.as_bytes().to_vec(); + + let authenticator = self.get_authenticator(); + let mut client = Client::new(authenticator); + let credential_request = CredentialRequestOptions { + public_key: PublicKeyCredentialRequestOptions { + challenge: challenge_bytes.into(), + timeout: None, + rp_id: None, + allow_credentials: None, + user_verification: UserVerificationRequirement::default(), + hints: None, + attestation: AttestationConveyancePreference::None, + attestation_formats: None, + extensions: None, + }, + }; + + let auth_cred = client + .authenticate(&self.origin_url, credential_request, DefaultClientData) + .await?; + + let stamp = WebAuthnStamp { + credential_id: BASE64_URL_SAFE_NO_PAD.encode(auth_cred.raw_id.as_slice()), + client_data_json: BASE64_URL_SAFE_NO_PAD + .encode(auth_cred.response.client_data_json.as_slice()), + authenticator_data: BASE64_URL_SAFE_NO_PAD + .encode(auth_cred.response.authenticator_data.as_slice()), + signature: BASE64_URL_SAFE_NO_PAD.encode(auth_cred.response.signature.as_slice()), + }; + + Ok(stamp) + } + + fn get_authenticator( + &self, + ) -> Authenticator, AutoUserValidation> { + Authenticator::new(self.aaguid, self.store.clone(), AutoUserValidation) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use coset::iana; + use passkey_types::crypto::sha256; + + #[tokio::test] + async fn test_create_passkey() { + let origin_url = Url::parse("https://example.com").unwrap(); + let mut stamper = WebAuthnStamper::new(origin_url); + let challenge = sha256(b"test challenge").to_vec(); + let credential_params = PublicKeyCredentialParameters { + ty: PublicKeyCredentialType::PublicKey, + alg: iana::Algorithm::ES256, + }; + let user_entity = PublicKeyCredentialUserEntity { + id: b"user123".to_vec().into(), + name: "user123".into(), + display_name: "User 123".into(), + }; + + let created_cred = stamper + .create_passkey(&challenge, credential_params, user_entity) + .await + .unwrap(); + + assert!(created_cred.attestation.credential_id.len() > 0); + } + + #[tokio::test] + async fn test_authenticate() { + let origin_url = Url::parse("https://example.com").unwrap(); + let mut stamper = WebAuthnStamper::new(origin_url); + let challenge = sha256(b"test challenge").to_vec(); + let credential_params = PublicKeyCredentialParameters { + ty: PublicKeyCredentialType::PublicKey, + alg: iana::Algorithm::ES256, + }; + let user_entity = PublicKeyCredentialUserEntity { + id: b"user123".to_vec().into(), + name: "user123".into(), + display_name: "User 123".into(), + }; + let created_cred = stamper + .create_passkey(&challenge, credential_params, user_entity) + .await + .unwrap(); + let authenticated_cred = stamper.stamp(&challenge).await.unwrap(); + + assert!(authenticated_cred.credential_id.len() > 0); + assert_eq!( + authenticated_cred.credential_id, + created_cred.attestation.credential_id + ); + } +}