diff --git a/Cargo.lock b/Cargo.lock index cd3006b..7bdb920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -929,6 +929,12 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -993,6 +999,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dyn-clonable" version = "0.9.0" @@ -1222,6 +1234,30 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "frame-benchmarking" version = "28.0.0" @@ -1644,6 +1680,16 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -1976,6 +2022,33 @@ dependencies = [ "adler2", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nalgebra" version = "0.32.6" @@ -2009,6 +2082,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2224,10 +2303,15 @@ dependencies = [ "fc-traits-authn", "frame-support", "frame-system", + "futures", "pallet-balances", "parity-scale-codec", + "passkey-authenticator", + "passkey-client", + "passkey-types", "scale-info", "sp-io", + "url", "verifier", ] @@ -2240,11 +2324,31 @@ dependencies = [ "async-trait", "coset", "log", + "mockall", "p256", "passkey-types", "rand", ] +[[package]] +name = "passkey-client" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b2f49d3758a7f3763daa87b3d10c235f093eef2f331de2bf6a1b7df17cb294" +dependencies = [ + "ciborium", + "coset", + "idna", + "mockall", + "passkey-authenticator", + "passkey-types", + "public-suffix", + "serde", + "serde_json", + "typeshare", + "url", +] + [[package]] name = "passkey-types" version = "0.3.0" @@ -2258,6 +2362,7 @@ dependencies = [ "getrandom", "hmac 0.12.1", "indexmap", + "p256", "rand", "serde", "serde_json", @@ -2303,6 +2408,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2377,6 +2488,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.2.22" @@ -2474,6 +2615,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "public-suffix" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a59553bc595dc1514e7d713e6167cf1c4d68ef6fcc2d10ad834a97a1ca9bc4" + [[package]] name = "quote" version = "1.0.37" @@ -3626,6 +3773,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.64" @@ -3898,6 +4051,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -3919,6 +4078,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7cec7b1..1c2ebbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,22 @@ repository = "https://github.com/virto-network/webauthn" [workspace.dependencies] # WebAuthN Verifier coset = { version = "0.3.0", default-features = false } +futures = { version = "0.3.31", default-features = false, features = [ + "executor", +] } p256 = { version = "0.13.2", default-features = false } -passkey-authenticator = { version = "0.3.0", default-features = false } +passkey-authenticator = { version = "0.3.0", default-features = false, features = [ + "testable", +] } +passkey-client = { version = "0.3.0", default-features = false, features = [ + "testable", +] } +passkey-types = { version = "0.3.0", default-features = false, features = [ + "testable", +] } sha2 = { version = "0.10.8", default-features = false } rand = "0.8.5" +url = "2.5.2" # FRAME codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [ diff --git a/pass-webauthn/Cargo.toml b/pass-webauthn/Cargo.toml index 6632422..869d1cb 100644 --- a/pass-webauthn/Cargo.toml +++ b/pass-webauthn/Cargo.toml @@ -16,9 +16,14 @@ verifier.workspace = true [dev-dependencies] frame-system.workspace = true +futures.workspace = true pallet-balances.workspace = true pallet-pass.workspace = true +passkey-authenticator.workspace = true +passkey-client.workspace = true +passkey-types.workspace = true sp-io.workspace = true +url.workspace = true [features] default = ["std", "runtime"] @@ -33,6 +38,7 @@ std = [ "codec/std", "frame-support/std", "frame-system/std", + "futures/std", "pallet-balances/std", "pallet-pass/std", "scale-info/std", diff --git a/pass-webauthn/src/tests.rs b/pass-webauthn/src/tests.rs index 749169d..883326b 100644 --- a/pass-webauthn/src/tests.rs +++ b/pass-webauthn/src/tests.rs @@ -3,7 +3,7 @@ use frame_support::{ assert_noop, assert_ok, derive_impl, parameter_types, sp_runtime::{str_array as s, traits::Hash}, - traits::ConstU64, + traits::{ConstU64, Get}, PalletId, }; use frame_system::{pallet_prelude::BlockNumberFor, Config, EnsureRootWithSuccess}; @@ -54,13 +54,15 @@ parameter_types! { pub NeverPays: Option> = None; } +type AuthorityId = AuthorityFromPalletId; + pub struct BlockChallenger; impl Challenger for BlockChallenger { type Context = BlockNumberFor; - fn generate(_: &Self::Context) -> traits_authn::Challenge { - ::Hashing::hash(&System::block_number().to_le_bytes()).0 + fn generate(ctx: &Self::Context) -> traits_authn::Challenge { + ::Hashing::hash(&ctx.to_le_bytes()).0 } } @@ -68,7 +70,7 @@ impl pallet_pass::Config for Test { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; type Currency = Balances; - type Authenticator = Authenticator>; + type Authenticator = Authenticator; type PalletsOrigin = OriginCaller; type PalletId = PassPalletId; type MaxSessionDuration = ConstU64<10>; @@ -86,16 +88,73 @@ fn new_test_ext() -> sp_io::TestExternalities { const USER: HashedUserId = s("the_user"); +fn build_attesttation_fields(ctx: &BlockNumberFor) -> (Vec, Vec, Vec, Vec) { + use futures::executor::block_on; + use passkey_authenticator::{ + public_key_der_from_cose_key, Authenticator, MockUserValidationMethod, + }; + use passkey_client::{Client, DefaultClientData}; + use passkey_types::{ + ctap2::Aaguid, + webauthn::{ + AttestationConveyancePreference, CredentialRequestOptions, + PublicKeyCredentialRequestOptions, UserVerificationRequirement, + }, + Passkey, + }; + use url::Url; + + let aaguid = Aaguid::new_empty(); + 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(); + let store = Some(key.clone()); + + let authenticator = Authenticator::new(aaguid, store, MockUserValidationMethod::new()); + 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), + allow_credentials: None, + user_verification: UserVerificationRequirement::default(), + hints: None, + attestation: AttestationConveyancePreference::None, + attestation_formats: None, + extensions: None, + }, + }; + + let authenticated_request = block_on(client.authenticate(&origin, request, DefaultClientData)) + .expect("authenticate works"); + + let authenticator_data = authenticated_request.response.authenticator_data; + let client_data = authenticated_request.response.client_data_json; + let public_key = public_key_der_from_cose_key(&key.key).expect("key conversion works"); + let signature = authenticated_request.response.signature; + + ( + authenticator_data.to_vec(), + client_data.to_vec(), + public_key.to_vec(), + signature.to_vec(), + ) +} + #[test] fn registration_fails_if_attestation_is_invalid() { new_test_ext().execute_with(|| { - // TODO: Fill with garbage data or incorrect signature (whatever works best) + 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); + assert_noop!( - Pass::register( - RuntimeOrigin::root(), - USER, - Attestation::new(b"".to_vec(), b"".to_vec(), b"".to_vec(), b"".to_vec()) - ), + Pass::register(RuntimeOrigin::root(), USER, attestation), pallet_pass::Error::::DeviceAttestationInvalid, ); }) @@ -104,11 +163,10 @@ fn registration_fails_if_attestation_is_invalid() { #[test] fn registration_works_if_attestation_is_valid() { new_test_ext().execute_with(|| { - // TODO: Fill with valid data and signature - assert_ok!(Pass::register( - RuntimeOrigin::root(), - USER, - Attestation::new(b"".to_vec(), b"".to_vec(), b"".to_vec(), b"".to_vec()) - )); + let (authenticator_data, client_data, public_key, signature) = + build_attesttation_fields(&System::block_number()); + let attestation = Attestation::new(authenticator_data, client_data, public_key, signature); + + assert_ok!(Pass::register(RuntimeOrigin::root(), USER, attestation)); }) }