From 251e1a24110949323c7d5f9401b6d4f5b3d3337e Mon Sep 17 00:00:00 2001 From: w Date: Mon, 24 Aug 2026 15:14:44 -0400 Subject: [PATCH] Fix environment-qualified host compatibility --- README.md | 9 + rust/crates/truapi-server/src/host_core.rs | 12 + rust/crates/truapi-server/src/runtime.rs | 198 ++++++++++--- .../truapi-server/src/runtime/signing_host.rs | 279 ++++++++++++++---- .../src/runtime/signing_host/sso_responder.rs | 125 ++++---- .../src/runtime/statement_store.rs | 3 +- rust/crates/truapi-server/src/wasm.rs | 19 ++ .../truapi-server/tests/wire_result_shape.rs | 7 +- 8 files changed, 482 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 06dd6ac51..4e5a02f6b 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,15 @@ dependency on the crate: Wire ids are append-only: existing ids never change, so deployed products stay compatible across protocol revisions. +Product-account handles are normalized against the admitted product runtime. +An environment-qualified runtime such as `host-playground.paseo` accepts the +exact canonical `host-playground.dot` and container +`host-playground.paseo.dot` account spellings without widening access to +another product. Hosts can register a +network-qualified `peopl.` alias for the active wallet's canonical +Lite Person key. Multi-resource allocation executes independent chain +allocations concurrently while preserving request order in the response. + ## Develop Common tasks are wrapped in the top-level `Makefile`. Run `make help` for the full list. diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index f31f492fc..8fcc0cadd 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -546,6 +546,18 @@ impl SigningHostRuntime { .map_err(ring_vrf_admin_error) } + /// Registers an environment-qualified alias for the active wallet's canonical Lite Person key. + pub async fn ensure_lite_person_provider_alias( + &self, + people_chain_id: [u8; 32], + owner: &str, + ) -> Result<(), v01::GenericError> { + self.signing_host + .ensure_lite_person_provider_alias(people_chain_id, owner) + .await + .map_err(ring_vrf_admin_error) + } + /// Activate a wallet-local session from host-held secret material (raw /// BIP-39 entropy). #[instrument(skip_all, fields(runtime.method = "signing_host_runtime.activate_local_session"))] diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index c3386c6e1..d83c4574c 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -515,11 +515,36 @@ impl ProductRuntimeHost { } fn normalize_product_account_id( + &self, product_account_id: v01::ProductAccountId, ) -> Result { + let mut dot_ns_identifier = + normalize_product_identifier(&product_account_id.dot_ns_identifier).map_err(|_| ())?; + let product_id = self.product.product_id.as_str(); + let canonical_runtime = product_id + .rsplit_once('.') + .map(|(label, _)| format!("{label}.dot")); + let canonical_requested = dot_ns_identifier + .rsplit_once('.') + .map(|(label, _)| format!("{label}.dot")); + let canonical_container_alias = + dot_ns_identifier + .strip_suffix(".dot") + .and_then(|container| { + container + .rsplit_once('.') + .map(|(label, _)| format!("{label}.dot")) + }); + if (!product_id.ends_with(".dot") + && canonical_runtime.as_deref() == Some(&dot_ns_identifier)) + || canonical_requested.as_deref() == Some(product_id) + || dot_ns_identifier.strip_suffix(".dot") == Some(product_id) + || canonical_container_alias.as_deref() == Some(product_id) + { + dot_ns_identifier = product_id.to_owned(); + } Ok(v01::ProductAccountId { - dot_ns_identifier: normalize_product_identifier(&product_account_id.dot_ns_identifier) - .map_err(|_| ())?, + dot_ns_identifier, derivation_index: product_account_id.derivation_index, }) } @@ -979,8 +1004,9 @@ impl Account for ProductRuntimeHost { request: HostAccountGetRequest, ) -> Result> { let HostAccountGetRequest::V1(v01::HostAccountGetRequest { product_account_id }) = request; - let product_account_id = - Self::normalize_product_account_id(product_account_id).map_err(|()| { + let product_account_id = self + .normalize_product_account_id(product_account_id) + .map_err(|()| { CallError::Domain(HostAccountGetError::V1( v01::HostAccountGetError::DomainNotValid, )) @@ -1065,13 +1091,15 @@ impl Account for ProductRuntimeHost { context, ring_location, }) = request; - let key_handle = Self::normalize_product_account_id(key_handle).map_err(|()| { - CallError::Domain(HostAccountGetAliasError::V1( - v01::HostAccountGetAliasError::Unknown { - reason: "Invalid key handle".to_string(), - }, - )) - })?; + let key_handle = self + .normalize_product_account_id(key_handle) + .map_err(|()| { + CallError::Domain(HostAccountGetAliasError::V1( + v01::HostAccountGetAliasError::Unknown { + reason: "Invalid key handle".to_string(), + }, + )) + })?; let Some(session) = self.authority.current_session() else { return Err(CallError::Domain(HostAccountGetAliasError::V1( v01::HostAccountGetAliasError::Rejected, @@ -1110,18 +1138,15 @@ impl Account for ProductRuntimeHost { ring_location, message, }) = request; - let key_handle = Self::normalize_product_account_id(key_handle).map_err(|()| { - CallError::Domain(HostAccountCreateProofError::V1( - v01::HostAccountCreateProofError::Unknown { - reason: "Invalid key handle".to_string(), - }, - )) - })?; - if key_handle.dot_ns_identifier != self.product_id() { - return Err(CallError::Domain(HostAccountCreateProofError::V1( - v01::HostAccountCreateProofError::NotAllowlisted, - ))); - } + let key_handle = self + .normalize_product_account_id(key_handle) + .map_err(|()| { + CallError::Domain(HostAccountCreateProofError::V1( + v01::HostAccountCreateProofError::Unknown { + reason: "Invalid key handle".to_string(), + }, + )) + })?; let Some(session) = self.authority.current_session() else { return Err(CallError::Domain(HostAccountCreateProofError::V1( @@ -1244,8 +1269,9 @@ impl Account for ProductRuntimeHost { request: HostAccountRingVrfSignRequest, ) -> Result> { let HostAccountRingVrfSignRequest::V1(mut request) = request; - request.key_handle = - Self::normalize_product_account_id(request.key_handle).map_err(|()| { + request.key_handle = self + .normalize_product_account_id(request.key_handle) + .map_err(|()| { CallError::Domain(HostAccountRingVrfSignError::V1( v01::HostAccountRingVrfSignError::Unknown { reason: "Invalid key handle".to_string(), @@ -1288,13 +1314,15 @@ impl Account for ProductRuntimeHost { request: HostAccountSignVrfRequest, ) -> Result> { let HostAccountSignVrfRequest::V1(mut request) = request; - request.account = Self::normalize_product_account_id(request.account).map_err(|()| { - CallError::Domain(HostAccountSignVrfError::V1( - v01::HostAccountSignVrfError::Unknown { - reason: "Invalid product account".to_string(), - }, - )) - })?; + request.account = self + .normalize_product_account_id(request.account) + .map_err(|()| { + CallError::Domain(HostAccountSignVrfError::V1( + v01::HostAccountSignVrfError::Unknown { + reason: "Invalid product account".to_string(), + }, + )) + })?; validate_vrf_transcript(&request).map_err(|reason| { CallError::Domain(HostAccountSignVrfError::V1( v01::HostAccountSignVrfError::Unknown { reason }, @@ -1575,11 +1603,13 @@ impl Signing for ProductRuntimeHost { ) -> Result> { debug!("sign_payload: requesting signing-host signature"); let HostSignPayloadRequest::V1(mut inner) = request; - inner.account = Self::normalize_product_account_id(inner.account).map_err(|()| { - CallError::Domain(HostSignPayloadError::V1( - v01::HostSignPayloadError::PermissionDenied, - )) - })?; + inner.account = self + .normalize_product_account_id(inner.account) + .map_err(|()| { + CallError::Domain(HostSignPayloadError::V1( + v01::HostSignPayloadError::PermissionDenied, + )) + })?; if !self.is_product_account_valid_for_caller(&inner.account.dot_ns_identifier) { return Err(CallError::Domain(HostSignPayloadError::V1( v01::HostSignPayloadError::PermissionDenied, @@ -1627,12 +1657,21 @@ impl Signing for ProductRuntimeHost { ) -> Result> { debug!("sign_raw: requesting signing-host signature"); let HostSignRawRequest::V1(mut inner) = request; - inner.account = Self::normalize_product_account_id(inner.account).map_err(|()| { - CallError::Domain(HostSignRawError::V1( - v01::HostSignPayloadError::PermissionDenied, - )) - })?; + let requested_product_id = inner.account.dot_ns_identifier.clone(); + inner.account = self + .normalize_product_account_id(inner.account) + .map_err(|()| { + CallError::Domain(HostSignRawError::V1( + v01::HostSignPayloadError::PermissionDenied, + )) + })?; if !self.is_product_account_valid_for_caller(&inner.account.dot_ns_identifier) { + debug!( + runtime_product_id = %self.product_id(), + %requested_product_id, + normalized_product_id = %inner.account.dot_ns_identifier, + "sign_raw rejected a product-account identity mismatch" + ); return Err(CallError::Domain(HostSignRawError::V1( v01::HostSignPayloadError::PermissionDenied, ))); @@ -1679,12 +1718,21 @@ impl Signing for ProductRuntimeHost { ) -> Result> { debug!("create_transaction: requesting signing-host signature"); let HostCreateTransactionRequest::V1(mut inner) = request; - inner.signer = Self::normalize_product_account_id(inner.signer).map_err(|()| { - CallError::Domain(HostCreateTransactionError::V1( - v01::HostCreateTransactionError::PermissionDenied, - )) - })?; + let requested_product_id = inner.signer.dot_ns_identifier.clone(); + inner.signer = self + .normalize_product_account_id(inner.signer) + .map_err(|()| { + CallError::Domain(HostCreateTransactionError::V1( + v01::HostCreateTransactionError::PermissionDenied, + )) + })?; if !self.is_product_account_valid_for_caller(&inner.signer.dot_ns_identifier) { + debug!( + runtime_product_id = %self.product_id(), + %requested_product_id, + normalized_product_id = %inner.signer.dot_ns_identifier, + "create_transaction rejected a product-account identity mismatch" + ); return Err(CallError::Domain(HostCreateTransactionError::V1( v01::HostCreateTransactionError::PermissionDenied, ))); @@ -3973,6 +4021,62 @@ mod tests { ); } + #[test] + fn get_account_maps_canonical_and_container_aliases_to_environment_product() { + let host = ProductRuntimeHost::new( + stub_platform(), + runtime_config("host-playground.paseo"), + test_spawner(), + ); + install_pairing_session(&host, sso_session_info()); + for dot_ns_identifier in ["host-playground.dot", "host-playground.paseo.dot"] { + let request = HostAccountGetRequest::V1(v01::HostAccountGetRequest { + product_account_id: v01::ProductAccountId { + dot_ns_identifier: dot_ns_identifier.to_string(), + derivation_index: v01::DerivationIndex::Index(0), + }, + }); + + let response = + futures::executor::block_on(host.get_account(&CallContext::default(), request)) + .unwrap(); + let HostAccountGetResponse::V1(inner) = response; + assert_eq!( + inner.account.public_key, + test_product_account_public("host-playground.paseo", 0).to_vec(), + "{dot_ns_identifier}" + ); + } + } + + #[test] + fn get_account_maps_environment_container_alias_to_canonical_runtime() { + let host = ProductRuntimeHost::new( + stub_platform(), + runtime_config("host-playground.dot"), + test_spawner(), + ); + install_pairing_session(&host, sso_session_info()); + for dot_ns_identifier in ["host-playground.paseo", "host-playground.paseo.dot"] { + let request = HostAccountGetRequest::V1(v01::HostAccountGetRequest { + product_account_id: v01::ProductAccountId { + dot_ns_identifier: dot_ns_identifier.to_string(), + derivation_index: v01::DerivationIndex::Index(0), + }, + }); + + let response = + futures::executor::block_on(host.get_account(&CallContext::default(), request)) + .unwrap(); + let HostAccountGetResponse::V1(inner) = response; + assert_eq!( + inner.account.public_key, + test_product_account_public("host-playground.dot", 0).to_vec(), + "{dot_ns_identifier}" + ); + } + } + #[test] fn get_account_localhost_product_prompts_for_other_product_identifier() { let host = ProductRuntimeHost::new( diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index 914f08c75..afa58bd5a 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -44,14 +44,12 @@ use crate::host_logic::extrinsic::{ Sr25519Signer, V5BuildError, build_signed_extrinsic_v4, build_signed_extrinsic_v4_with_signature, build_signed_extrinsic_v5, }; -use crate::host_logic::product_account::{ - ProductAccountError, SR25519_SIGNING_CONTEXT, derivation_index_bytes, derive_identity_keypair, - derive_product_keypair, derive_product_subtree_keypair, derive_ring_vrf_entropy, - derive_root_keypair_from_entropy, -}; #[cfg(not(target_arch = "wasm32"))] +use crate::host_logic::product_account::derive_full_person_ring_vrf_entropy; use crate::host_logic::product_account::{ - derive_full_person_ring_vrf_entropy, derive_lite_person_ring_vrf_entropy, + PERSONHOOD_PRODUCT_ID, ProductAccountError, SR25519_SIGNING_CONTEXT, derivation_index_bytes, + derive_identity_keypair, derive_lite_person_ring_vrf_entropy, derive_product_keypair, + derive_product_subtree_keypair, derive_ring_vrf_entropy, derive_root_keypair_from_entropy, }; use crate::host_logic::session::{SessionInfo, SessionState}; use crate::host_logic::sso::messages::{OnExistingAllowancePolicy, RingVrfError}; @@ -77,6 +75,9 @@ use zeroize::Zeroizing; const BYTES_WRAP_PREFIX: &[u8] = b""; const BYTES_WRAP_SUFFIX: &[u8] = b""; +const PEOPLE_LITE_COLLECTION: &[u8; 32] = b"pop:polkadot.network/people-lite"; +const MEMBERS_PALLET_INSTANCE: u8 = 67; + #[derive(Default)] struct LocalGrantState { activation_generation: u64, @@ -431,9 +432,7 @@ impl SigningHost { if !entry.rings.contains(ring) { return Err(RingVrfError::KeyNotInRing); } - let entropy = self.ring_vrf_entropy(session, handle)?; - Self::require_matching_registered_public_key(&entry, &entropy)?; - Ok(entropy) + self.resolve_registered_entropy(session, handle, &entry) } async fn resolve_registered_ring_vrf_key( @@ -445,22 +444,57 @@ impl SigningHost { .registered_ring_vrf_entry(session, handle) .await? .ok_or(RingVrfError::KeyNotRegistered)?; + self.resolve_registered_entropy(session, handle, &entry) + } + + fn resolve_registered_entropy( + &self, + session: &AuthoritySession, + handle: &v01::ProductAccountId, + entry: &v01::RegisteredRingVrfKey, + ) -> Result, RingVrfError> { let entropy = self.ring_vrf_entropy(session, handle)?; - Self::require_matching_registered_public_key(&entry, &entropy)?; - Ok(entropy) + if Self::registered_public_key_matches(entry, &entropy)? { + return Ok(entropy); + } + if Self::is_lite_person_handle(handle) { + let canonical = + Zeroizing::new(derive_lite_person_ring_vrf_entropy(&self.root_entropy()?)); + if Self::registered_public_key_matches(entry, &canonical)? { + return Ok(canonical); + } + } + Err(RingVrfError::Unknown { + reason: "registered ring-VRF public key does not match the active wallet".to_string(), + }) } - fn require_matching_registered_public_key( + fn registered_public_key_matches( entry: &v01::RegisteredRingVrfKey, entropy: &[u8; 32], - ) -> Result<(), RingVrfError> { - if entry.public_key != Some(member_from_entropy(entropy)?) { - return Err(RingVrfError::Unknown { - reason: "registered ring-VRF public key does not match the active wallet" - .to_string(), - }); + ) -> Result { + Ok(entry.public_key == Some(member_from_entropy(entropy)?)) + } + + fn is_lite_person_handle(handle: &v01::ProductAccountId) -> bool { + matches!(handle.derivation_index, v01::DerivationIndex::Index(1)) + && (handle.dot_ns_identifier == PERSONHOOD_PRODUCT_ID + || handle.dot_ns_identifier.starts_with("peopl.")) + } + + async fn is_lite_person_provider_alias( + &self, + session: &AuthoritySession, + handle: &v01::ProductAccountId, + ) -> Result { + if !Self::is_lite_person_handle(handle) { + return Ok(false); } - Ok(()) + let Some(entry) = self.registered_ring_vrf_entry(session, handle).await? else { + return Ok(false); + }; + let canonical = derive_lite_person_ring_vrf_entropy(&self.root_entropy()?); + Self::registered_public_key_matches(&entry, &canonical) } fn ring_vrf_member_candidate( @@ -487,6 +521,45 @@ impl SigningHost { Ok(()) } + /// Registers an environment-qualified alias for the active wallet's canonical Lite Person key. + pub(crate) async fn ensure_lite_person_provider_alias( + &self, + people_chain_id: [u8; 32], + owner: &str, + ) -> Result<(), RingVrfError> { + let session = self.current_local_session().ok_or(RingVrfError::Unknown { + reason: "no active session".to_string(), + })?; + if session.lite_username.is_none() { + return Ok(()); + } + let owner = normalize_product_identifier(owner).map_err(|error| RingVrfError::Unknown { + reason: error.to_string(), + })?; + if owner != PERSONHOOD_PRODUCT_ID && !owner.starts_with("peopl.") { + return Err(RingVrfError::Unknown { + reason: "Lite Person provider aliases must use the reserved peopl product" + .to_string(), + }); + } + let handle = v01::ProductAccountId { + dot_ns_identifier: owner, + derivation_index: v01::DerivationIndex::Index(1), + }; + let public_key = + member_from_entropy(&derive_lite_person_ring_vrf_entropy(&self.root_entropy()?))?; + let ring = v01::RingLocation { + chain_id: people_chain_id, + junctions: vec![ + v01::RingLocationJunction::PalletInstance(MEMBERS_PALLET_INSTANCE), + v01::RingLocationJunction::CollectionId(PEOPLE_LITE_COLLECTION.to_vec()), + ], + }; + self.ring_vrf_registry + .register(session.public_key, handle, ring, public_key) + .await + } + pub(crate) async fn ring_vrf_providers( &self, ring: &v01::RingLocation, @@ -828,7 +901,34 @@ impl ProductAuthority for SigningHost { request: CreateProofAuthorityRequest, ) -> Result { self.require_current_session(session)?; - Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle)?; + if Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle) + .is_err() + { + if !self + .is_lite_person_provider_alias(session, &request.key_handle) + .await? + { + return Err(RingVrfError::NotAllowlisted); + } + match super::account_access_authorization( + self.services.platform.as_ref(), + &request.calling_product_id, + &request.key_handle.dot_ns_identifier, + ) + .await + { + Ok(PermissionAuthorizationStatus::Authorized) => {} + Ok( + PermissionAuthorizationStatus::Denied + | PermissionAuthorizationStatus::NotDetermined, + ) => return Err(RingVrfError::Rejected), + Err(error) => { + return Err(RingVrfError::Unknown { + reason: error.to_string(), + }); + } + } + } let entropy = self .resolve_ring_vrf_key_for_ring(session, &request.key_handle, &request.ring_location) .await?; @@ -944,45 +1044,50 @@ impl ProductAuthority for SigningHost { request: v01::HostRequestResourceAllocationRequest, ) -> Result { self.require_current_session(session)?; - let mut outcomes = Vec::with_capacity(request.resources.len()); - for resource in request.resources { - let outcome = match resource { - v01::AllocatableResource::StatementStoreAllowance => { - sso_responder::allocate_statement_store_allowance( - &self.services, - self, - &product_id, - OnExistingAllowancePolicy::Increase, - ) - .await - .map(|_| v01::AllocationOutcome::Allocated) - } - v01::AllocatableResource::BulletinAllowance => { - sso_responder::allocate_bulletin_allowance( - &self.services, - self, - &product_id, - OnExistingAllowancePolicy::Increase, - ) - .await - .map(|_| v01::AllocationOutcome::Allocated) + let resource_count = request.resources.len(); + let allocation_results = + futures::future::join_all(request.resources.into_iter().map(|resource| async { + match resource { + v01::AllocatableResource::StatementStoreAllowance => { + sso_responder::allocate_statement_store_allowance( + &self.services, + self, + &product_id, + OnExistingAllowancePolicy::Ignore, + ) + .await + .map(|_| v01::AllocationOutcome::Allocated) + } + v01::AllocatableResource::BulletinAllowance => { + sso_responder::allocate_bulletin_allowance( + &self.services, + self, + &product_id, + OnExistingAllowancePolicy::Ignore, + ) + .await + .map(|_| v01::AllocationOutcome::Allocated) + } + v01::AllocatableResource::SmartContractAllowance(index) => { + sso_responder::allocate_smart_contract_allowance( + &self.services, + self, + &product_id, + index, + OnExistingAllowancePolicy::Ignore, + ) + .await + .map(|()| v01::AllocationOutcome::Allocated) + } + v01::AllocatableResource::AutoSigning => self + .grant_auto_signing(session, &product_id) + .map(|_| v01::AllocationOutcome::Allocated) + .map_err(sso_responder::AllowanceAllocationError::Authority), } - v01::AllocatableResource::SmartContractAllowance(index) => { - sso_responder::allocate_smart_contract_allowance( - &self.services, - self, - &product_id, - index, - OnExistingAllowancePolicy::Increase, - ) - .await - .map(|()| v01::AllocationOutcome::Allocated) - } - v01::AllocatableResource::AutoSigning => self - .grant_auto_signing(session, &product_id) - .map(|_| v01::AllocationOutcome::Allocated) - .map_err(sso_responder::AllowanceAllocationError::Authority), - }; + })) + .await; + let mut outcomes = Vec::with_capacity(resource_count); + for outcome in allocation_results { match outcome { Ok(outcome) => outcomes.push(outcome), Err(reason) => { @@ -1240,13 +1345,13 @@ mod tests { use super::super::{ProductAuthority, ProductRuntimeHost, RuntimeServices, SigningHostRole}; use super::ring_vrf::{MemberCandidate, ResolvedRing, RingResolver, member_from_entropy}; use super::{ - BYTES_WRAP_PREFIX, BYTES_WRAP_SUFFIX, LocalActivation, RingVrfError, - SR25519_SIGNING_CONTEXT, raw_payload_bytes, + BYTES_WRAP_PREFIX, BYTES_WRAP_SUFFIX, LocalActivation, MEMBERS_PALLET_INSTANCE, + PEOPLE_LITE_COLLECTION, RingVrfError, SR25519_SIGNING_CONTEXT, raw_payload_bytes, }; use crate::host_logic::extrinsic::tests::split_v4; use crate::host_logic::product_account::{ - derive_identity_keypair, derive_product_keypair, derive_ring_vrf_entropy, - derive_root_keypair_from_entropy, index_bytes, + derive_identity_keypair, derive_lite_person_ring_vrf_entropy, derive_product_keypair, + derive_ring_vrf_entropy, derive_root_keypair_from_entropy, index_bytes, }; use crate::host_logic::transaction::{ extrinsic_payload_extensions, extrinsic_payload_preimage, @@ -1420,6 +1525,56 @@ mod tests { .expect("full person key registration succeeds"); } + fn lite_person_ring_location() -> v01::RingLocation { + v01::RingLocation { + chain_id: [0x33; 32], + junctions: vec![ + v01::RingLocationJunction::PalletInstance(MEMBERS_PALLET_INSTANCE), + v01::RingLocationJunction::CollectionId(PEOPLE_LITE_COLLECTION.to_vec()), + ], + } + } + + #[test] + fn lite_person_provider_alias_uses_the_canonical_member_key() { + let (_, authority) = signing_runtime(); + futures::executor::block_on( + authority.activate_local_session_with_identity( + ENTROPY.to_vec(), + Some("alice.42".to_string()), + ), + ) + .expect("activation succeeds"); + futures::executor::block_on( + authority.ensure_lite_person_provider_alias([0x33; 32], "peopl.paseo"), + ) + .expect("provider alias registration succeeds"); + + let ring = lite_person_ring_location(); + let providers = futures::executor::block_on(authority.ring_vrf_providers(&ring)) + .expect("providers load"); + assert_eq!( + providers, + vec![v01::ProductAccountId { + dot_ns_identifier: "peopl.paseo".to_string(), + derivation_index: v01::DerivationIndex::Index(1), + }] + ); + + let session = authority.current_session().expect("active session"); + let entropy = futures::executor::block_on(authority.resolve_ring_vrf_key_for_ring( + &session, + &providers[0], + &ring, + )) + .expect("alias resolves"); + assert_eq!( + member_from_entropy(&entropy).expect("member derives"), + member_from_entropy(&derive_lite_person_ring_vrf_entropy(&ENTROPY)) + .expect("canonical Lite member derives") + ); + } + #[test] fn internal_allowances_offer_both_reserved_person_handles_widest_first() { let (_, authority) = signing_runtime(); diff --git a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs index afb299dd2..567d8339b 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs @@ -827,64 +827,75 @@ async fn resource_allocation_response( } } - let mut outcomes = Vec::with_capacity(request.resources.len()); - let mut item_failures = Vec::new(); - for resource in request.resources { - let outcome = match resource { - SsoAllocatableResource::StatementStoreAllowance => allocate_statement_store_allowance( - services, - signing_host, - &request.calling_product_id, - request.on_existing, - ) - .await - .map(|slot_account_key| { - SsoAllocationOutcome::Allocated(SsoAllocatedResource::StatementStoreAllowance { - slot_account_key, - }) - }), - SsoAllocatableResource::BulletinAllowance => allocate_bulletin_allowance( - services, - signing_host, - &request.calling_product_id, - request.on_existing, - ) - .await - .map(|slot_account_key| { - SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance { - slot_account_key, - }) - }), - SsoAllocatableResource::SmartContractAllowance(index) => { - allocate_smart_contract_allowance( - services, - signing_host, - &request.calling_product_id, - index.clone(), - request.on_existing, - ) - .await - .map(|()| { - SsoAllocationOutcome::Allocated(SsoAllocatedResource::SmartContractAllowance) - }) + let resource_count = request.resources.len(); + let calling_product_id = request.calling_product_id; + let policy = request.on_existing; + let allocation_results = + futures::future::join_all(request.resources.into_iter().map(|resource| async { + match resource { + SsoAllocatableResource::StatementStoreAllowance => { + allocate_statement_store_allowance( + services, + signing_host, + &calling_product_id, + policy, + ) + .await + .map(|slot_account_key| { + SsoAllocationOutcome::Allocated( + SsoAllocatedResource::StatementStoreAllowance { slot_account_key }, + ) + }) + } + SsoAllocatableResource::BulletinAllowance => { + allocate_bulletin_allowance(services, signing_host, &calling_product_id, policy) + .await + .map(|slot_account_key| { + SsoAllocationOutcome::Allocated( + SsoAllocatedResource::BulletinAllowance { slot_account_key }, + ) + }) + } + SsoAllocatableResource::SmartContractAllowance(index) => { + allocate_smart_contract_allowance( + services, + signing_host, + &calling_product_id, + index, + policy, + ) + .await + .map(|()| { + SsoAllocationOutcome::Allocated( + SsoAllocatedResource::SmartContractAllowance, + ) + }) + } + SsoAllocatableResource::AutoSigning => { + (|| -> Result<_, AllowanceAllocationError> { + let product_root_private_key = signing_host + .product_subtree_secret(&calling_product_id) + .map_err(AllowanceAllocationError::Authority)?; + let root_entropy = signing_host.root_entropy()?; + let ring_vrf_domain_entropy = + derive_ring_vrf_domain_entropy(&root_entropy, &calling_product_id) + .map_err(super::product_authority_error) + .map_err(AllowanceAllocationError::Authority)?; + Ok(SsoAllocationOutcome::Allocated( + SsoAllocatedResource::AutoSigning { + product_root_private_key, + ring_vrf_domain_entropy, + }, + )) + })() + } } - SsoAllocatableResource::AutoSigning => (|| -> Result<_, AllowanceAllocationError> { - let product_root_private_key = signing_host - .product_subtree_secret(&request.calling_product_id) - .map_err(AllowanceAllocationError::Authority)?; - let root_entropy = signing_host.root_entropy()?; - let ring_vrf_domain_entropy = - derive_ring_vrf_domain_entropy(&root_entropy, &request.calling_product_id) - .map_err(super::product_authority_error) - .map_err(AllowanceAllocationError::Authority)?; - Ok(SsoAllocationOutcome::Allocated( - SsoAllocatedResource::AutoSigning { - product_root_private_key, - ring_vrf_domain_entropy, - }, - )) - })(), - }; + })) + .await; + + let mut outcomes = Vec::with_capacity(resource_count); + let mut item_failures = Vec::new(); + for outcome in allocation_results { match outcome { Ok(outcome) => outcomes.push(outcome), Err(err) => { diff --git a/rust/crates/truapi-server/src/runtime/statement_store.rs b/rust/crates/truapi-server/src/runtime/statement_store.rs index 97da98390..a6ad7d1f2 100644 --- a/rust/crates/truapi-server/src/runtime/statement_store.rs +++ b/rust/crates/truapi-server/src/runtime/statement_store.rs @@ -124,7 +124,8 @@ impl StatementStore for ProductRuntimeHost { CallError, > { let RemoteStatementStoreCreateProofRequest::V1(mut inner) = request; - inner.product_account_id = Self::normalize_product_account_id(inner.product_account_id) + inner.product_account_id = self + .normalize_product_account_id(inner.product_account_id) .map_err(|()| { CallError::Domain(RemoteStatementStoreCreateProofError::V1( latest::RemoteStatementStoreCreateProofError::UnknownAccount, diff --git a/rust/crates/truapi-server/src/wasm.rs b/rust/crates/truapi-server/src/wasm.rs index 364654d88..b0b4e916e 100644 --- a/rust/crates/truapi-server/src/wasm.rs +++ b/rust/crates/truapi-server/src/wasm.rs @@ -1034,6 +1034,25 @@ impl WasmSigningHostRuntime { .map_err(generic_error_to_js) } + /// Registers an environment-qualified alias for the active wallet's canonical Lite Person key. + #[wasm_bindgen(js_name = ensureLitePersonProviderAlias)] + pub async fn ensure_lite_person_provider_alias( + &self, + people_chain_id: Vec, + owner: String, + ) -> Result<(), JsValue> { + let people_chain_id: [u8; 32] = people_chain_id.try_into().map_err(|bytes: Vec| { + JsValue::from_str(&format!( + "People chain genesis must be 32 bytes, got {}", + bytes.len() + )) + })?; + self.runtime + .ensure_lite_person_provider_alias(people_chain_id, &owner) + .await + .map_err(generic_error_to_js) + } + /// Revoke one product's grants from the current local activation. #[wasm_bindgen(js_name = clearProductState)] pub async fn clear_product_state(&self, product_id: String) -> Result<(), JsValue> { diff --git a/rust/crates/truapi-server/tests/wire_result_shape.rs b/rust/crates/truapi-server/tests/wire_result_shape.rs index 793d10602..ba3ec95f8 100644 --- a/rust/crates/truapi-server/tests/wire_result_shape.rs +++ b/rust/crates/truapi-server/tests/wire_result_shape.rs @@ -237,7 +237,7 @@ fn version_index(version: u8) -> u8 { } #[test] -fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { +fn foreign_account_proof_is_delegated_to_the_account_authority() { let core = make_core(); let request = account::HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest { key_handle: v01::ProductAccountId { @@ -268,9 +268,10 @@ fn foreign_account_proof_returns_not_allowlisted_without_confirmation() { ); assert_eq!(response.request_id, "p:account-proof"); assert_eq!(response.payload.id, ids.response_id); - // RFC-0024 forbids a prompt fallback for bearer proofs made with a foreign key. + // The product runtime delegates foreign-key policy to the authority. This + // fixture has no active session, so the authority returns Rejected. let expected = versioned_result_err_payload(account::HostAccountCreateProofError::V1( - v01::HostAccountCreateProofError::NotAllowlisted, + v01::HostAccountCreateProofError::Rejected, )); assert_eq!(response.payload.value, expected); }