Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<network>` 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.
Expand Down
12 changes: 12 additions & 0 deletions rust/crates/truapi-server/src/host_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down
198 changes: 151 additions & 47 deletions rust/crates/truapi-server/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,11 +515,36 @@ impl ProductRuntimeHost {
}

fn normalize_product_account_id(
&self,
product_account_id: v01::ProductAccountId,
) -> Result<v01::ProductAccountId, ()> {
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,
})
}
Expand Down Expand Up @@ -979,8 +1004,9 @@ impl Account for ProductRuntimeHost {
request: HostAccountGetRequest,
) -> Result<HostAccountGetResponse, CallError<HostAccountGetError>> {
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,
))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1244,8 +1269,9 @@ impl Account for ProductRuntimeHost {
request: HostAccountRingVrfSignRequest,
) -> Result<HostAccountRingVrfSignResponse, CallError<HostAccountRingVrfSignError>> {
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(),
Expand Down Expand Up @@ -1288,13 +1314,15 @@ impl Account for ProductRuntimeHost {
request: HostAccountSignVrfRequest,
) -> Result<HostAccountSignVrfResponse, CallError<HostAccountSignVrfError>> {
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 },
Expand Down Expand Up @@ -1575,11 +1603,13 @@ impl Signing for ProductRuntimeHost {
) -> Result<HostSignPayloadResponse, CallError<HostSignPayloadError>> {
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,
Expand Down Expand Up @@ -1627,12 +1657,21 @@ impl Signing for ProductRuntimeHost {
) -> Result<HostSignRawResponse, CallError<HostSignRawError>> {
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,
)));
Expand Down Expand Up @@ -1679,12 +1718,21 @@ impl Signing for ProductRuntimeHost {
) -> Result<HostCreateTransactionResponse, CallError<HostCreateTransactionError>> {
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,
)));
Expand Down Expand Up @@ -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(
Expand Down
Loading