diff --git a/Cargo.lock b/Cargo.lock index 58b064b24..c86ae0b1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1167,6 +1167,16 @@ dependencies = [ "syn", ] +[[package]] +name = "devutil-auth-ticket" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "primitives-auth-ticket", + "robonode-crypto", +] + [[package]] name = "digest" version = "0.8.1" @@ -2205,6 +2215,7 @@ dependencies = [ "hex-literal", "humanode-rpc", "humanode-runtime", + "pallet-bioauth", "parity-scale-codec", "qr2term", "reqwest", diff --git a/crates/devutil-auth-ticket/Cargo.toml b/crates/devutil-auth-ticket/Cargo.toml new file mode 100644 index 000000000..55e324d1f --- /dev/null +++ b/crates/devutil-auth-ticket/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "devutil-auth-ticket" +version = "0.1.0" +edition = "2018" +authors = ["Humanode Team "] +publish = false + +[dependencies] +primitives-auth-ticket = { version = "0.1", path = "../primitives-auth-ticket" } +robonode-crypto = { version = "0.1", path = "../robonode-crypto" } + +anyhow = "1" +hex = "0.4" diff --git a/crates/devutil-auth-ticket/src/lib.rs b/crates/devutil-auth-ticket/src/lib.rs new file mode 100644 index 000000000..ffe6d7fee --- /dev/null +++ b/crates/devutil-auth-ticket/src/lib.rs @@ -0,0 +1,45 @@ +use std::convert::TryFrom; + +pub use hex::{decode, encode}; +pub use primitives_auth_ticket::{AuthTicket, OpaqueAuthTicket}; + +use robonode_crypto::{ed25519_dalek::Signer, Keypair}; + +pub struct Input { + pub robonode_keypair: Vec, + pub auth_ticket: AuthTicket, +} + +pub struct Output { + pub auth_ticket: Vec, + pub robonode_signature: Vec, + pub robonode_public_key: Vec, +} + +pub fn make(input: Input) -> Result { + let Input { + auth_ticket, + robonode_keypair, + } = input; + + let robonode_keypair = Keypair::from_bytes(&robonode_keypair)?; + + let opaque_auth_ticket = OpaqueAuthTicket::from(&auth_ticket); + + let robonode_signature = robonode_keypair + .sign(opaque_auth_ticket.as_ref()) + .to_bytes(); + + assert!(robonode_keypair + .verify( + opaque_auth_ticket.as_ref(), + &robonode_crypto::Signature::try_from(&robonode_signature[..]).unwrap() + ) + .is_ok()); + + Ok(Output { + auth_ticket: opaque_auth_ticket.into(), + robonode_signature: robonode_signature.into(), + robonode_public_key: robonode_keypair.public.as_bytes()[..].into(), + }) +} diff --git a/crates/devutil-auth-ticket/src/main.rs b/crates/devutil-auth-ticket/src/main.rs new file mode 100644 index 000000000..dd87274b1 --- /dev/null +++ b/crates/devutil-auth-ticket/src/main.rs @@ -0,0 +1,33 @@ +use devutil_auth_ticket::*; + +fn read_hex_env(key: &'static str) -> Vec { + let val = std::env::var(key).unwrap(); + decode(val).unwrap() +} + +fn main() { + let robonode_keypair = read_hex_env("ROBONODE_KEYPAIR"); + let public_key = read_hex_env("AUTH_TICKET_PUBLIC_KEY"); + let authentication_nonce = read_hex_env("AUTH_TICKET_AUTHENTICATION_NONCE"); + + let auth_ticket = AuthTicket { + public_key, + authentication_nonce, + }; + + let output = make(Input { + robonode_keypair, + auth_ticket, + }) + .unwrap(); + + print!( + "{}\n{}\n{}\n\n{:?}\n{:?}\n{:?}\n", + encode(output.auth_ticket.clone()), + encode(output.robonode_signature.clone()), + encode(output.robonode_public_key.clone()), + output.auth_ticket, + output.robonode_signature, + output.robonode_public_key, + ); +} diff --git a/crates/humanode-peer/Cargo.toml b/crates/humanode-peer/Cargo.toml index 843521890..a91dea7a6 100644 --- a/crates/humanode-peer/Cargo.toml +++ b/crates/humanode-peer/Cargo.toml @@ -9,6 +9,7 @@ publish = false bioauth-flow = { version = "0.1", path = "../bioauth-flow" } humanode-rpc = { version = "0.1", path = "../humanode-rpc" } humanode-runtime = { version = "0.1", path = "../humanode-runtime" } +pallet-bioauth = { version = "0.1", path = "../pallet-bioauth" } robonode-client = { version = "0.1", path = "../robonode-client" } async-trait = "0.1" diff --git a/crates/humanode-peer/src/chain_spec.rs b/crates/humanode-peer/src/chain_spec.rs index 547613992..62d569c80 100644 --- a/crates/humanode-peer/src/chain_spec.rs +++ b/crates/humanode-peer/src/chain_spec.rs @@ -44,7 +44,7 @@ pub fn local_testnet_config() -> Result { WASM_BINARY.ok_or_else(|| "Development wasm binary not available".to_string())?; let robonode_public_key = RobonodePublicKeyWrapper::from_bytes( - &hex!("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")[..], + &hex!("5dde03934419252d13336e5a5881f5b1ef9ea47084538eb229f86349e7f394ab")[..], ) .map_err(|err| format!("{:?}", err))?; diff --git a/crates/humanode-peer/src/service.rs b/crates/humanode-peer/src/service.rs index a30ac4505..282c75881 100644 --- a/crates/humanode-peer/src/service.rs +++ b/crates/humanode-peer/src/service.rs @@ -120,7 +120,7 @@ pub async fn new_full(config: Configuration) -> Result Result Result break v, - Err(error) => { - error!(message = "bioauth flow - authentication failure", ?error); - } + let authenticate_response = loop { + let result = flow + .authenticate(crate::validator_key::FakeTodo("TODO")) + .await; + match result { + Ok(v) => break v, + Err(error) => { + error!(message = "bioauth flow - authentication failure", ?error); + } + }; }; - }; - info!("bioauth flow - authentication complete"); + info!("bioauth flow - authentication complete"); - info!(message = "We've obtained an auth ticket", auth_ticket = ?authenticate_response.auth_ticket); - }); + info!(message = "We've obtained an auth ticket", auth_ticket = ?authenticate_response.auth_ticket); + + let authenticate = pallet_bioauth::Authenticate { + ticket: authenticate_response.auth_ticket.into(), + ticket_signature: authenticate_response.auth_ticket_signature.into(), + }; + let call = pallet_bioauth::Call::authenticate(authenticate); + + let ext = humanode_runtime::UncheckedExtrinsic::new_unsigned(call.into()); + + let at = client.chain_info().best_hash; + transaction_pool + .pool() + .submit_and_watch( + &sp_runtime::generic::BlockId::Hash(at), + sp_runtime::transaction_validity::TransactionSource::Local, + ext.into(), + ) + .await + .unwrap(); + }) + }; task_manager .spawn_handle() diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index a5b46d295..4ca1702a5 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -319,7 +319,7 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, TransactionPayment: pallet_transaction_payment::{Pallet, Storage}, Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event}, - PalletBioauth: pallet_bioauth::{Pallet, Config, Call, Storage, Event}, + PalletBioauth: pallet_bioauth::{Pallet, Config, Call, Storage, Event, ValidateUnsigned}, } ); diff --git a/crates/pallet-bioauth/src/lib.rs b/crates/pallet-bioauth/src/lib.rs index 1218b197d..1fcdb2036 100644 --- a/crates/pallet-bioauth/src/lib.rs +++ b/crates/pallet-bioauth/src/lib.rs @@ -11,7 +11,7 @@ pub use pallet::*; use serde::{Deserialize, Serialize}; use sp_runtime::{ traits::{DispatchInfoOf, Dispatchable, SignedExtension}, - transaction_validity::{InvalidTransaction, TransactionValidity, TransactionValidityError}, + transaction_validity::{TransactionValidity, TransactionValidityError}, }; use sp_std::fmt::Debug; use sp_std::marker::PhantomData; @@ -146,8 +146,8 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Event documentation should end with an array that provides descriptive names for event - /// parameters. [something, who] - AuthTicketStored(StoredAuthTicket, T::AccountId), + /// parameters. [stored_auth_ticket] + AuthTicketStored(StoredAuthTicket), } /// Possible error conditions during `authenticate` call processing. @@ -155,7 +155,7 @@ pub mod pallet { pub enum Error { /// The robonode public key is not at the chain state. RobonodePublicKeyIsAbsent, - /// We were unable to validate the signature, i.e. it is uknclear whether it is valid or + /// We were unable to validate the signature, i.e. it is unclear whether it is valid or /// not. UnableToValidateAuthTicketSignature, /// The signature for the auth ticket is invalid. @@ -203,7 +203,7 @@ pub mod pallet { impl Pallet { #[pallet::weight(10_000 + T::DbWeight::get().writes(1))] pub fn authenticate(origin: OriginFor, req: Authenticate) -> DispatchResult { - let who = ensure_signed(origin)?; + ensure_none(origin)?; let stored_auth_ticket = Self::extract_auth_ticket_checked(req)?; let event_stored_auth_ticket = stored_auth_ticket.clone(); @@ -226,7 +226,7 @@ pub mod pallet { })?; // Emit an event. - Self::deposit_event(Event::AuthTicketStored(event_stored_auth_ticket, who)); + Self::deposit_event(Event::AuthTicketStored(event_stored_auth_ticket)); Ok(()) } @@ -253,13 +253,65 @@ pub mod pallet { Ok(auth_ticket.into()) } + + pub fn check_tx(call: &Call) -> TransactionValidity { + let transaction = match call { + Call::authenticate(ref transaction) => transaction, + // Deny all unknown transactions. + _ => { + // The only supported transaction by this pallet is `authenticate`, so anything + // else is illegal. + return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)); + } + }; + + let stored_auth_ticket = Self::extract_auth_ticket_checked(transaction.clone()) + .map_err(|error| { + frame_support::sp_tracing::error!( + message = "Auth Ticket could not be extracted", + ?error + ); + // Use custom code 's' for "signature" error. + TransactionValidityError::Invalid(InvalidTransaction::Custom(b's')) + })?; + + let list = StoredAuthTickets::::get(); + + validate_authentication_attempt(&list, &stored_auth_ticket).map_err(|_e| { + // Use custom code 'c' for "conflict" error. + TransactionValidityError::Invalid(InvalidTransaction::Custom(b'c')) + })?; + + // We must use non-default [`TransactionValidity`] here. + ValidTransaction::with_tag_prefix("bioauth") + // Apparently tags are required for the tx pool to build a chain of transactions; + // in our case, we the structure of the [`StoredAuthTickets`] is supposed to be + // unordered, and act like a CRDT. + // TODO: ensure we have the unordered (CRDT) semantics for the [`authenticate`] txs. + .and_provides(stored_auth_ticket) + .priority(50) + .longevity(1) + .propagate(true) + .build() + } } -} -// The following section implements the `SignedExtension` trait -// for the `CheckBioauthTx` type. + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned( + _source: TransactionSource, + _call: &Self::Call, + ) -> TransactionValidity { + // Allow all transactions from this pallet, and delegate the actual logic to the + // SignedExtension implementation logic. + // See https://github.com/paritytech/substrate/issues/3419 + Ok(Default::default()) + } + } +} -/// The `CheckBioauthTx` struct. +/// Checks the validity of the unsigned [`Call::authenticate`] tx. #[derive(Encode, Decode, Clone, Eq, PartialEq, Default)] pub struct CheckBioauthTx(PhantomData); @@ -276,7 +328,6 @@ impl Debug for CheckBioauthTx { } } -/// Implementation of the `SignedExtension` trait for the `CheckBioauthTx` struct. impl SignedExtension for CheckBioauthTx where T::Call: Dispatchable, @@ -300,23 +351,19 @@ where _len: usize, ) -> TransactionValidity { let _account_id = who; - - // check for `authenticate` match call.is_sub_type() { - Some(Call::authenticate(ref transaction)) => { - // We need to call our validate_bioauth from pallet - let stored_auth_ticket = Pallet::::extract_auth_ticket_checked( - transaction.clone(), - ) - .map_err(|_e| TransactionValidityError::Invalid(InvalidTransaction::Call))?; - - let list = StoredAuthTickets::::get(); - - validate_authentication_attempt(&list, &stored_auth_ticket) - .map_err(|_e| TransactionValidityError::Invalid(InvalidTransaction::Call))?; + Some(call) => Pallet::::check_tx(call), + _ => Ok(Default::default()), + } + } - Ok(Default::default()) - } + fn validate_unsigned( + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + match call.is_sub_type() { + Some(call) => Pallet::::check_tx(call), _ => Ok(Default::default()), } } diff --git a/crates/pallet-bioauth/src/tests.rs b/crates/pallet-bioauth/src/tests.rs index 7bd4a6073..af796eec0 100644 --- a/crates/pallet-bioauth/src/tests.rs +++ b/crates/pallet-bioauth/src/tests.rs @@ -1,8 +1,11 @@ +use std::convert::TryInto; + use crate as pallet_bioauth; use crate::*; use crate::{mock::*, Error}; -use frame_support::pallet_prelude::ValidTransaction; -use frame_support::{assert_noop, assert_ok, weights::DispatchInfo}; +use frame_support::pallet_prelude::*; +use frame_support::{assert_noop, assert_ok}; +use primitives_auth_ticket::{AuthTicket, OpaqueAuthTicket}; pub fn make_input(public_key: &[u8], nonce: &[u8], signature: &[u8]) -> crate::Authenticate { let ticket = @@ -22,7 +25,7 @@ fn it_permits_authnetication_with_an_empty_state() { // Prepare test input. let input = make_input(b"qwe", b"rty", b"should_be_valid"); - assert_ok!(Bioauth::authenticate(Origin::signed(1), input)); + assert_ok!(Bioauth::authenticate(Origin::none(), input)); assert_eq!( Bioauth::stored_auth_tickets(), vec![crate::StoredAuthTicket { @@ -40,7 +43,7 @@ fn it_denies_authnetication_with_invalid_signature() { let input = make_input(b"qwe", b"rty", b"invalid"); assert_noop!( - Bioauth::authenticate(Origin::signed(1), input), + Bioauth::authenticate(Origin::none(), input), Error::::AuthTicketSignatureInvalid ); }); @@ -51,14 +54,14 @@ fn it_denies_authnetication_with_conlicting_nonce() { new_test_ext().execute_with(|| { // Prepare the test precondition. let precondition_input = make_input(b"pk1", b"conflict!", b"should_be_valid"); - assert_ok!(Bioauth::authenticate(Origin::signed(1), precondition_input)); + assert_ok!(Bioauth::authenticate(Origin::none(), precondition_input)); // Prepare test input. let input = make_input(b"pk2", b"conflict!", b"should_be_valid"); // Ensure the expected error is thrown when no value is present. assert_noop!( - Bioauth::authenticate(Origin::signed(1), input), + Bioauth::authenticate(Origin::none(), input), Error::::NonceAlreadyUsed, ); }); @@ -69,14 +72,14 @@ fn it_denies_authnetication_with_conlicting_public_keys() { new_test_ext().execute_with(|| { // Prepare the test precondition. let precondition_input = make_input(b"conflict!", b"nonce1", b"should_be_valid"); - assert_ok!(Bioauth::authenticate(Origin::signed(1), precondition_input)); + assert_ok!(Bioauth::authenticate(Origin::none(), precondition_input)); // Prepare test input. let input = make_input(b"conflict!", b"nonce2", b"should_be_valid"); // Ensure the expected error is thrown when no value is present. assert_noop!( - Bioauth::authenticate(Origin::signed(1), input), + Bioauth::authenticate(Origin::none(), input), Error::::PublicKeyAlreadyUsed, ); }); @@ -93,7 +96,7 @@ fn signed_ext_check_bioauth_tx_deny_invalid_signature() { assert_eq!( CheckBioauthTx::(PhantomData).validate(&1, &call, &info, 1), - InvalidTransaction::Call.into() + InvalidTransaction::Custom(b's').into() ); }) } @@ -104,12 +107,21 @@ fn signed_ext_check_bioauth_tx_permit_empty_state() { // Prepare test input. let input = make_input(b"qwe", b"rty", b"should_be_valid"); + let opaque_auth_ticket: OpaqueAuthTicket = input.ticket.clone().into(); + let auth_ticket: AuthTicket = (&opaque_auth_ticket).try_into().unwrap(); + let expected_tag: StoredAuthTicket = auth_ticket.into(); + let call = >::authenticate(input).into(); let info = DispatchInfo::default(); assert_eq!( CheckBioauthTx::(PhantomData).validate(&1, &call, &info, 1), - Ok(ValidTransaction::default()) + ValidTransaction::with_tag_prefix("bioauth") + .and_provides(expected_tag) + .priority(50) + .longevity(1) + .propagate(true) + .build() ); }) } @@ -119,7 +131,7 @@ fn signed_ext_check_bioauth_tx_deny_conlicting_nonce() { new_test_ext().execute_with(|| { // Prepare the test precondition. let precondition_input = make_input(b"pk1", b"conflict!", b"should_be_valid"); - assert_ok!(Bioauth::authenticate(Origin::signed(1), precondition_input)); + assert_ok!(Bioauth::authenticate(Origin::none(), precondition_input)); // Prepare test input. let input = make_input(b"pk2", b"conflict!", b"should_be_valid"); @@ -129,7 +141,7 @@ fn signed_ext_check_bioauth_tx_deny_conlicting_nonce() { assert_eq!( CheckBioauthTx::(PhantomData).validate(&1, &call, &info, 1), - InvalidTransaction::Call.into() + InvalidTransaction::Custom(b'c').into() ); }) } @@ -139,7 +151,7 @@ fn signed_ext_check_bioauth_tx_deny_public_keys() { new_test_ext().execute_with(|| { // Prepare the test precondition. let precondition_input = make_input(b"conflict!", b"nonce1", b"should_be_valid"); - assert_ok!(Bioauth::authenticate(Origin::signed(1), precondition_input)); + assert_ok!(Bioauth::authenticate(Origin::none(), precondition_input)); // Prepare test input. let input = make_input(b"conflict!", b"nonce2", b"should_be_valid"); @@ -149,7 +161,7 @@ fn signed_ext_check_bioauth_tx_deny_public_keys() { assert_eq!( CheckBioauthTx::(PhantomData).validate(&1, &call, &info, 1), - InvalidTransaction::Call.into() + InvalidTransaction::Custom(b'c').into() ); }) }