diff --git a/Cargo.lock b/Cargo.lock index 7bdb920..61ae576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2310,6 +2310,8 @@ dependencies = [ "passkey-client", "passkey-types", "scale-info", + "serde_json", + "simple-base64", "sp-io", "url", "verifier", @@ -3086,6 +3088,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simple-base64" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6385ef05b7bbfddaa8bf6306d059adac087990d659576c73b7da802d9a6ce91f" + [[package]] name = "simple-mermaid" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1c2ebbc..89af21d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ repository = "https://github.com/virto-network/webauthn" [workspace.dependencies] # WebAuthN Verifier +base64 = { package = "simple-base64", version = "0.23.2", default-features = false } coset = { version = "0.3.0", default-features = false } futures = { version = "0.3.31", default-features = false, features = [ "executor", @@ -21,8 +22,9 @@ passkey-client = { version = "0.3.0", default-features = false, features = [ passkey-types = { version = "0.3.0", default-features = false, features = [ "testable", ] } -sha2 = { version = "0.10.8", default-features = false } rand = "0.8.5" +sha2 = { version = "0.10.8", default-features = false } +serde_json = { version = "1.0.128", default-features = false } url = "2.5.2" # FRAME diff --git a/pass-webauthn/Cargo.toml b/pass-webauthn/Cargo.toml index 869d1cb..a8ad3f3 100644 --- a/pass-webauthn/Cargo.toml +++ b/pass-webauthn/Cargo.toml @@ -7,10 +7,12 @@ repository.workspace = true version = "0.1.0" [dependencies] +base64.workspace = true codec.workspace = true frame-support.workspace = true frame-support.optional = true scale-info.workspace = true +serde_json.workspace = true traits-authn.workspace = true verifier.workspace = true @@ -35,6 +37,7 @@ runtime-benchmarks = [ "pallet-pass/runtime-benchmarks", ] std = [ + "base64/std", "codec/std", "frame-support/std", "frame-system/std", @@ -42,6 +45,7 @@ std = [ "pallet-balances/std", "pallet-pass/std", "scale-info/std", + "serde_json/std", "sp-io/std", "traits-authn/std", "verifier/std", diff --git a/pass-webauthn/src/impls.rs b/pass-webauthn/src/impls.rs index 5d6ff8f..96decc3 100644 --- a/pass-webauthn/src/impls.rs +++ b/pass-webauthn/src/impls.rs @@ -1,6 +1,8 @@ use core::marker::PhantomData; -use frame_support::Parameter; +use codec::Decode; +use frame_support::{sp_runtime::traits::TrailingZeroInput, Parameter}; +use serde_json::Value; use traits_authn::{ AuthorityId, Challenge, Challenger, DeviceChallengeResponse, DeviceId, HashedUserId, UserChallengeResponse, @@ -33,7 +35,16 @@ where } fn challenge(&self) -> Challenge { - todo!("Extract `challenge`, format into `Challenge` format (that is, [u8; 32])"); + || -> Result { + let client_data_json = + serde_json::from_slice::(&self.client_data).map_err(|_| ())?; + + let challenge_str = + base64::decode(client_data_json["challenge"].as_str().ok_or(())?.as_bytes()) + .map_err(|_| ())?; + Decode::decode(&mut TrailingZeroInput::new(&challenge_str)).map_err(|_| ())? + }() + .unwrap_or_default() } } @@ -57,7 +68,18 @@ where } fn authority(&self) -> AuthorityId { - todo!("Extract `rp_id`, format into `AuthorityId` format (that is, [u8; 32])"); + || -> Result { + let client_data_json = + serde_json::from_slice::(&self.client_data).map_err(|_| ())?; + + let origin = client_data_json["origin"].as_str().ok_or(())?; + let (_, domain) = origin.split_once("//").ok_or(())?; + let (rp_id_subdomain, _) = domain.split_once(".").ok_or(())?; + + Decode::decode(&mut TrailingZeroInput::new(rp_id_subdomain.as_bytes())) + .map_err(|_| ())? + }() + .unwrap_or_default() } fn device_id(&self) -> &DeviceId { diff --git a/pass-webauthn/src/tests.rs b/pass-webauthn/src/tests.rs index 883326b..526831f 100644 --- a/pass-webauthn/src/tests.rs +++ b/pass-webauthn/src/tests.rs @@ -3,13 +3,13 @@ use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, sp_runtime::{str_array as s, traits::Hash}, - traits::{ConstU64, Get}, + traits::ConstU64, PalletId, }; use frame_system::{pallet_prelude::BlockNumberFor, Config, EnsureRootWithSuccess}; use traits_authn::{util::AuthorityFromPalletId, Challenger, HashedUserId}; -use crate::{Attestation, Authenticator}; +use crate::{Attestation, Authenticator, Credential}; #[frame_support::runtime] pub mod runtime { @@ -50,7 +50,7 @@ impl pallet_balances::Config for Test { } parameter_types! { - pub PassPalletId: PalletId = PalletId(*b"pass/web"); + pub PassPalletId: PalletId = PalletId(*b"pass_web"); pub NeverPays: Option> = None; } @@ -76,6 +76,27 @@ impl pallet_pass::Config for Test { type MaxSessionDuration = ConstU64<10>; type RegisterOrigin = EnsureRootWithSuccess; type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = Helper; +} + +#[cfg(feature = "runtime-benchmarks")] +pub struct Helper; +#[cfg(feature = "runtime-benchmarks")] +impl pallet_pass::BenchmarkHelper for Helper { + fn register_origin() -> frame_system::pallet_prelude::OriginFor { + RuntimeOrigin::root() + } + + fn device_attestation(_: traits_authn::DeviceId) -> pallet_pass::DeviceAttestationOf { + let (a, b, c, d) = build_attesttation_fields(&System::block_number()); + Attestation::new(a, b, c, d) + } + + fn credential(_: HashedUserId) -> pallet_pass::CredentialOf { + let (a, b, c, d) = build_attesttation_fields(&System::block_number()); + Credential::new(a, b, c, d) + } } fn new_test_ext() -> sp_io::TestExternalities { @@ -108,18 +129,19 @@ fn build_attesttation_fields(ctx: &BlockNumberFor) -> (Vec, Vec, V let rp_id = String::from_utf8(PassPalletId::get().0.to_vec()) .expect("converting from ascii to utf-8 is guaranteed; qed"); let origin = - Url::parse(&format!("urn://blockchain/{rp_id}")).expect("urn parses as a valid URL"); - let key = Passkey::mock(rp_id.clone()).build(); + Url::parse(&format!("https://{rp_id}.pallet-pass.int")).expect("urn parses as a valid URL"); + let key = Passkey::mock(rp_id).build(); let store = Some(key.clone()); - let authenticator = Authenticator::new(aaguid, store, MockUserValidationMethod::new()); + let authenticator = + Authenticator::new(aaguid, store, MockUserValidationMethod::verified_user(1)); let mut client = Client::new(authenticator); let request = CredentialRequestOptions { public_key: PublicKeyCredentialRequestOptions { challenge: BlockChallenger::generate(ctx).as_slice().into(), timeout: None, - rp_id: Some(rp_id), + rp_id: None, allow_credentials: None, user_verification: UserVerificationRequirement::default(), hints: None, @@ -151,7 +173,23 @@ fn registration_fails_if_attestation_is_invalid() { let (authenticator_data, client_data, public_key, signature) = build_attesttation_fields(&System::block_number()); let signature = [signature, b"Whoops!".to_vec()].concat(); - let attestation = Attestation::new(authenticator_data, client_data, public_key, signature); + + use passkey_types::ctap2::AuthenticatorData; + let raw_authenticator_data = AuthenticatorData::from_slice(&authenticator_data) + .expect("this conversion works both ways"); + + println!( + "authenticator_data = {:?}\nclient_data_json = {}", + &raw_authenticator_data, + &String::from_utf8(client_data.clone()).expect("converting json works") + ); + + let attestation = Attestation::new( + authenticator_data.to_vec(), + client_data, + public_key, + signature, + ); assert_noop!( Pass::register(RuntimeOrigin::root(), USER, attestation),