diff --git a/CLAUDE.md b/CLAUDE.md
index 340511b9b..f8cade281 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -73,6 +73,14 @@ scripts/truapi-host-installer.sh
rather than importing a concrete protocol version. Runtime crates may use
`truapi::versioned::*` for wire envelopes, but should unwrap them into latest
payloads immediately.
+- Inter-host SSO uses `#[sso_service]` as described
+ in the [macro guide](rust/crates/truapi-macros/README.md). Keep per-variant pairing,
+ dispatch, and correlation in that macro; do not add manual per-variant catalogs.
+ Requests with a caller share `ProductRequest
` around canonical payloads;
+ handler method names select request variants. Responses share `Response
`;
+ handler signatures name their result payload
+ and response variant. Transcript classification belongs in shared reply handling
+ or the handler; request context carries only the call and signing session.
- Native bindings expose canonical Rust domain and protocol types directly.
Add feature-gated UniFFI derives to those types and custom conversions for
unsupported leaf values instead of defining parallel `Native*` mirrors.
diff --git a/Cargo.lock b/Cargo.lock
index 92401e469..49772c936 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5676,6 +5676,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"truapi",
+ "truapi-macros",
"truapi-platform",
"unicode-normalization",
"uniffi",
diff --git a/README.md b/README.md
index d6a684304..7ec96f70d 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,10 @@ returning a typed outcome: response bytes to post back, a disconnect marker, or
ignored) and `prepareDisconnectRequest` (builds the SCALE-encoded wire message
for a wallet-initiated disconnect) on `TrUAPIHostRuntime`. Response posting and
session-record cleanup remain on the wallet side.
+See the core's [inter-host SSO design](rust/crates/truapi-server/README.md#inter-host-sso)
+for typed handlers, canonical resource types, and consent bound to the signing session.
+Product and SSO signing share canonical payloads and the one-byte `OptionBool`
+encoding for `with_signed_transaction`.
### JS Host SDKs
diff --git a/docs/rfcs/0024-personhood-as-product.md b/docs/rfcs/0024-personhood-as-product.md
index 595dca471..2a2301822 100644
--- a/docs/rfcs/0024-personhood-as-product.md
+++ b/docs/rfcs/0024-personhood-as-product.md
@@ -329,7 +329,7 @@ struct ListRingVrfKeysResponse {
}
```
-A Host holding a current registry snapshot answers `list` locally. `RingVrfProofRequest` and `RingVrfAliasRequest` gain `key_handle: ProductAccountId` alongside the `calling_product_id` they already carry, and a `RingVrfSignRequest` / `Response` pair mirrors `account_ring_vrf_sign` with the same two fields plus `message`. `RingVrfError` gains `KeyNotRegistered`, `KeyNotInRing`, and `NotAllowlisted`.
+A Host holding a current registry snapshot answers `list` locally. `CreateAccountProofRequest` and `GetAccountAliasRequest` carry `key_handle: ProductAccountId` and `calling_product_id`. The `RingVrfSignRequest` / `RingVrfSignResponse` pair mirrors `account_ring_vrf_sign` with the same two request fields plus `message`. `RingVrfError` includes `KeyNotRegistered`, `KeyNotInRing`, and `NotAllowlisted`.
**Registration always reaches the Account Holder, but never blocks on it.** The phone is the authoritative registry — it needs the complete set to serve slot assignment and PGAS claims, and to show the user what their keys are used for. A Host holding the product's domain entropy answers immediately and mirrors the registration fire-and-forget; registration is idempotent, so re-notifying the phone about an entry it already has costs nothing. Without the entropy the Host issues the request and waits.
diff --git a/js/packages/truapi/src/wire-equality.test.ts b/js/packages/truapi/src/wire-equality.test.ts
index af656c48f..fed3fe014 100644
--- a/js/packages/truapi/src/wire-equality.test.ts
+++ b/js/packages/truapi/src/wire-equality.test.ts
@@ -11,6 +11,7 @@ import { describe, expect, it } from "bun:test";
import { str } from "./scale.js";
import { decodeWireMessage, encodeWireMessage } from "./transport.js";
import * as W from "./generated/wire-table.js";
+import { HostSignPayloadRequest } from "./generated/types.js";
function toHex(u: Uint8Array): string {
return Array.from(u)
@@ -38,6 +39,43 @@ function unwrap(result: Result, message: string): T {
}
describe("encodeWireMessage / decodeWireMessage wire equality", () => {
+ it("matches Rust signing bytes for absent, true, and false transaction flags", () => {
+ for (const [flag, byte] of [
+ [undefined, "00"],
+ [true, "01"],
+ [false, "02"],
+ ] as const) {
+ const request: HostSignPayloadRequest = {
+ account: {
+ dotNsIdentifier: "myapp.dot",
+ derivationIndex: { tag: "Index", value: 7 },
+ },
+ payload: {
+ blockHash: "0x",
+ blockNumber: "0x",
+ era: "0x",
+ genesisHash: "0x",
+ method: "0x",
+ nonce: "0x",
+ specVersion: "0x",
+ tip: "0x",
+ transactionVersion: "0x",
+ signedExtensions: [],
+ version: 4,
+ assetId: undefined,
+ metadataHash: undefined,
+ mode: undefined,
+ withSignedTransaction: flag,
+ },
+ };
+ const encoded = HostSignPayloadRequest.enc(request);
+ expect(toHex(encoded)).toBe(
+ `246d796170702e646f7400070000000000000000000000000004000000000000${byte}`,
+ );
+ expect(HostSignPayloadRequest.dec(encoded)).toEqual(request);
+ }
+ });
+
it("encodes handshake_request (discriminant 0) to match the Rust reference", () => {
const inner = new Uint8Array([0x00, 0x01]); // V1 variant + codec_version=1
const encoded = unwrap(
diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs
index 52e139637..838235f1c 100644
--- a/rust/crates/truapi-codegen/src/ts.rs
+++ b/rust/crates/truapi-codegen/src/ts.rs
@@ -2883,6 +2883,7 @@ fn ts_inner_option(ty: &TypeRef) -> Result {
fn ts_inner_option_with_named(ty: &TypeRef, qualified: bool, mode: NameMode<'_>) -> Result {
match ty {
TypeRef::Option(inner) => ts_type_with_named(inner, qualified, mode),
+ TypeRef::Primitive(name) if name == "optionBool" => Ok("boolean".to_string()),
other => ts_type_with_named(other, qualified, mode),
}
}
@@ -2897,7 +2898,8 @@ fn ts_type_qualified_preserve(ty: &TypeRef) -> Result {
fn ts_field_name(name: &str, ty: &TypeRef) -> (String, bool) {
let camel = to_camel_case(name);
- let optional = matches!(ty, TypeRef::Option(_));
+ let optional = matches!(ty, TypeRef::Option(_))
+ || matches!(ty, TypeRef::Primitive(name) if name == "optionBool");
(camel, optional)
}
@@ -3054,6 +3056,19 @@ mod tests {
}
}
+ #[test]
+ fn option_bool_fields_are_optional_and_keep_their_compact_codec() {
+ let api = api_with_payload_fields(vec![(
+ "with_signed_transaction",
+ TypeRef::Primitive("optionBool".to_string()),
+ )]);
+
+ let source = generate_types(&api, 1).expect("generate types");
+
+ assert!(source.contains("withSignedTransaction?: boolean;"));
+ assert!(source.contains("withSignedTransaction: S.OptionBool"));
+ }
+
#[test]
fn schema_hash_moves_when_a_payload_field_type_changes() {
// The drift class this fingerprint exists to catch: same frame ids, same
diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs
index 5d59d7f93..278c6b671 100644
--- a/rust/crates/truapi-codegen/tests/golden/wire_table.rs
+++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs
@@ -50,7 +50,7 @@ pub enum WireKind {
/// `TRUAPI_WIRE_SCHEMA_HASH`. A host stamps it on each debug envelope so
/// the debugger refuses to decode a frame whose contract differs from
/// its own, even when the coarse handshake codec version is unchanged.
-pub const TRUAPI_WIRE_SCHEMA_HASH: &str = "0449982638d57658";
+pub const TRUAPI_WIRE_SCHEMA_HASH: &str = "43581e5572c0315a";
/// Wire discriminants for `system_handshake`.
pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds {
diff --git a/rust/crates/truapi-server/Cargo.toml b/rust/crates/truapi-server/Cargo.toml
index 390556255..2af00655d 100644
--- a/rust/crates/truapi-server/Cargo.toml
+++ b/rust/crates/truapi-server/Cargo.toml
@@ -33,6 +33,7 @@ ws-bridge = ["dep:tokio", "dep:tokio-tungstenite", "dep:rand", "dep:base64"]
[dependencies]
truapi = { path = "../truapi" }
truapi-platform = { path = "../truapi-platform" }
+truapi-macros = { path = "../truapi-macros" }
async-trait = "0.1"
derive_more = { version = "2", features = ["debug", "display", "error", "from"] }
futures = "0.3"
diff --git a/rust/crates/truapi-server/README.md b/rust/crates/truapi-server/README.md
index ae4369389..1e391e642 100644
--- a/rust/crates/truapi-server/README.md
+++ b/rust/crates/truapi-server/README.md
@@ -250,6 +250,32 @@ role-specific lifecycle, so no method exists on a role that can't mean it:
session/SSO crypto, key derivation, and permission policy, while all I/O
(statement-store RPC, storage, prompts, chain RPC) stays in the layers above.
+### Inter-host SSO
+
+`PairingHost::call(request)` sends typed requests to
+[`SigningHostSsoService`](src/runtime/signing_host/sso_service.rs). Handlers own
+consent and business logic; `sso_responder.rs` owns the transport loop and shared
+allowance helpers. Resource consent is bound to the request's signing session:
+account changes, disconnects, and reactivation invalidate pending approval before
+allocation or key return. Allocation failure details stay in local transcripts.
+Allocation requests use the canonical `truapi::latest::AllocatableResource` type.
+Signing uses canonical request and result types. Product-scoped VRF requests use
+`ProductRequest` to attach the caller to a canonical payload. Both product and
+SSO signing encode `with_signed_transaction` with the one-byte `OptionBool` codec.
+
+The `host_logic::sso::messages::v1::RemoteMessage` enum owns the SCALE wire
+contract. Its response variants wrap named result payloads in `Response
`,
+which carries `responding_to` once. Macros generate request/response pairing
+and dispatch; see the
+[macro guide](../truapi-macros/README.md) for handler signatures and reply handling.
+A new operation needs payload definitions, wire variants, a handler, and a typed
+client call.
+
+Rust consumers must update renamed SSO types and helpers even when SCALE encoding
+is unchanged. Use `RemoteMessage::request(message_id, request)` to construct
+requests. Decoded `SsoSessionStatement::RemoteMessages` preserves message order;
+match variants directly or use the request's `SsoRequest::response_from_message`.
+
## Wire envelope
Every frame on the wire is encoded as:
diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs
index c0863017e..b5b79c34a 100644
--- a/rust/crates/truapi-server/src/host_core.rs
+++ b/rust/crates/truapi-server/src/host_core.rs
@@ -30,11 +30,12 @@ use truapi_platform::{
use crate::core::TrUApiCore;
use crate::frame::ProtocolMessage;
-use crate::host_logic::sso::messages::{RemoteMessage, RemoteMessageData, SsoRequestOutcome, v1};
+use crate::host_logic::sso::messages::{RemoteMessage, SsoRequestOutcome};
+use crate::runtime::sso_service::Dispatch;
use crate::runtime::{
ChatConnection, DEFAULT_REMOTE_AUTHORITY_RESPONSE_TIMEOUT, LocalActivation, PairedSsoPeer,
PairingHostRole, ProductAuthority, ProductRuntimeHost, ResponderExit, RuntimeServices,
- SigningHostRole, answer_remote_message, establish_pairing, respond_to_pairing, resume_pairing,
+ SigningHostRole, SigningHostSsoService, establish_pairing, respond_to_pairing, resume_pairing,
};
use crate::subscription::{HostInitiatedSubscriptionManager, Spawner};
use crate::transport::Transport;
@@ -780,20 +781,11 @@ impl SigningHostRuntime {
&self,
message: RemoteMessage,
) -> SsoRequestOutcome {
- let RemoteMessageData::V1(request) = message.data;
- if matches!(request, v1::RemoteMessage::Disconnected) {
- return SsoRequestOutcome::Disconnected;
- }
- match answer_remote_message(
- &self.services,
- &self.signing_host,
- message.message_id,
- request,
- )
- .await
- {
- Some(answer) => SsoRequestOutcome::Response(answer.response),
- None => SsoRequestOutcome::Ignored,
+ let service = SigningHostSsoService::new(self.signing_host.clone());
+ match service.dispatch(service.current_session(), message).await {
+ Dispatch::Response(answer) => SsoRequestOutcome::Response(answer.message),
+ Dispatch::Disconnected => SsoRequestOutcome::Disconnected,
+ Dispatch::NotARequest(_) => SsoRequestOutcome::Ignored,
}
}
}
@@ -2245,9 +2237,7 @@ mod tests {
#[test]
fn answer_sso_request_distinguishes_disconnect_from_ignorable_messages() {
- use crate::host_logic::sso::messages::{
- RemoteMessage, RemoteMessageData, SignRawLegacyResponse, v1,
- };
+ use crate::host_logic::sso::messages::{RemoteMessage, RemoteMessageData, Response, v1};
use truapi_platform::{HostInfo, PlatformInfo, SigningHostConfig};
const ENTROPY: [u8; 32] = [0xab; 32];
@@ -2279,10 +2269,10 @@ mod tests {
let response_variant = RemoteMessage {
message_id: "m2".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::SignRawLegacyResponse(
- SignRawLegacyResponse {
+ data: RemoteMessageData::V1(v1::RemoteMessage::SignRawWithLegacyAccountResponse(
+ Response {
responding_to: "m2".to_string(),
- signature: Ok(vec![]),
+ payload: Ok(vec![]),
},
)),
};
@@ -2336,6 +2326,6 @@ mod tests {
panic!("expected a product subtree response payload");
};
assert_eq!(payload.responding_to, "m3");
- assert!(payload.product_public_key.is_ok());
+ assert!(payload.payload.is_ok());
}
}
diff --git a/rust/crates/truapi-server/src/host_logic/sso.rs b/rust/crates/truapi-server/src/host_logic/sso.rs
index 04f2cd46e..35271ad74 100644
--- a/rust/crates/truapi-server/src/host_logic/sso.rs
+++ b/rust/crates/truapi-server/src/host_logic/sso.rs
@@ -1,6 +1,7 @@
//! Inter-host SSO with the paired wallet: `pairing` bootstraps the
//! QR/deeplink handshake, `messages` carries the session-channel payloads
-//! exchanged afterwards.
+//! exchanged afterwards, `wire` types the request/response pairing.
pub mod messages;
pub mod pairing;
+pub mod wire;
diff --git a/rust/crates/truapi-server/src/host_logic/sso/messages.rs b/rust/crates/truapi-server/src/host_logic/sso/messages.rs
index 076703c62..bf66c958c 100644
--- a/rust/crates/truapi-server/src/host_logic/sso/messages.rs
+++ b/rust/crates/truapi-server/src/host_logic/sso/messages.rs
@@ -1,11 +1,10 @@
//! SCALE codecs for host-papp SSO session-channel messages.
//!
//! These are the encrypted payloads carried inside statement-store
-//! `SsoStatementData::Request` / `Response` frames.
-//! The runtime builds them when forwarding TrUAPI account, signing, resource
-//! allocation, and transaction requests to the paired signing host, then
-//! decodes the signing host's responses while waiting on the SSO
-//! statement-store channels.
+//! `SsoStatementData::Request` / `Response` frames. A pairing host sends a
+//! request with [`RemoteMessage::request`]; each wire response carries a
+//! [`Response`] envelope. Annotated handler signatures pair requests with their
+//! response variants (see `sso::wire`).
//! The encrypted statement envelope and message identifiers are specified in
//! host-spec:
//!
@@ -23,13 +22,13 @@
use core::fmt;
-use parity_scale_codec::{Decode, Encode, OptionBool};
+use parity_scale_codec::{Decode, Encode};
use truapi::latest::{
- AccountId, AllocatableResource, DerivationIndex, HostAccountCreateProofResponse,
- HostAccountGetAliasResponse, LegacyAccountTxPayload, ProductAccountId, ProductAccountTxPayload,
- ProductProofContext, RawPayload, RingLocation,
+ AccountId, AllocatableResource, HostAccountCreateProofResponse, HostAccountGetAliasResponse,
+ HostAccountSignVrfError, HostSignPayloadRequest, HostSignPayloadResponse, HostSignRawRequest,
+ LegacyAccountTxPayload, ProductAccountTxPayload, RawPayload, RegisteredRingVrfKey,
+ VrfSignature,
};
-use truapi::v01::{HostAccountSignVrfError, HostAccountSignVrfRequest, VrfSignature};
use crate::host_logic::session::SsoSessionInfo;
use crate::host_logic::sso::pairing::{
@@ -75,8 +74,7 @@ impl TryFrom for SsoResponseCode {
}
/// Top-level remote message sent over the encrypted SSO channel.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)]
-#[display("{data}")]
+#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct RemoteMessage {
/// Correlation id used to match signing-host responses to pairing-host requests.
pub message_id: String,
@@ -85,13 +83,23 @@ pub struct RemoteMessage {
}
/// Versioned remote message body.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)]
+#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum RemoteMessageData {
/// Version 1 of the remote message catalog.
- #[display("{_0}")]
V1(v1::RemoteMessage),
}
+/// A response payload addressed to the request it answers.
+///
+/// SCALE encodes the correlation id before the payload for every response variant.
+#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
+pub struct Response {
+ /// `message_id` of the request being answered.
+ pub responding_to: String,
+ /// The operation's result, without transport metadata.
+ pub payload: P,
+}
+
/// Outcome of answering one SSO remote message on behalf of a caller that
/// owns the session transport. Generic over the response representation:
/// the typed runtime layer carries a decoded [`RemoteMessage`], the FFI
@@ -107,127 +115,24 @@ pub enum SsoRequestOutcome {
Ignored,
}
-/// Signing request flavor sent to the signing host.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub enum SigningRequest {
- /// Sign a full Substrate extrinsic payload.
- Payload(Box),
- /// Sign raw bytes or a string message.
- Raw(SigningRawRequest),
-}
-
-/// Request sent when a product asks the paired signing host to sign a Substrate
-/// payload with a product-derived account.
+/// A product's canonical request payload and the identity of its caller.
///
-/// Built from [`truapi::v01::HostSignPayloadRequest`] but kept as a dedicated wire type
-/// because the host-papp SSO dialect flattens the public request payload and
-/// encodes `with_signed_transaction` as `OptionBool`.
+/// SCALE encodes the caller followed directly by the payload fields.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SigningPayloadRequest {
- /// Product account that signs the payload.
- pub product_account_id: ProductAccountId,
- /// Reference block hash.
- pub block_hash: Vec,
- /// Reference block number.
- pub block_number: Vec,
- /// Mortality era encoding.
- pub era: Vec,
- /// Chain genesis hash.
- pub genesis_hash: Vec,
- /// SCALE-encoded call data.
- pub method: Vec,
- /// Account nonce.
- pub nonce: Vec,
- /// Runtime spec version.
- pub spec_version: Vec,
- /// Transaction tip.
- pub tip: Vec,
- /// Transaction format version.
- pub transaction_version: Vec,
- /// Extension identifiers.
- pub signed_extensions: Vec,
- /// Extrinsic version.
- pub version: u32,
- /// For multi-asset tips.
- pub asset_id: Option>,
- /// CheckMetadataHash extension.
- pub metadata_hash: Option>,
- /// Metadata mode.
- pub mode: Option,
- /// Request the full signed transaction back.
- pub with_signed_transaction: OptionBool,
-}
-
-impl SigningPayloadRequest {
- fn from_host_request(value: truapi::v01::HostSignPayloadRequest) -> Self {
- let payload = value.payload;
- Self {
- product_account_id: value.account,
- block_hash: payload.block_hash,
- block_number: payload.block_number,
- era: payload.era,
- genesis_hash: payload.genesis_hash,
- method: payload.method,
- nonce: payload.nonce,
- spec_version: payload.spec_version,
- tip: payload.tip,
- transaction_version: payload.transaction_version,
- signed_extensions: payload.signed_extensions,
- version: payload.version,
- asset_id: payload.asset_id,
- metadata_hash: payload.metadata_hash,
- mode: payload.mode,
- with_signed_transaction: OptionBool(payload.with_signed_transaction),
- }
- }
-}
-
-impl From for truapi::v01::HostSignPayloadRequest {
- fn from(value: SigningPayloadRequest) -> Self {
- Self {
- account: value.product_account_id,
- payload: truapi::v01::HostSignPayloadData {
- block_hash: value.block_hash,
- block_number: value.block_number,
- era: value.era,
- genesis_hash: value.genesis_hash,
- method: value.method,
- nonce: value.nonce,
- spec_version: value.spec_version,
- tip: value.tip,
- transaction_version: value.transaction_version,
- signed_extensions: value.signed_extensions,
- version: value.version,
- asset_id: value.asset_id,
- metadata_hash: value.metadata_hash,
- mode: value.mode,
- with_signed_transaction: value.with_signed_transaction.0,
- },
- }
- }
+pub struct ProductRequest {
+ /// Product making the request.
+ pub calling_product_id: String,
+ /// Canonical payload sent to the signing host.
+ pub payload: P,
}
-/// Request sent when a product asks the paired signing host to sign raw bytes or a
-/// string message with a product-derived account.
-///
-/// Built from [`truapi::v01::HostSignRawRequest`] and wrapped in
-/// [`v1::RemoteMessage::SignRequest`] before being encrypted into an SSO session
-/// statement.
+/// Signing request flavor sent to the signing host.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SigningRawRequest {
- /// Product account that signs the payload.
- pub product_account_id: ProductAccountId,
- /// Raw bytes or string message to sign.
- pub data: SigningRawPayload,
-}
-
-impl SigningRawRequest {
- fn from_host_request(value: truapi::v01::HostSignRawRequest) -> Self {
- Self {
- product_account_id: value.account,
- data: value.payload.into(),
- }
- }
+pub enum SignRequest {
+ /// Sign a full Substrate extrinsic payload.
+ Payload(Box),
+ /// Sign raw bytes or a string message.
+ Raw(HostSignRawRequest),
}
/// Request sent when a product asks the paired signing host to sign raw data with a
@@ -236,106 +141,30 @@ impl SigningRawRequest {
/// Unlike product-account signing, the signer is the raw account id selected
/// from the user's legacy accounts.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SignRawLegacyRequest {
+pub struct SignRawWithLegacyAccountRequest {
/// Legacy account that signs the payload.
pub account: AccountId,
/// Raw bytes or string message to sign.
- pub data: SigningRawPayload,
-}
-
-/// Raw data accepted by SSO signing requests.
-///
-/// Used by both product-account raw signing and legacy-account raw signing to
-/// distinguish binary payloads from string messages on the session-channel
-/// wire.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub enum SigningRawPayload {
- /// Raw binary payload.
- Bytes(Vec),
- /// String message payload.
- Payload(String),
-}
-
-impl From for SigningRawPayload {
- fn from(value: RawPayload) -> Self {
- match value {
- RawPayload::Bytes { bytes } => Self::Bytes(bytes),
- RawPayload::Payload { payload } => Self::Payload(payload),
- }
- }
-}
-
-impl From for RawPayload {
- fn from(value: SigningRawPayload) -> Self {
- match value {
- SigningRawPayload::Bytes(bytes) => Self::Bytes { bytes },
- SigningRawPayload::Payload(payload) => Self::Payload { payload },
- }
- }
-}
-
-impl From for truapi::v01::HostSignRawRequest {
- fn from(value: SigningRawRequest) -> Self {
- Self {
- account: value.product_account_id,
- payload: value.data.into(),
- }
- }
+ pub data: RawPayload,
}
/// Response returned by the signing host for a product-account signing request.
///
/// Decoded from [`v1::RemoteMessage::SignResponse`] while the runtime is waiting
/// for a matching SSO remote message id.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SigningResponse {
- /// `message_id` of the signing request being answered.
- pub responding_to: String,
- /// Signing result, or an error description from the signing host.
- pub payload: Result,
-}
-
-/// Successful product-account signing result returned by the signing host.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SigningPayloadResponseData {
- /// The cryptographic signature.
- pub signature: Vec,
- /// Full signed transaction, when the request asked for it.
- pub signed_transaction: Option>,
-}
+pub type SignResponse = Result;
/// Response returned by the signing host for a legacy-account raw signing request.
///
-/// Decoded from [`v1::RemoteMessage::SignRawLegacyResponse`] and mapped back to
+/// Decoded from [`v1::RemoteMessage::SignRawWithLegacyAccountResponse`] and mapped back to
/// the public raw-signing response shape.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SignRawLegacyResponse {
- /// `message_id` of the legacy raw-signing request being answered.
- pub responding_to: String,
- /// Signature bytes, or an error description from the signing host.
- pub signature: Result, String>,
-}
-
-/// RFC-0023 VRF-signing request forwarded to the Account Holder.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SignVrfRequest {
- /// Product making the request, used for the Account Holder confirmation.
- pub calling_product_id: String,
- /// Product account and ordered Merlin transcript.
- pub payload: HostAccountSignVrfRequest,
-}
+pub type SignRawWithLegacyAccountResponse = Result, String>;
/// RFC-0023 VRF-signing response returned by the Account Holder.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct SignVrfResponse {
- /// `message_id` of the VRF-signing request being answered.
- pub responding_to: String,
- /// Fixed-width schnorrkel VRF signature or the public API error.
- pub payload: Result,
-}
+pub type SignVrfResponse = Result;
/// Failure returned by the Account Holder for RFC-0024 ring-VRF operations.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
+#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)]
pub enum RingVrfError {
/// The `RingLocation` did not resolve to a known ring.
RingNotFound,
@@ -350,124 +179,27 @@ pub enum RingVrfError {
/// User or Account Holder rejected the request.
Rejected,
/// Catch-all failure, carrying a diagnostic reason.
+ #[display("Unknown: {reason}")]
Unknown {
/// Diagnostic failure description.
reason: String,
},
}
-/// Request sent when a product asks the Account Holder for a contextual alias.
-///
-/// Used by `Account::get_account_alias`; `calling_product_id` names the caller,
-/// `key_handle` selects a registered member key, and `context` binds the alias.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfAliasRequest {
- /// Product id of the calling product.
- pub calling_product_id: String,
- /// Explicit ring-VRF key handle.
- pub key_handle: ProductAccountId,
- /// Context that scopes the derived alias.
- pub context: ProductProofContext,
- /// Ring whose member key derives the alias.
- pub ring_location: RingLocation,
-}
-
/// Response returned by the Account Holder for a ring-VRF alias request.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfAliasResponse {
- /// `message_id` of the alias request being answered.
- pub responding_to: String,
- /// Derived alias, or the ring-VRF failure.
- pub payload: Result,
-}
-
-/// Request sent when a product asks the Account Holder for a ring-VRF proof.
-///
-/// Used by `Account::create_account_proof`; carries the same `(context,
-/// ring_location)` as the alias request plus the opaque `message` bound into
-/// the proof (RFC 0004).
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfProofRequest {
- /// Product id of the calling product.
- pub calling_product_id: String,
- /// Explicit ring-VRF key handle.
- pub key_handle: ProductAccountId,
- /// Context that scopes the proof.
- pub context: ProductProofContext,
- /// Ring whose member key produces the proof.
- pub ring_location: RingLocation,
- /// Opaque message bound into the proof.
- pub message: Vec,
-}
-
-/// Request to register a ring-VRF key with the Account Holder.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RegisterRingVrfKeyRequest {
- /// Product id of the calling product and key owner.
- pub calling_product_id: String,
- /// Key derivation index within the owner's ring-VRF domain.
- pub index: DerivationIndex,
- /// Ring declared for the key.
- pub ring: RingLocation,
-}
+pub type GetAccountAliasResponse = Result;
/// Response returned by the Account Holder for key registration.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RegisterRingVrfKeyResponse {
- /// `message_id` of the registration request being answered.
- pub responding_to: String,
- /// Member public key or ring-VRF failure.
- pub payload: Result<[u8; 32], RingVrfError>,
-}
-
-/// Request to list registered ring-VRF keys.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct ListRingVrfKeysRequest {
- /// Product id of the calling product.
- pub calling_product_id: String,
- /// Product whose registry entries should be listed.
- pub owner: String,
- /// Disclosure requested by the caller.
- pub disclosure: truapi::v01::RingVrfKeyDisclosure,
-}
+pub type RegisterRingVrfKeyResponse = Result<[u8; 32], RingVrfError>;
/// Response returned by the Account Holder for registry listing.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct ListRingVrfKeysResponse {
- /// `message_id` of the listing request being answered.
- pub responding_to: String,
- /// Registry entries or ring-VRF failure.
- pub payload: Result, RingVrfError>,
-}
-
-/// Request to sign bytes with a ring-VRF key.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfSignRequest {
- /// Product id of the calling product.
- pub calling_product_id: String,
- /// Registered key handle.
- pub key_handle: ProductAccountId,
- /// Message to sign.
- pub message: Vec,
-}
+pub type ListRingVrfKeysResponse = Result, RingVrfError>;
/// Response returned by the Account Holder for direct ring-VRF signing.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfSignResponse {
- /// `message_id` of the signing request being answered.
- pub responding_to: String,
- /// Signature bytes or ring-VRF failure.
- pub payload: Result, RingVrfError>,
-}
+pub type RingVrfSignResponse = Result, RingVrfError>;
/// Response returned by the Account Holder for a ring-VRF proof request.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct RingVrfProofResponse {
- /// `message_id` of the proof request being answered.
- pub responding_to: String,
- /// Created proof, or the ring-VRF failure.
- pub payload: Result,
-}
+pub type CreateAccountProofResponse = Result;
/// Request sent when a product asks the signing host to allocate SSO-backed
/// resources.
@@ -480,38 +212,11 @@ pub struct ResourceAllocationRequest {
/// Product id the allocation is requested for.
pub calling_product_id: String,
/// Resources to allocate; outcomes come back in the same order.
- pub resources: Vec,
+ pub resources: Vec,
/// Policy applied when an allocation already exists for this product.
pub on_existing: OnExistingAllowancePolicy,
}
-/// Resources the signing host may allocate for the calling product.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub enum SsoAllocatableResource {
- /// Statement Store slot allowance for the product's allowance account.
- StatementStoreAllowance,
- /// Bulletin chain slot allowance for the product's allowance account.
- BulletinAllowance,
- /// Pre-warmed PGAS balance for the product account selected by this
- /// derivation index.
- SmartContractAllowance(DerivationIndex),
- /// Transfer of the product subtree key so the host can sign locally.
- AutoSigning,
-}
-
-impl From for SsoAllocatableResource {
- fn from(value: AllocatableResource) -> Self {
- match value {
- AllocatableResource::StatementStoreAllowance => Self::StatementStoreAllowance,
- AllocatableResource::BulletinAllowance => Self::BulletinAllowance,
- AllocatableResource::SmartContractAllowance(index) => {
- Self::SmartContractAllowance(index)
- }
- AllocatableResource::AutoSigning => Self::AutoSigning,
- }
- }
-}
-
/// Signing-host policy for already-existing resource allowance.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub enum OnExistingAllowancePolicy {
@@ -522,13 +227,7 @@ pub enum OnExistingAllowancePolicy {
}
/// Response returned by the signing host for a resource-allocation request.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct ResourceAllocationResponse {
- /// `message_id` of the allocation request being answered.
- pub responding_to: String,
- /// Per-resource outcomes in request order, or an error description.
- pub payload: Result, String>,
-}
+pub type ResourceAllocationResponse = Result, String>;
/// Per-resource allocation result from the signing host.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
@@ -591,13 +290,7 @@ pub struct ProductSubtreeRequest {
}
/// Account Holder response carrying a product subtree public key.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct ProductSubtreeResponse {
- /// `message_id` of the subtree request being answered.
- pub responding_to: String,
- /// Raw 32-byte sr25519 subtree public key or a stable failure reason.
- pub product_public_key: Result<[u8; 32], String>,
-}
+pub type ProductSubtreeResponse = Result<[u8; 32], String>;
/// Request sent when a product asks the signing host to create a transaction
/// for a product-derived account.
@@ -617,7 +310,7 @@ pub enum CreateTransactionPayload {
/// Request sent when a product asks the signing host to create a transaction
/// for a user-imported legacy account.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct CreateTransactionLegacyRequest {
+pub struct CreateTransactionWithLegacyAccountRequest {
/// Transaction payload to build.
pub payload: CreateTransactionLegacyPayload,
}
@@ -629,72 +322,18 @@ pub enum CreateTransactionLegacyPayload {
V1(LegacyAccountTxPayload),
}
-/// Response returned by the signing host for either product-account or legacy-account
-/// transaction creation.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
-pub struct CreateTransactionResponse {
- /// `message_id` of the transaction-creation request being answered.
- pub responding_to: String,
- /// SCALE-encoded transaction, or an error description. Signed unless the
- /// request supplied its own V5 `VerifyMultiSignature` extension.
- pub signed_transaction: Result, String>,
-}
+/// SCALE-encoded transaction for a product or legacy account, or an error description.
+/// Signed unless the request supplied its own V5 `VerifyMultiSignature` extension.
+pub type CreateTransactionResponse = Result, String>;
/// Decoded inbound statement-channel outcome.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SsoSessionStatement {
/// The outbound request statement was acknowledged with a success code.
RequestAccepted,
- /// Remote response matching the pending remote message id.
- RemoteResponse(SsoRemoteResponse),
- /// The peer ended the SSO session.
- Disconnected,
-}
-
-/// Signing-host response variants that can satisfy a pending remote request.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum SsoRemoteResponse {
- /// Product-account signing response.
- Sign(SigningResponse),
- /// Legacy-account raw-signing response.
- SignRawLegacy(SignRawLegacyResponse),
- /// Product-account VRF signing response.
- SignVrf(SignVrfResponse),
- /// Contextual-alias response.
- RingVrfAlias(RingVrfAliasResponse),
- /// Ring-VRF proof response.
- RingVrfProof(RingVrfProofResponse),
- /// Resource-allocation response.
- ResourceAllocation(ResourceAllocationResponse),
- /// Transaction-creation response.
- CreateTransaction(CreateTransactionResponse),
- /// Product subtree public-key response.
- ProductSubtree(ProductSubtreeResponse),
- /// Ring-VRF key registration response.
- RegisterRingVrfKey(RegisterRingVrfKeyResponse),
- /// Ring-VRF key listing response.
- ListRingVrfKeys(ListRingVrfKeysResponse),
- /// Direct ring-VRF signing response.
- RingVrfSign(RingVrfSignResponse),
-}
-
-impl SsoRemoteResponse {
- /// Stable response discriminant that never formats response payloads.
- pub(crate) const fn kind(&self) -> &'static str {
- match self {
- Self::Sign(_) => "sign",
- Self::SignRawLegacy(_) => "sign-raw-legacy",
- Self::SignVrf(_) => "sign-vrf",
- Self::RingVrfAlias(_) => "ring-vrf-alias",
- Self::RingVrfProof(_) => "ring-vrf-proof",
- Self::ResourceAllocation(_) => "resource-allocation",
- Self::CreateTransaction(_) => "create-transaction",
- Self::ProductSubtree(_) => "product-subtree",
- Self::RegisterRingVrfKey(_) => "register-ring-vrf-key",
- Self::ListRingVrfKeys(_) => "list-ring-vrf-keys",
- Self::RingVrfSign(_) => "ring-vrf-sign",
- }
- }
+ /// Application messages in wire order, each decoded on its own so a
+ /// consumer can stop at its match before a later undecodable message.
+ RemoteMessages(Vec>),
}
/// Decode and classify an inbound encrypted SSO session statement.
@@ -702,7 +341,6 @@ pub fn decode_sso_session_statement(
session: &SsoSessionInfo,
statement: &[u8],
expected_statement_request_id: &str,
- expected_remote_message_id: &str,
) -> Result, String> {
let verified =
decode_verified_statement_data(statement, None).map_err(|err| err.to_string())?;
@@ -738,23 +376,16 @@ pub fn decode_sso_session_statement(
classify_response_ack(request_id, response_code).map(Some)
}
SsoStatementData::Response { .. } => Ok(None),
- SsoStatementData::Request { data, .. } => {
- for message in data {
- let message = decode_remote_message(&message)?;
- if matches!(
- &message.data,
- RemoteMessageData::V1(v1::RemoteMessage::Disconnected)
- ) {
- return Ok(Some(SsoSessionStatement::Disconnected));
- }
- if let Some(response) =
- remote_response_for_message(message, expected_remote_message_id)
- {
- return Ok(Some(SsoSessionStatement::RemoteResponse(response)));
- }
- }
- Ok(None)
- }
+ SsoStatementData::Request { data, .. } => Ok(Some(SsoSessionStatement::RemoteMessages(
+ data.iter()
+ .map(|message| {
+ decode_remote_message(message).map(|message| {
+ let RemoteMessageData::V1(message) = message.data;
+ message
+ })
+ })
+ .collect(),
+ ))),
}
}
@@ -769,287 +400,6 @@ fn classify_response_ack(
}
}
-fn remote_response_for_message(
- message: RemoteMessage,
- expected_remote_message_id: &str,
-) -> Option {
- let RemoteMessageData::V1(data) = message.data;
- match data {
- v1::RemoteMessage::SignResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::Sign(response))
- }
- v1::RemoteMessage::RingVrfAliasResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::RingVrfAlias(response))
- }
- v1::RemoteMessage::RingVrfProofResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::RingVrfProof(response))
- }
- v1::RemoteMessage::SignRawLegacyResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::SignRawLegacy(response))
- }
- v1::RemoteMessage::SignVrfResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::SignVrf(response))
- }
- v1::RemoteMessage::ResourceAllocationResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::ResourceAllocation(response))
- }
- v1::RemoteMessage::CreateTransactionResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::CreateTransaction(response))
- }
- v1::RemoteMessage::ProductSubtreeResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::ProductSubtree(response))
- }
- v1::RemoteMessage::RegisterRingVrfKeyResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::RegisterRingVrfKey(response))
- }
- v1::RemoteMessage::ListRingVrfKeysResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::ListRingVrfKeys(response))
- }
- v1::RemoteMessage::RingVrfSignResponse(response)
- if response.responding_to == expected_remote_message_id =>
- {
- Some(SsoRemoteResponse::RingVrfSign(response))
- }
- _ => None,
- }
-}
-
-/// Build an RFC-0024 ring-VRF key registration request for the Account Holder.
-pub fn register_ring_vrf_key_message(
- message_id: String,
- calling_product_id: String,
- index: DerivationIndex,
- ring: RingLocation,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::RegisterRingVrfKeyRequest(
- RegisterRingVrfKeyRequest {
- calling_product_id,
- index,
- ring,
- },
- )),
- }
-}
-
-/// Build an RFC-0024 ring-VRF key listing request for the Account Holder.
-pub fn list_ring_vrf_keys_message(
- message_id: String,
- calling_product_id: String,
- owner: String,
- disclosure: truapi::v01::RingVrfKeyDisclosure,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::ListRingVrfKeysRequest(
- ListRingVrfKeysRequest {
- calling_product_id,
- owner,
- disclosure,
- },
- )),
- }
-}
-
-/// Build an RFC-0024 direct ring-VRF signing request for the Account Holder.
-pub fn ring_vrf_sign_message(
- message_id: String,
- calling_product_id: String,
- key_handle: ProductAccountId,
- message: Vec,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfSignRequest(RingVrfSignRequest {
- calling_product_id,
- key_handle,
- message,
- })),
- }
-}
-
-/// Build an RFC-0023 VRF-signing request for the Account Holder.
-pub fn sign_vrf_message(
- message_id: String,
- calling_product_id: String,
- payload: HostAccountSignVrfRequest,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::SignVrfRequest(SignVrfRequest {
- calling_product_id,
- payload,
- })),
- }
-}
-
-/// Build a signing-host payload-signing request message.
-pub fn sign_payload_message(
- message_id: String,
- request: truapi::v01::HostSignPayloadRequest,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::SignRequest(Box::new(
- SigningRequest::Payload(Box::new(SigningPayloadRequest::from_host_request(request))),
- ))),
- }
-}
-
-/// Build a signing-host raw-signing request message.
-pub fn sign_raw_message(
- message_id: String,
- request: truapi::v01::HostSignRawRequest,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::SignRequest(Box::new(
- SigningRequest::Raw(SigningRawRequest::from_host_request(request)),
- ))),
- }
-}
-
-/// Build a signing-host legacy raw-signing request message.
-pub fn sign_raw_legacy_message(
- message_id: String,
- account: AccountId,
- payload: RawPayload,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::SignRawLegacyRequest(
- SignRawLegacyRequest {
- account,
- data: payload.into(),
- },
- )),
- }
-}
-
-/// Build an Account Holder contextual-alias request message.
-pub fn alias_request_message(
- message_id: String,
- calling_product_id: String,
- key_handle: ProductAccountId,
- context: ProductProofContext,
- ring_location: RingLocation,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasRequest(
- RingVrfAliasRequest {
- calling_product_id,
- key_handle,
- context,
- ring_location,
- },
- )),
- }
-}
-
-/// Build an Account Holder ring-VRF proof request message.
-pub fn proof_request_message(
- message_id: String,
- calling_product_id: String,
- key_handle: ProductAccountId,
- context: ProductProofContext,
- ring_location: RingLocation,
- message: Vec,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofRequest(
- RingVrfProofRequest {
- calling_product_id,
- key_handle,
- context,
- ring_location,
- message,
- },
- )),
- }
-}
-
-/// Build a consent-free product subtree public-key request.
-pub fn product_subtree_request_message(message_id: String, product_id: String) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::ProductSubtreeRequest(
- ProductSubtreeRequest { product_id },
- )),
- }
-}
-
-/// Build a signing-host resource-allocation request message.
-pub fn resource_allocation_message(
- message_id: String,
- calling_product_id: String,
- resources: Vec,
- on_existing: OnExistingAllowancePolicy,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationRequest(
- ResourceAllocationRequest {
- calling_product_id,
- resources: resources.into_iter().map(Into::into).collect(),
- on_existing,
- },
- )),
- }
-}
-
-/// Build a signing-host transaction-creation request message.
-pub fn create_transaction_message(
- message_id: String,
- payload: ProductAccountTxPayload,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::CreateTransactionRequest(
- CreateTransactionRequest {
- payload: CreateTransactionPayload::V1(payload),
- },
- )),
- }
-}
-
-/// Build a signing-host legacy-account transaction-creation request message.
-pub fn create_transaction_legacy_message(
- message_id: String,
- payload: LegacyAccountTxPayload,
-) -> RemoteMessage {
- RemoteMessage {
- message_id,
- data: RemoteMessageData::V1(v1::RemoteMessage::CreateTransactionLegacyRequest(
- CreateTransactionLegacyRequest {
- payload: CreateTransactionLegacyPayload::V1(payload),
- },
- )),
- }
-}
-
/// Inbound request decoded from a peer-signed session statement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IncomingSsoRequest {
@@ -1227,10 +577,16 @@ fn outgoing_request_data(
mod tests {
use super::*;
use crate::host_logic::sso::pairing::decrypt_session_statement_data;
+ use crate::host_logic::sso::wire::SsoRequest;
use crate::host_logic::statement_store::{
StatementField, build_signed_statement, decode_statement_data,
};
+ use crate::test_support::sso_host_and_responder_sessions;
use schnorrkel::{ExpansionMode, MiniSecretKey};
+ use truapi::latest::{
+ DerivationIndex, HostAccountSignVrfRequest, ProductAccountId, ProductProofContext,
+ RingLocation,
+ };
use truapi::latest::{HostSignPayloadData, TxPayloadExtension};
use truapi::v01::RingLocationJunction;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
@@ -1277,19 +633,19 @@ mod tests {
};
assert_eq!(message.encode(), vec![0, 0, 0]);
- assert_eq!(message.to_string(), "disconnected");
+ assert_eq!(message.name(), "Disconnected");
}
#[test]
fn raw_sign_request_uses_remote_message_variant_indices() {
- let message = sign_raw_message(
+ let message = RemoteMessage::request(
"m1".to_string(),
- truapi::latest::HostSignRawRequest {
+ SignRequest::Raw(truapi::latest::HostSignRawRequest {
account: account(),
payload: RawPayload::Bytes {
bytes: vec![0xde, 0xad],
},
- },
+ }),
);
let encoded = message.encode();
@@ -1309,65 +665,82 @@ mod tests {
dot_ns_identifier: "peopl.dot".to_string(),
derivation_index: DerivationIndex::Index(0),
};
- let legacy_tx = create_transaction_legacy_message(
+ let legacy_tx = RemoteMessage::request(
+ String::new(),
+ CreateTransactionWithLegacyAccountRequest {
+ payload: CreateTransactionLegacyPayload::V1(LegacyAccountTxPayload {
+ signer: [1; 32],
+ genesis_hash: [2; 32],
+ call_data: Vec::new(),
+ extensions: Vec::new(),
+ tx_ext_version: 0,
+ }),
+ },
+ )
+ .encode();
+ let legacy_raw = RemoteMessage::request(
String::new(),
- LegacyAccountTxPayload {
- signer: [1; 32],
- genesis_hash: [2; 32],
- call_data: Vec::new(),
- extensions: Vec::new(),
- tx_ext_version: 0,
+ SignRawWithLegacyAccountRequest {
+ account: [1; 32],
+ data: RawPayload::Bytes { bytes: vec![] },
},
)
.encode();
- let legacy_raw =
- sign_raw_legacy_message(String::new(), [1; 32], RawPayload::Bytes { bytes: vec![] })
- .encode();
- let register = register_ring_vrf_key_message(
+ let register = RemoteMessage::request(
String::new(),
- "caller.dot".to_string(),
- DerivationIndex::Index(0),
- ring_location.clone(),
+ ProductRequest {
+ calling_product_id: "caller.dot".to_string(),
+ payload: truapi::latest::HostAccountRegisterRingVrfKeyRequest {
+ index: DerivationIndex::Index(0),
+ ring: ring_location.clone(),
+ },
+ },
)
.encode();
let register_response = RemoteMessage {
message_id: String::new(),
- data: RemoteMessageData::V1(v1::RemoteMessage::RegisterRingVrfKeyResponse(
- RegisterRingVrfKeyResponse {
- responding_to: String::new(),
- payload: Ok([1; 32]),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::RegisterRingVrfKeyResponse(Response {
+ responding_to: String::new(),
+ payload: Ok([1; 32]),
+ })),
}
.encode();
- let list = list_ring_vrf_keys_message(
+ let list = RemoteMessage::request(
String::new(),
- "caller.dot".to_string(),
- "peopl.dot".to_string(),
- truapi::v01::RingVrfKeyDisclosure::Anonymized,
+ ProductRequest {
+ calling_product_id: "caller.dot".to_string(),
+ payload: truapi::latest::HostAccountListRingVrfKeysRequest {
+ owner: "peopl.dot".to_string(),
+ disclosure: truapi::v01::RingVrfKeyDisclosure::Anonymized,
+ },
+ },
)
.encode();
let list_response = RemoteMessage {
message_id: String::new(),
- data: RemoteMessageData::V1(v1::RemoteMessage::ListRingVrfKeysResponse(
- ListRingVrfKeysResponse {
- responding_to: String::new(),
- payload: Ok(Vec::new()),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::ListRingVrfKeysResponse(Response {
+ responding_to: String::new(),
+ payload: Ok(Vec::new()),
+ })),
}
.encode();
- let sign =
- ring_vrf_sign_message(String::new(), "caller.dot".to_string(), key_handle, vec![])
- .encode();
+ let sign = RemoteMessage::request(
+ String::new(),
+ ProductRequest {
+ calling_product_id: "caller.dot".to_string(),
+ payload: truapi::latest::HostAccountRingVrfSignRequest {
+ key_handle,
+ message: vec![],
+ },
+ },
+ )
+ .encode();
let sign_response = RemoteMessage {
message_id: String::new(),
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfSignResponse(
- RingVrfSignResponse {
- responding_to: String::new(),
- payload: Ok(Vec::new()),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfSignResponse(Response {
+ responding_to: String::new(),
+ payload: Ok(Vec::new()),
+ })),
}
.encode();
@@ -1408,20 +781,26 @@ mod tests {
derivation_index: DerivationIndex::Index(0),
};
let messages = [
- v1::RemoteMessage::RegisterRingVrfKeyRequest(RegisterRingVrfKeyRequest {
+ v1::RemoteMessage::RegisterRingVrfKeyRequest(ProductRequest {
calling_product_id: "game.dot".to_string(),
- index: DerivationIndex::Index(4),
- ring: ring.clone(),
+ payload: truapi::latest::HostAccountRegisterRingVrfKeyRequest {
+ index: DerivationIndex::Index(4),
+ ring: ring.clone(),
+ },
}),
- v1::RemoteMessage::ListRingVrfKeysRequest(ListRingVrfKeysRequest {
+ v1::RemoteMessage::ListRingVrfKeysRequest(ProductRequest {
calling_product_id: "game.dot".to_string(),
- owner: "peopl.dot".to_string(),
- disclosure: truapi::v01::RingVrfKeyDisclosure::PublicKey,
+ payload: truapi::latest::HostAccountListRingVrfKeysRequest {
+ owner: "peopl.dot".to_string(),
+ disclosure: truapi::v01::RingVrfKeyDisclosure::PublicKey,
+ },
}),
- v1::RemoteMessage::RingVrfSignRequest(RingVrfSignRequest {
+ v1::RemoteMessage::RingVrfSignRequest(ProductRequest {
calling_product_id: "game.dot".to_string(),
- key_handle: handle,
- message: (0..16).collect(),
+ payload: truapi::latest::HostAccountRingVrfSignRequest {
+ key_handle: handle,
+ message: (0..16).collect(),
+ },
}),
];
let expected = [
@@ -1452,20 +831,28 @@ mod tests {
derivation_index: DerivationIndex::Index(0),
};
- let alias = alias_request_message(
+ let alias = RemoteMessage::request(
"m-alias".to_string(),
- "caller.dot".to_string(),
- key_handle.clone(),
- context.clone(),
- ring_location.clone(),
+ ProductRequest {
+ calling_product_id: "caller.dot".to_string(),
+ payload: truapi::latest::HostAccountGetAliasRequest {
+ key_handle: key_handle.clone(),
+ context: context.clone(),
+ ring_location: ring_location.clone(),
+ },
+ },
);
- let proof = proof_request_message(
+ let proof = RemoteMessage::request(
"m-proof".to_string(),
- "caller.dot".to_string(),
- key_handle,
- context,
- ring_location,
- b"vote".to_vec(),
+ ProductRequest {
+ calling_product_id: "caller.dot".to_string(),
+ payload: truapi::latest::HostAccountCreateProofRequest {
+ key_handle,
+ context,
+ ring_location,
+ message: b"vote".to_vec(),
+ },
+ },
);
assert_host_papp_0_8_11_fixture(
@@ -1486,26 +873,22 @@ mod tests {
};
let alias_response = RemoteMessage {
message_id: "r-alias".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasResponse(
- RingVrfAliasResponse {
- responding_to: "m-alias".to_string(),
- payload: Ok(contextual_alias.clone()),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::GetAccountAliasResponse(Response {
+ responding_to: "m-alias".to_string(),
+ payload: Ok(contextual_alias.clone()),
+ })),
};
let proof_response = RemoteMessage {
message_id: "r-proof".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::RingVrfProofResponse(
- RingVrfProofResponse {
- responding_to: "m-proof".to_string(),
- payload: Ok(HostAccountCreateProofResponse {
- proof: vec![0x55, 0x66],
- contextual_alias,
- ring_index: 7,
- ring_revision: 9,
- }),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::CreateAccountProofResponse(Response {
+ responding_to: "m-proof".to_string(),
+ payload: Ok(HostAccountCreateProofResponse {
+ proof: vec![0x55, 0x66],
+ contextual_alias,
+ ring_index: 7,
+ ring_revision: 9,
+ }),
+ })),
};
assert_host_papp_0_8_11_fixture(
@@ -1531,8 +914,12 @@ mod tests {
#[test]
fn product_subtree_messages_match_mobile_wire_contract() {
- let request =
- product_subtree_request_message("request".to_string(), "browse.dot".to_string());
+ let request = RemoteMessage::request(
+ "request".to_string(),
+ ProductSubtreeRequest {
+ product_id: "browse.dot".to_string(),
+ },
+ );
assert_eq!(
hex::encode(request.encode()),
"1c7265717565737400102862726f7773652e646f74"
@@ -1540,12 +927,10 @@ mod tests {
let response = RemoteMessage {
message_id: "response".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::ProductSubtreeResponse(
- ProductSubtreeResponse {
- responding_to: "request".to_string(),
- product_public_key: Ok([0xAB; 32]),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::ProductSubtreeResponse(Response {
+ responding_to: "request".to_string(),
+ payload: Ok([0xAB; 32]),
+ })),
};
assert_eq!(
hex::encode(response.encode()),
@@ -1554,12 +939,13 @@ mod tests {
"ab".repeat(32)
)
);
+ let RemoteMessageData::V1(data) = response.data;
assert_eq!(
- remote_response_for_message(response, "request"),
- Some(SsoRemoteResponse::ProductSubtree(ProductSubtreeResponse {
+ ProductSubtreeRequest::response_from_message(data),
+ Some(Response {
responding_to: "request".to_string(),
- product_public_key: Ok([0xAB; 32]),
- }))
+ payload: Ok([0xAB; 32]),
+ })
);
}
@@ -1576,7 +962,13 @@ mod tests {
value: vec![1, 2],
}],
};
- let request = sign_vrf_message("req".to_string(), "browse.dot".to_string(), payload);
+ let request = RemoteMessage::request(
+ "req".to_string(),
+ ProductRequest {
+ calling_product_id: "browse.dot".to_string(),
+ payload,
+ },
+ );
assert_eq!(
hex::encode(request.encode()),
"0c726571000e2862726f7773652e646f742862726f7773652e646f7400070000000c6374780418646f6d61696e080102"
@@ -1584,7 +976,7 @@ mod tests {
let response = RemoteMessage {
message_id: "resp".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::SignVrfResponse(SignVrfResponse {
+ data: RemoteMessageData::V1(v1::RemoteMessage::SignVrfResponse(Response {
responding_to: "req".to_string(),
payload: Ok(VrfSignature {
pre_output: [0x11; 32],
@@ -1600,12 +992,15 @@ mod tests {
"22".repeat(64)
)
);
+ let RemoteMessageData::V1(data) = response.data;
assert!(matches!(
- remote_response_for_message(response, "req"),
- Some(SsoRemoteResponse::SignVrf(SignVrfResponse {
+ ProductRequest::::response_from_message(
+ data
+ ),
+ Some(Response {
payload: Ok(VrfSignature { .. }),
..
- }))
+ })
));
}
@@ -1613,17 +1008,15 @@ mod tests {
fn auto_signing_secret_is_fixed_width_on_the_mobile_wire() {
let message = RemoteMessage {
message_id: "m".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationResponse(
- ResourceAllocationResponse {
- responding_to: "r".to_string(),
- payload: Ok(vec![SsoAllocationOutcome::Allocated(
- SsoAllocatedResource::AutoSigning {
- product_root_private_key: sequential_bytes(0),
- ring_vrf_domain_entropy: sequential_bytes(64),
- },
- )]),
- },
- )),
+ data: RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationResponse(Response {
+ responding_to: "r".to_string(),
+ payload: Ok(vec![SsoAllocationOutcome::Allocated(
+ SsoAllocatedResource::AutoSigning {
+ product_root_private_key: sequential_bytes(0),
+ ring_vrf_domain_entropy: sequential_bytes(64),
+ },
+ )]),
+ })),
};
assert_eq!(
hex::encode(message.encode()),
@@ -1677,7 +1070,7 @@ mod tests {
);
let auto_signing_secret = [0xA5; 64];
- let response = ResourceAllocationResponse {
+ let response = Response {
responding_to: "secret-test".to_string(),
payload: Ok(vec![SsoAllocationOutcome::Allocated(
SsoAllocatedResource::AutoSigning {
@@ -1690,8 +1083,9 @@ mod tests {
assert!(response_debug.contains("auto-signing"));
assert!(!response_debug.contains("165, 165"));
- let remote_response = SsoRemoteResponse::ResourceAllocation(response.clone());
- let session_statement = SsoSessionStatement::RemoteResponse(remote_response);
+ let session_statement = SsoSessionStatement::RemoteMessages(vec![Ok(
+ v1::RemoteMessage::ResourceAllocationResponse(response.clone()),
+ )]);
let statement_debug = format!("{session_statement:?}");
assert!(statement_debug.contains("ResourceAllocation"));
assert!(statement_debug.contains("auto-signing"));
@@ -1715,16 +1109,18 @@ mod tests {
#[test]
fn resource_allocation_message_wire_shape_pin() {
- let message = resource_allocation_message(
+ let message = RemoteMessage::request(
"m-resource".to_string(),
- "truapi-playground.dot".to_string(),
- vec![
- AllocatableResource::StatementStoreAllowance,
- AllocatableResource::BulletinAllowance,
- AllocatableResource::SmartContractAllowance(DerivationIndex::Index(9)),
- AllocatableResource::AutoSigning,
- ],
- OnExistingAllowancePolicy::Increase,
+ ResourceAllocationRequest {
+ calling_product_id: "truapi-playground.dot".to_string(),
+ resources: vec![
+ AllocatableResource::StatementStoreAllowance,
+ AllocatableResource::BulletinAllowance,
+ AllocatableResource::SmartContractAllowance(DerivationIndex::Index(9)),
+ AllocatableResource::AutoSigning,
+ ],
+ on_existing: OnExistingAllowancePolicy::Increase,
+ },
);
assert_host_papp_0_8_11_fixture(
@@ -1735,21 +1131,23 @@ mod tests {
#[test]
fn create_transaction_message_wire_shape_pin() {
- let message = create_transaction_message(
+ let message = RemoteMessage::request(
"m-product-tx".to_string(),
- ProductAccountTxPayload {
- signer: ProductAccountId {
- dot_ns_identifier: "truapi-playground.dot".to_string(),
- derivation_index: DerivationIndex::Index(0),
- },
- genesis_hash: sequential_bytes(32),
- call_data: vec![0, 0],
- extensions: vec![TxPayloadExtension {
- id: "CheckNonce".to_string(),
- extra: vec![1],
- additional_signed: vec![2, 3],
- }],
- tx_ext_version: 0,
+ CreateTransactionRequest {
+ payload: CreateTransactionPayload::V1(ProductAccountTxPayload {
+ signer: ProductAccountId {
+ dot_ns_identifier: "truapi-playground.dot".to_string(),
+ derivation_index: DerivationIndex::Index(0),
+ },
+ genesis_hash: sequential_bytes(32),
+ call_data: vec![0, 0],
+ extensions: vec![TxPayloadExtension {
+ id: "CheckNonce".to_string(),
+ extra: vec![1],
+ additional_signed: vec![2, 3],
+ }],
+ tx_ext_version: 0,
+ }),
},
);
@@ -1761,21 +1159,23 @@ mod tests {
#[test]
fn playground_create_transaction_message_wire_shape_pin() {
- let message = create_transaction_message(
+ let message = RemoteMessage::request(
"create-transaction-1".to_string(),
- ProductAccountTxPayload {
- signer: ProductAccountId {
- dot_ns_identifier: "truapi-playground.dot".to_string(),
- derivation_index: DerivationIndex::Index(0),
- },
- genesis_hash: [
- 0xbf, 0x04, 0x88, 0xdb, 0xe9, 0xda, 0xa1, 0xde, 0x1c, 0x08, 0xc5, 0xf7, 0x43,
- 0xe2, 0x6f, 0xdc, 0x2a, 0x4e, 0xcd, 0x74, 0xcf, 0x87, 0xdd, 0x1b, 0x4b, 0x1e,
- 0xeb, 0x99, 0xae, 0x4e, 0xf1, 0x9f,
- ],
- call_data: vec![0, 0],
- extensions: vec![],
- tx_ext_version: 0,
+ CreateTransactionRequest {
+ payload: CreateTransactionPayload::V1(ProductAccountTxPayload {
+ signer: ProductAccountId {
+ dot_ns_identifier: "truapi-playground.dot".to_string(),
+ derivation_index: DerivationIndex::Index(0),
+ },
+ genesis_hash: [
+ 0xbf, 0x04, 0x88, 0xdb, 0xe9, 0xda, 0xa1, 0xde, 0x1c, 0x08, 0xc5, 0xf7,
+ 0x43, 0xe2, 0x6f, 0xdc, 0x2a, 0x4e, 0xcd, 0x74, 0xcf, 0x87, 0xdd, 0x1b,
+ 0x4b, 0x1e, 0xeb, 0x99, 0xae, 0x4e, 0xf1, 0x9f,
+ ],
+ call_data: vec![0, 0],
+ extensions: vec![],
+ tx_ext_version: 0,
+ }),
},
);
@@ -1787,18 +1187,20 @@ mod tests {
#[test]
fn create_transaction_legacy_message_matches_host_papp_0_8_11_fixture() {
- let message = create_transaction_legacy_message(
+ let message = RemoteMessage::request(
"m-legacy-tx".to_string(),
- LegacyAccountTxPayload {
- signer: sequential_bytes(0),
- genesis_hash: sequential_bytes(32),
- call_data: vec![0, 0],
- extensions: vec![TxPayloadExtension {
- id: "CheckNonce".to_string(),
- extra: vec![1],
- additional_signed: vec![2, 3],
- }],
- tx_ext_version: 0,
+ CreateTransactionWithLegacyAccountRequest {
+ payload: CreateTransactionLegacyPayload::V1(LegacyAccountTxPayload {
+ signer: sequential_bytes(0),
+ genesis_hash: sequential_bytes(32),
+ call_data: vec![0, 0],
+ extensions: vec![TxPayloadExtension {
+ id: "CheckNonce".to_string(),
+ extra: vec![1],
+ additional_signed: vec![2, 3],
+ }],
+ tx_ext_version: 0,
+ }),
},
);
@@ -1811,21 +1213,25 @@ mod tests {
#[test]
fn sign_raw_legacy_messages_match_host_papp_0_8_11_fixtures() {
assert_host_papp_0_8_11_fixture(
- sign_raw_legacy_message(
+ RemoteMessage::request(
"m-legacy-raw".to_string(),
- sequential_bytes(0),
- RawPayload::Bytes {
- bytes: b"Hi".to_vec(),
+ SignRawWithLegacyAccountRequest {
+ account: sequential_bytes(0),
+ data: RawPayload::Bytes {
+ bytes: b"Hi".to_vec(),
+ },
},
),
"0x306d2d6c65676163792d726177000a000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f00084869",
);
assert_host_papp_0_8_11_fixture(
- sign_raw_legacy_message(
+ RemoteMessage::request(
"m-legacy-raw-payload".to_string(),
- sequential_bytes(0),
- RawPayload::Payload {
- payload: "Hi ".to_string(),
+ SignRawWithLegacyAccountRequest {
+ account: sequential_bytes(0),
+ data: RawPayload::Payload {
+ payload: "Hi ".to_string(),
+ },
},
),
"0x506d2d6c65676163792d7261772d7061796c6f6164000a000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f01443c42797465733e48693c2f42797465733e",
@@ -1851,62 +1257,34 @@ mod tests {
asset_id: None,
metadata_hash: None,
mode: None,
- with_signed_transaction: Some(true),
+ with_signed_transaction: parity_scale_codec::OptionBool(Some(true)),
},
};
- let true_encoded = SigningPayloadRequest::from_host_request(request.clone()).encode();
- request.payload.with_signed_transaction = Some(false);
- let false_encoded = SigningPayloadRequest::from_host_request(request.clone()).encode();
- request.payload.with_signed_transaction = None;
- let none_encoded = SigningPayloadRequest::from_host_request(request).encode();
-
- assert_eq!(true_encoded.last(), Some(&1));
- assert_eq!(false_encoded.last(), Some(&2));
- assert_eq!(none_encoded.last(), Some(&0));
- }
-
- #[test]
- fn maps_public_resource_names_to_sso_dialect() {
- let message = resource_allocation_message(
- "alloc".to_string(),
- "myapp.dot".to_string(),
- vec![
- AllocatableResource::StatementStoreAllowance,
- AllocatableResource::BulletinAllowance,
- AllocatableResource::SmartContractAllowance(DerivationIndex::Index(9)),
- AllocatableResource::AutoSigning,
- ],
- OnExistingAllowancePolicy::Increase,
- );
- let RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationRequest(request)) =
- message.data
- else {
- panic!("expected resource allocation request");
- };
-
- assert_eq!(
- request.resources,
- vec![
- SsoAllocatableResource::StatementStoreAllowance,
- SsoAllocatableResource::BulletinAllowance,
- SsoAllocatableResource::SmartContractAllowance(DerivationIndex::Index(9)),
- SsoAllocatableResource::AutoSigning,
- ]
- );
- assert_eq!(request.on_existing, OnExistingAllowancePolicy::Increase);
+ for (flag, byte) in [(None, 0), (Some(true), 1), (Some(false), 2)] {
+ request.payload.with_signed_transaction = parity_scale_codec::OptionBool(flag);
+ let expected = hex::decode(format!(
+ "246d796170702e646f7400070000000000000000000000000004000000000000{byte:02x}"
+ ))
+ .unwrap();
+ assert_eq!(request.encode(), expected);
+ assert_eq!(
+ HostSignPayloadRequest::decode(&mut expected.as_slice()).unwrap(),
+ request
+ );
+ }
}
#[test]
fn builds_signed_encrypted_outgoing_request_statement() {
let session = session();
- let remote_message = sign_raw_message(
+ let remote_message = RemoteMessage::request(
"remote-1".to_string(),
- truapi::latest::HostSignRawRequest {
+ SignRequest::Raw(truapi::latest::HostSignRawRequest {
account: account(),
payload: RawPayload::Payload {
payload: "hello ".to_string(),
},
- },
+ }),
);
let statement = build_outgoing_request_statement_with_nonce(
@@ -1939,14 +1317,14 @@ mod tests {
#[test]
fn ignores_own_echoed_session_request_statement() {
let session = session();
- let remote_message = sign_raw_message(
+ let remote_message = RemoteMessage::request(
"remote-1".to_string(),
- truapi::latest::HostSignRawRequest {
+ SignRequest::Raw(truapi::latest::HostSignRawRequest {
account: account(),
payload: RawPayload::Payload {
payload: "hello ".to_string(),
},
- },
+ }),
);
let statement = build_outgoing_request_statement_with_nonce(
&session,
@@ -1957,74 +1335,25 @@ mod tests {
)
.unwrap();
- let decoded =
- decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap();
+ let decoded = decode_sso_session_statement(&session, &statement, "statement-1").unwrap();
assert_eq!(decoded, None);
}
- fn host_and_responder_sessions() -> (SsoSessionInfo, SsoSessionInfo) {
- use crate::host_logic::sso::pairing::{
- ResponderIdentity, create_pairing_bootstrap, derive_x25519_keypair_from_entropy,
- establish_responder_session_info, establish_sso_session_info,
- };
- use truapi_platform::{HostInfo, PairingHostConfig, PlatformInfo};
-
- let config = PairingHostConfig::new(
- HostInfo {
- name: "Test Host".to_string(),
- icon: None,
- version: None,
- platform: truapi::latest::HostPlatform::Unknown,
- },
- PlatformInfo::default(),
- [0; 32],
- [0xbb; 32],
- [0xcc; 32],
- "polkadotapp".to_string(),
- )
- .expect("test pairing config is valid");
- let bootstrap = create_pairing_bootstrap(&config).unwrap();
- let statement_keypair = MiniSecretKey::from_bytes(&[7; 32])
- .unwrap()
- .expand_to_keypair(ExpansionMode::Ed25519);
- let (encryption_secret_key, encryption_public_key) =
- derive_x25519_keypair_from_entropy(&[0xAB; 16], b"sso");
- let responder = ResponderIdentity {
- statement_secret: statement_keypair.secret.to_bytes(),
- statement_public_key: statement_keypair.public.to_bytes(),
- encryption_secret_key,
- encryption_public_key,
- };
- let responder_session = establish_responder_session_info(
- &responder,
- bootstrap.statement_store_public_key,
- bootstrap.encryption_public_key,
- )
- .unwrap();
- let host_session = establish_sso_session_info(
- &bootstrap,
- responder.statement_public_key,
- responder.encryption_public_key,
- )
- .unwrap();
- (host_session, responder_session)
- }
-
/// A host-built request statement decodes on the responder side into the
/// batched remote messages, and the responder's ack plus response
/// statements resolve the host's pending wait.
#[test]
fn host_request_round_trips_through_responder_statements() {
- let (host_session, responder_session) = host_and_responder_sessions();
- let request = sign_raw_message(
+ let (host_session, responder_session) = sso_host_and_responder_sessions();
+ let request = RemoteMessage::request(
"remote-1".to_string(),
- truapi::latest::HostSignRawRequest {
+ SignRequest::Raw(truapi::latest::HostSignRawRequest {
account: account(),
payload: RawPayload::Payload {
payload: "hello ".to_string(),
},
- },
+ }),
);
let expiry = fresh_expiry();
let host_statement = build_outgoing_request_statement(
@@ -2055,15 +1384,15 @@ mod tests {
)
.unwrap();
assert_eq!(
- decode_sso_session_statement(&host_session, &ack, "statement-1", "remote-1").unwrap(),
+ decode_sso_session_statement(&host_session, &ack, "statement-1").unwrap(),
Some(SsoSessionStatement::RequestAccepted)
);
let response = RemoteMessage {
message_id: "resp-1".to_string(),
- data: RemoteMessageData::V1(v1::RemoteMessage::SignResponse(SigningResponse {
+ data: RemoteMessageData::V1(v1::RemoteMessage::SignResponse(Response {
responding_to: "remote-1".to_string(),
- payload: Ok(SigningPayloadResponseData {
+ payload: Ok(HostSignPayloadResponse {
signature: vec![9; 64],
signed_transaction: None,
}),
@@ -2076,30 +1405,26 @@ mod tests {
fresh_expiry(),
)
.unwrap();
- let decoded = decode_sso_session_statement(
- &host_session,
- &response_statement,
- "statement-1",
- "remote-1",
- )
- .unwrap();
+ let decoded =
+ decode_sso_session_statement(&host_session, &response_statement, "statement-1")
+ .unwrap();
assert_eq!(
decoded,
- Some(SsoSessionStatement::RemoteResponse(
- SsoRemoteResponse::Sign(SigningResponse {
+ Some(SsoSessionStatement::RemoteMessages(vec![Ok(
+ v1::RemoteMessage::SignResponse(Response {
responding_to: "remote-1".to_string(),
- payload: Ok(SigningPayloadResponseData {
+ payload: Ok(HostSignPayloadResponse {
signature: vec![9; 64],
signed_transaction: None,
}),
})
- ))
+ )]))
);
}
#[test]
fn responder_ignores_own_echo_and_transport_acks() {
- let (host_session, responder_session) = host_and_responder_sessions();
+ let (host_session, responder_session) = sso_host_and_responder_sessions();
let own_statement = build_outgoing_request_statement(
&responder_session,
"resp-statement-1".to_string(),
@@ -2130,7 +1455,7 @@ mod tests {
#[test]
fn responder_ignores_expired_host_request() {
- let (host_session, responder_session) = host_and_responder_sessions();
+ let (host_session, responder_session) = sso_host_and_responder_sessions();
let stale_statement = build_outgoing_request_statement(
&host_session,
"statement-1".to_string(),
@@ -2150,7 +1475,7 @@ mod tests {
#[test]
fn responder_recovers_request_id_from_undecodable_messages() {
- let (host_session, responder_session) = host_and_responder_sessions();
+ let (host_session, responder_session) = sso_host_and_responder_sessions();
let encrypted = encrypt_session_statement_data(
&host_session,
&SsoStatementData::Request {
@@ -2193,8 +1518,7 @@ mod tests {
let session = session();
let statement = response_ack_statement(&session, fresh_expiry());
- let decoded =
- decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap();
+ let decoded = decode_sso_session_statement(&session, &statement, "statement-1").unwrap();
assert_eq!(decoded, Some(SsoSessionStatement::RequestAccepted));
}
@@ -2206,8 +1530,7 @@ mod tests {
let session = session();
let statement = response_ack_statement(&session, elapsed_expiry());
- let decoded =
- decode_sso_session_statement(&session, &statement, "statement-1", "remote-1").unwrap();
+ let decoded = decode_sso_session_statement(&session, &statement, "statement-1").unwrap();
assert_eq!(decoded, None);
}
diff --git a/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs b/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs
index 68f9c09dc..1532e8506 100644
--- a/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs
+++ b/rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs
@@ -6,102 +6,82 @@
//!
use parity_scale_codec::{Decode, Encode};
+use truapi::latest::{
+ HostAccountCreateProofRequest, HostAccountGetAliasRequest, HostAccountListRingVrfKeysRequest,
+ HostAccountRegisterRingVrfKeyRequest, HostAccountRingVrfSignRequest, HostAccountSignVrfRequest,
+};
use super::{
- CreateTransactionLegacyRequest, CreateTransactionRequest, CreateTransactionResponse,
- ListRingVrfKeysRequest, ListRingVrfKeysResponse, ProductSubtreeRequest, ProductSubtreeResponse,
- RegisterRingVrfKeyRequest, RegisterRingVrfKeyResponse, ResourceAllocationRequest,
- ResourceAllocationResponse, RingVrfAliasRequest, RingVrfAliasResponse, RingVrfProofRequest,
- RingVrfProofResponse, RingVrfSignRequest, RingVrfSignResponse, SignRawLegacyRequest,
- SignRawLegacyResponse, SignVrfRequest, SignVrfResponse, SigningRequest, SigningResponse,
+ CreateAccountProofResponse, CreateTransactionRequest, CreateTransactionResponse,
+ CreateTransactionWithLegacyAccountRequest, GetAccountAliasResponse, ListRingVrfKeysResponse,
+ ProductRequest, ProductSubtreeRequest, ProductSubtreeResponse, RegisterRingVrfKeyResponse,
+ ResourceAllocationRequest, ResourceAllocationResponse, Response, RingVrfSignResponse,
+ SignRawWithLegacyAccountRequest, SignRawWithLegacyAccountResponse, SignRequest, SignResponse,
+ SignVrfResponse,
};
/// v1 messages exchanged with the paired signing host over the encrypted SSO channel.
///
/// The variant order is part of the SCALE wire protocol used inside
/// statement-store session statements.
-#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, derive_more::Display)]
+#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub enum RemoteMessage {
/// The peer is ending the SSO session.
- #[display("disconnected")]
Disconnected,
/// Ask the signing host to sign a payload or raw data with a product account.
- #[display("sign_request")]
- SignRequest(Box),
+ SignRequest(SignRequest),
/// Signing host's answer to [`RemoteMessage::SignRequest`].
- #[display("sign_response")]
- SignResponse(SigningResponse),
+ SignResponse(Response),
/// Ask the Account Holder for a contextual alias.
- #[display("get_account_alias")]
- RingVrfAliasRequest(RingVrfAliasRequest),
- /// Account Holder's answer to [`RemoteMessage::RingVrfAliasRequest`].
- #[display("get_account_alias_response")]
- RingVrfAliasResponse(RingVrfAliasResponse),
+ GetAccountAliasRequest(ProductRequest),
+ /// Account Holder's answer to [`RemoteMessage::GetAccountAliasRequest`].
+ GetAccountAliasResponse(Response),
/// Ask the signing host to allocate SSO-backed resources.
- #[display("resource_allocation")]
ResourceAllocationRequest(ResourceAllocationRequest),
/// Signing host's answer to [`RemoteMessage::ResourceAllocationRequest`].
- #[display("resource_allocation_response")]
- ResourceAllocationResponse(ResourceAllocationResponse),
+ ResourceAllocationResponse(Response),
/// Ask the signing host to create a signed product-account transaction.
- #[display("create_transaction")]
CreateTransactionRequest(CreateTransactionRequest),
/// Signing host's answer to either transaction-creation request.
- #[display("create_transaction_response")]
- CreateTransactionResponse(CreateTransactionResponse),
+ CreateTransactionResponse(Response),
/// Ask the signing host to create a signed legacy-account transaction.
- #[display("create_transaction_legacy")]
- CreateTransactionLegacyRequest(CreateTransactionLegacyRequest),
+ CreateTransactionWithLegacyAccountRequest(CreateTransactionWithLegacyAccountRequest),
/// Ask the signing host to sign raw data with a legacy account.
- #[display("sign_raw_legacy")]
- SignRawLegacyRequest(SignRawLegacyRequest),
- /// Signing host's answer to [`RemoteMessage::SignRawLegacyRequest`].
- #[display("sign_raw_legacy_response")]
- SignRawLegacyResponse(SignRawLegacyResponse),
+ SignRawWithLegacyAccountRequest(SignRawWithLegacyAccountRequest),
+ /// Signing host's answer to [`RemoteMessage::SignRawWithLegacyAccountRequest`].
+ SignRawWithLegacyAccountResponse(Response),
/// Ask the Account Holder for a ring-VRF proof.
- #[display("create_account_proof")]
- RingVrfProofRequest(RingVrfProofRequest),
- /// Account Holder's answer to [`RemoteMessage::RingVrfProofRequest`].
- #[display("create_account_proof_response")]
- RingVrfProofResponse(RingVrfProofResponse),
+ CreateAccountProofRequest(ProductRequest),
+ /// Account Holder's answer to [`RemoteMessage::CreateAccountProofRequest`].
+ CreateAccountProofResponse(Response),
/// Ask the Account Holder to sign an RFC-0023 sr25519 VRF transcript.
#[codec(index = 14)]
- #[display("sign_vrf")]
- SignVrfRequest(SignVrfRequest),
+ SignVrfRequest(ProductRequest),
/// Account Holder's answer to [`RemoteMessage::SignVrfRequest`].
#[codec(index = 15)]
- #[display("sign_vrf_response")]
- SignVrfResponse(SignVrfResponse),
+ SignVrfResponse(Response),
/// Consent-free request for a product's hard-subtree public key.
#[codec(index = 16)]
- #[display("product_subtree")]
ProductSubtreeRequest(ProductSubtreeRequest),
/// Account Holder's answer to [`RemoteMessage::ProductSubtreeRequest`].
#[codec(index = 17)]
- #[display("product_subtree_response")]
- ProductSubtreeResponse(ProductSubtreeResponse),
+ ProductSubtreeResponse(Response),
/// Register a ring-VRF key with the Account Holder.
#[codec(index = 18)]
- #[display("register_ring_vrf_key")]
- RegisterRingVrfKeyRequest(RegisterRingVrfKeyRequest),
+ RegisterRingVrfKeyRequest(ProductRequest),
/// Account Holder's answer to [`RemoteMessage::RegisterRingVrfKeyRequest`].
#[codec(index = 19)]
- #[display("register_ring_vrf_key_response")]
- RegisterRingVrfKeyResponse(RegisterRingVrfKeyResponse),
+ RegisterRingVrfKeyResponse(Response),
/// List registered ring-VRF keys.
#[codec(index = 20)]
- #[display("list_ring_vrf_keys")]
- ListRingVrfKeysRequest(ListRingVrfKeysRequest),
+ ListRingVrfKeysRequest(ProductRequest),
/// Account Holder's answer to [`RemoteMessage::ListRingVrfKeysRequest`].
#[codec(index = 21)]
- #[display("list_ring_vrf_keys_response")]
- ListRingVrfKeysResponse(ListRingVrfKeysResponse),
+ ListRingVrfKeysResponse(Response),
/// Sign bytes with a registered ring-VRF key.
#[codec(index = 22)]
- #[display("ring_vrf_sign")]
- RingVrfSignRequest(RingVrfSignRequest),
+ RingVrfSignRequest(ProductRequest),
/// Account Holder's answer to [`RemoteMessage::RingVrfSignRequest`].
#[codec(index = 23)]
- #[display("ring_vrf_sign_response")]
- RingVrfSignResponse(RingVrfSignResponse),
+ RingVrfSignResponse(Response),
}
diff --git a/rust/crates/truapi-server/src/host_logic/sso/wire.rs b/rust/crates/truapi-server/src/host_logic/sso/wire.rs
new file mode 100644
index 000000000..1f9fb98a4
--- /dev/null
+++ b/rust/crates/truapi-server/src/host_logic/sso/wire.rs
@@ -0,0 +1,120 @@
+//! Typed SSO requests and their response variants.
+//!
+//! `#[sso_service]` generates each request's wire conversions from its handler
+//! signature. Responses share the [`Response`] envelope; the request identifies
+//! the response variant even when different operations have identical payload types.
+
+use core::fmt::Display;
+
+use truapi::latest::HostAccountSignVrfError;
+
+use super::messages::{RemoteMessage, RemoteMessageData, Response, RingVrfError, v1};
+
+/// A request payload and the wire response selected by its handler declaration.
+pub trait SsoRequest: Sized {
+ /// Method name used for tracing.
+ const NAME: &'static str;
+ /// The handler's result payload, without correlation metadata.
+ type Response;
+ /// Wrap into the request variant.
+ fn into_message(self) -> v1::RemoteMessage;
+ /// Wrap an envelope into this request's response variant.
+ fn response_into_message(response: Response) -> v1::RemoteMessage;
+ /// Unwrap this request's response variant; `None` for any other message.
+ fn response_from_message(message: v1::RemoteMessage) -> Option>;
+}
+
+/// Failure payload that can express "no signing session".
+pub trait SsoError: Display {
+ /// The signing host has no active session to serve the request with.
+ fn not_connected() -> Self;
+}
+
+impl SsoError for String {
+ fn not_connected() -> Self {
+ "signing host session is not active".to_string()
+ }
+}
+
+impl SsoError for RingVrfError {
+ fn not_connected() -> Self {
+ Self::Unknown {
+ reason: String::not_connected(),
+ }
+ }
+}
+
+impl SsoError for HostAccountSignVrfError {
+ fn not_connected() -> Self {
+ Self::NotConnected
+ }
+}
+
+/// Outcome code and reason recorded in the SSO transcript for one response.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ResponseOutcome {
+ /// Stable outcome code such as `ok`, `error`, or `publish_failed`.
+ pub outcome: &'static str,
+ /// Single-line failure description, when any.
+ pub reason: Option,
+}
+
+impl ResponseOutcome {
+ /// Classify a plain payload: `ok`, or `error` with the failure's reason.
+ pub fn from_payload(payload: &Result) -> Self {
+ match payload {
+ Ok(_) => Self {
+ outcome: "ok",
+ reason: None,
+ },
+ Err(err) => Self {
+ outcome: "error",
+ reason: Some(err.to_string()),
+ },
+ }
+ }
+}
+
+impl RemoteMessage {
+ /// Service method name for requests; variant name for other messages.
+ pub(crate) fn name(&self) -> &'static str {
+ let RemoteMessageData::V1(message) = &self.data;
+ message.name()
+ }
+
+ /// Outgoing request carrying `request` under `message_id`.
+ pub fn request(message_id: String, request: R) -> Self {
+ Self {
+ message_id,
+ data: RemoteMessageData::V1(request.into_message()),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::host_logic::sso::messages::{
+ CreateTransactionRequest, CreateTransactionWithLegacyAccountRequest,
+ SignRawWithLegacyAccountRequest,
+ };
+
+ #[test]
+ fn identical_payloads_keep_their_request_specific_response_variants() {
+ let response = Response {
+ responding_to: "m-1".to_string(),
+ payload: Ok(vec![7]),
+ };
+ let transaction = CreateTransactionRequest::response_into_message(response.clone());
+ assert_eq!(transaction.name(), "CreateTransactionResponse");
+ assert_eq!(
+ CreateTransactionWithLegacyAccountRequest::response_from_message(transaction.clone()),
+ Some(response.clone()),
+ );
+ assert!(SignRawWithLegacyAccountRequest::response_from_message(transaction).is_none());
+
+ let signature = SignRawWithLegacyAccountRequest::response_into_message(response);
+ assert_eq!(signature.name(), "SignRawWithLegacyAccountResponse");
+ assert!(CreateTransactionRequest::response_from_message(signature).is_none());
+ }
+}
diff --git a/rust/crates/truapi-server/src/host_logic/transaction.rs b/rust/crates/truapi-server/src/host_logic/transaction.rs
index 66552c0ca..8e4074452 100644
--- a/rust/crates/truapi-server/src/host_logic/transaction.rs
+++ b/rust/crates/truapi-server/src/host_logic/transaction.rs
@@ -155,7 +155,7 @@ mod tests {
asset_id: None,
metadata_hash: None,
mode: None,
- with_signed_transaction: None,
+ with_signed_transaction: parity_scale_codec::OptionBool(None),
}
}
diff --git a/rust/crates/truapi-server/src/native.rs b/rust/crates/truapi-server/src/native.rs
index ec240ab58..79b503084 100644
--- a/rust/crates/truapi-server/src/native.rs
+++ b/rust/crates/truapi-server/src/native.rs
@@ -3517,7 +3517,7 @@ mod tests {
panic!("expected a product subtree response payload");
};
assert_eq!(payload.responding_to, "m9");
- assert!(payload.product_public_key.is_ok());
+ assert!(payload.payload.is_ok());
}
#[test]
diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs
index 86fd42cd7..bc7ba5dd3 100644
--- a/rust/crates/truapi-server/src/runtime.rs
+++ b/rust/crates/truapi-server/src/runtime.rs
@@ -27,6 +27,7 @@ mod signing_host;
pub(crate) mod sso_pairing;
/// SSO remote request/response messaging over the statement store.
pub(crate) mod sso_remote;
+pub(crate) mod sso_service;
/// Native Statement Store and Bulletin allowance allocation.
#[cfg(not(target_arch = "wasm32"))]
pub mod statement_allowance;
@@ -51,7 +52,7 @@ pub(crate) use services::RuntimeServices;
#[cfg(not(target_arch = "wasm32"))]
pub use signing_host::StatementRenewalTarget;
pub(crate) use signing_host::{
- LocalActivation, SigningHost as SigningHostRole, answer_remote_message, establish_pairing,
+ LocalActivation, SigningHost as SigningHostRole, SigningHostSsoService, establish_pairing,
respond_to_pairing, resume_pairing,
};
pub use signing_host::{PairedSsoPeer, ResponderExit};
@@ -751,17 +752,7 @@ fn validate_vrf_transcript(request: &v01::HostAccountSignVrfRequest) -> Result<(
}
fn vrf_call_error(err: AuthorityError) -> CallError {
- let error = match err {
- AuthorityError::Disconnected => v01::HostAccountSignVrfError::NotConnected,
- AuthorityError::Rejected => v01::HostAccountSignVrfError::Rejected,
- AuthorityError::Cancelled(err) => v01::HostAccountSignVrfError::Unknown {
- reason: err.to_string(),
- },
- AuthorityError::Unavailable { reason }
- | AuthorityError::NotSupported { reason }
- | AuthorityError::Unknown { reason } => v01::HostAccountSignVrfError::Unknown { reason },
- };
- CallError::Domain(HostAccountSignVrfError::V1(error))
+ CallError::Domain(HostAccountSignVrfError::V1(err.into()))
}
fn account_get_authority_error(err: AuthorityError) -> CallError {
let error = match err {
diff --git a/rust/crates/truapi-server/src/runtime/authority.rs b/rust/crates/truapi-server/src/runtime/authority.rs
index 302473ea3..256fa5501 100644
--- a/rust/crates/truapi-server/src/runtime/authority.rs
+++ b/rust/crates/truapi-server/src/runtime/authority.rs
@@ -3,25 +3,29 @@
//! Pairing and signing hosts implement these traits differently, but
//! `ProductRuntimeHost` can use this module's shared request/session types
//! without knowing where the key material lives.
+//! Alias, proof, and ring-VRF operations reuse the request payloads in
+//! `host_logic::sso::messages` for both local calls and SSO transport.
use async_trait::async_trait;
use std::sync::Arc;
use truapi::latest::{
- AccountId, HostAccountCreateProofResponse, HostAccountGetAliasResponse,
- HostAccountListRingVrfKeysResponse, HostAccountRegisterRingVrfKeyResponse,
- HostAccountRingVrfSignResponse, HostCreateTransactionResponse,
- HostRequestResourceAllocationRequest, HostRequestResourceAllocationResponse,
- HostSignPayloadRequest, HostSignPayloadResponse, HostSignPayloadWithLegacyAccountRequest,
- HostSignRawRequest, HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload,
- ProductAccountId, ProductAccountTxPayload, ProductProofContext, RingLocation,
+ AccountId, HostAccountCreateProofRequest, HostAccountCreateProofResponse,
+ HostAccountGetAliasRequest, HostAccountGetAliasResponse, HostAccountListRingVrfKeysRequest,
+ HostAccountListRingVrfKeysResponse, HostAccountRegisterRingVrfKeyRequest,
+ HostAccountRegisterRingVrfKeyResponse, HostAccountRingVrfSignRequest,
+ HostAccountRingVrfSignResponse, HostAccountSignVrfError, HostAccountSignVrfRequest,
+ HostCreateTransactionResponse, HostRequestResourceAllocationRequest,
+ HostRequestResourceAllocationResponse, HostSignPayloadRequest, HostSignPayloadResponse,
+ HostSignPayloadWithLegacyAccountRequest, HostSignRawRequest,
+ HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, ProductAccountId,
+ ProductAccountTxPayload, VrfSignature,
};
-use truapi::v01::{HostAccountSignVrfRequest, VrfSignature};
use truapi::versioned::account::{HostRequestLoginError, HostRequestLoginResponse};
use truapi::{CallContext, CallError, CancellationReason};
use truapi_platform::ProductContext;
use crate::host_logic::session::{SessionInfo, SessionState};
-use crate::host_logic::sso::messages::RingVrfError;
+use crate::host_logic::sso::messages::{ProductRequest, RingVrfError};
use crate::host_logic::statement_store::statement_public_key_from_secret;
/// Secret key allocated for Bulletin preimage submission.
@@ -159,6 +163,21 @@ impl From for RingVrfError {
}
}
+impl From for HostAccountSignVrfError {
+ fn from(err: AuthorityError) -> Self {
+ match err {
+ AuthorityError::Disconnected => Self::NotConnected,
+ AuthorityError::Rejected => Self::Rejected,
+ AuthorityError::Cancelled(err) => Self::Unknown {
+ reason: err.to_string(),
+ },
+ AuthorityError::Unavailable { reason }
+ | AuthorityError::NotSupported { reason }
+ | AuthorityError::Unknown { reason } => Self::Unknown { reason },
+ }
+ }
+}
+
/// Cancellation cause for an account-authority call.
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)]
#[display(
@@ -224,67 +243,6 @@ pub(crate) enum CreateTransactionAuthorityRequest {
IdentityAccount(LegacyAccountTxPayload),
}
-/// Contextual-alias request forwarded to the account authority.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct AccountAliasAuthorityRequest {
- /// Calling product, so the Account Holder can scope context derivation.
- pub calling_product_id: String,
- /// Explicit ring-VRF key handle.
- pub key_handle: ProductAccountId,
- /// Product-scoped context the derived alias is bound to.
- pub context: ProductProofContext,
- /// Ring the explicit key must be registered for.
- pub ring_location: RingLocation,
-}
-
-/// Ring-VRF proof request forwarded to the account authority.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct CreateProofAuthorityRequest {
- /// Calling product, so the Account Holder can scope context derivation.
- pub calling_product_id: String,
- /// Explicit ring-VRF key handle.
- pub key_handle: ProductAccountId,
- /// Product-scoped context the derived alias is bound to.
- pub context: ProductProofContext,
- /// Ring the explicit key must be registered for.
- pub ring_location: RingLocation,
- /// Opaque message bound into the proof.
- pub message: Vec,
-}
-
-/// Ring-VRF key registration request forwarded to the account authority.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct RegisterRingVrfKeyAuthorityRequest {
- /// Calling product that owns the key.
- pub calling_product_id: String,
- /// Key derivation index within the caller's ring-VRF domain.
- pub index: truapi::v01::DerivationIndex,
- /// Declared ring for the key.
- pub ring: RingLocation,
-}
-
-/// Ring-VRF key listing request forwarded to the account authority.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct ListRingVrfKeysAuthorityRequest {
- /// Calling product requesting the list.
- pub calling_product_id: String,
- /// Owner product whose entries should be listed.
- pub owner: String,
- /// Disclosure requested by the caller.
- pub disclosure: truapi::v01::RingVrfKeyDisclosure,
-}
-
-/// Direct ring-VRF member-key signing request.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct RingVrfSignAuthorityRequest {
- /// Calling product requesting the signature.
- pub calling_product_id: String,
- /// Registered key handle.
- pub key_handle: ProductAccountId,
- /// Message to sign.
- pub message: Vec,
-}
-
/// Statement-store allowance signing material held by the authority layer.
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct StatementStoreAllowanceKey {
@@ -418,7 +376,7 @@ pub(crate) trait ProductAuthority: Send + Sync {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: AccountAliasAuthorityRequest,
+ request: ProductRequest,
) -> Result;
/// Create a ring-VRF proof bound to a context and message.
@@ -429,7 +387,7 @@ pub(crate) trait ProductAuthority: Send + Sync {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: CreateProofAuthorityRequest,
+ request: ProductRequest,
) -> Result;
/// Register a ring-VRF key owned by the calling product.
@@ -437,7 +395,7 @@ pub(crate) trait ProductAuthority: Send + Sync {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) -> Result;
/// List registered ring-VRF keys.
@@ -445,7 +403,7 @@ pub(crate) trait ProductAuthority: Send + Sync {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: ListRingVrfKeysAuthorityRequest,
+ request: ProductRequest,
) -> Result;
/// Sign bytes directly with a registered ring-VRF key.
@@ -453,7 +411,7 @@ pub(crate) trait ProductAuthority: Send + Sync {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RingVrfSignAuthorityRequest,
+ request: ProductRequest,
) -> Result;
/// Ask the account authority to allocate product-scoped resources.
diff --git a/rust/crates/truapi-server/src/runtime/capabilities/account.rs b/rust/crates/truapi-server/src/runtime/capabilities/account.rs
index 51c12c999..c60cb57fe 100644
--- a/rust/crates/truapi-server/src/runtime/capabilities/account.rs
+++ b/rust/crates/truapi-server/src/runtime/capabilities/account.rs
@@ -25,10 +25,7 @@ use truapi_platform::{
normalize_product_identifier,
};
-use crate::runtime::authority::{
- AccountAliasAuthorityRequest, CreateProofAuthorityRequest, ListRingVrfKeysAuthorityRequest,
- RegisterRingVrfKeyAuthorityRequest, RingVrfSignAuthorityRequest,
-};
+use crate::host_logic::sso::messages::ProductRequest;
use crate::runtime::{
ProductRuntimeHost, account_access_authorization, account_get_authority_error,
remote_authority_call, remote_authority_context, ring_vrf_alias_error, ring_vrf_list_error,
@@ -126,18 +123,15 @@ impl Account for ProductRuntimeHost {
cx: &CallContext,
request: HostAccountGetAliasRequest,
) -> Result> {
- let HostAccountGetAliasRequest::V1(v01::HostAccountGetAliasRequest {
- key_handle,
- 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 HostAccountGetAliasRequest::V1(mut request) = request;
+ request.key_handle =
+ Self::normalize_product_account_id(request.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,
@@ -151,11 +145,9 @@ impl Account for ProductRuntimeHost {
self.authority.account_alias(
&cx,
&session,
- AccountAliasAuthorityRequest {
+ ProductRequest {
calling_product_id,
- key_handle,
- context,
- ring_location,
+ payload: request,
},
),
)
@@ -170,20 +162,16 @@ impl Account for ProductRuntimeHost {
cx: &CallContext,
request: HostAccountCreateProofRequest,
) -> Result> {
- let HostAccountCreateProofRequest::V1(v01::HostAccountCreateProofRequest {
- key_handle,
- context,
- 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() {
+ let HostAccountCreateProofRequest::V1(mut request) = request;
+ request.key_handle =
+ Self::normalize_product_account_id(request.key_handle).map_err(|()| {
+ CallError::Domain(HostAccountCreateProofError::V1(
+ v01::HostAccountCreateProofError::Unknown {
+ reason: "Invalid key handle".to_string(),
+ },
+ ))
+ })?;
+ if request.key_handle.dot_ns_identifier != self.product_id() {
return Err(CallError::Domain(HostAccountCreateProofError::V1(
v01::HostAccountCreateProofError::NotAllowlisted,
)));
@@ -202,12 +190,9 @@ impl Account for ProductRuntimeHost {
self.authority.create_proof(
&cx,
&session,
- CreateProofAuthorityRequest {
+ ProductRequest {
calling_product_id,
- key_handle,
- context,
- ring_location,
- message,
+ payload: request,
},
),
)
@@ -225,10 +210,7 @@ impl Account for ProductRuntimeHost {
request: HostAccountRegisterRingVrfKeyRequest,
) -> Result>
{
- let HostAccountRegisterRingVrfKeyRequest::V1(v01::HostAccountRegisterRingVrfKeyRequest {
- index,
- ring,
- }) = request;
+ let HostAccountRegisterRingVrfKeyRequest::V1(request) = request;
let Some(session) = self.authority.current_session() else {
return Err(CallError::Domain(HostAccountRegisterRingVrfKeyError::V1(
v01::HostAccountRegisterRingVrfKeyError::NotConnected,
@@ -241,10 +223,9 @@ impl Account for ProductRuntimeHost {
self.authority.register_ring_vrf_key(
&cx,
&session,
- RegisterRingVrfKeyAuthorityRequest {
+ ProductRequest {
calling_product_id,
- index,
- ring,
+ payload: request,
},
),
)
@@ -264,16 +245,13 @@ impl Account for ProductRuntimeHost {
request: HostAccountListRingVrfKeysRequest,
) -> Result>
{
- let HostAccountListRingVrfKeysRequest::V1(v01::HostAccountListRingVrfKeysRequest {
- owner,
- disclosure,
- }) = request;
+ let HostAccountListRingVrfKeysRequest::V1(mut request) = request;
let Some(session) = self.authority.current_session() else {
return Err(CallError::Domain(HostAccountListRingVrfKeysError::V1(
v01::HostAccountListRingVrfKeysError::NotConnected,
)));
};
- let owner = normalize_product_identifier(&owner).map_err(|err| {
+ request.owner = normalize_product_identifier(&request.owner).map_err(|err| {
CallError::Domain(HostAccountListRingVrfKeysError::V1(
v01::HostAccountListRingVrfKeysError::Unknown {
reason: err.to_string(),
@@ -287,10 +265,9 @@ impl Account for ProductRuntimeHost {
self.authority.list_ring_vrf_keys(
&cx,
&session,
- ListRingVrfKeysAuthorityRequest {
+ ProductRequest {
calling_product_id,
- owner,
- disclosure,
+ payload: request,
},
),
)
@@ -335,10 +312,9 @@ impl Account for ProductRuntimeHost {
self.authority.ring_vrf_sign(
&cx,
&session,
- RingVrfSignAuthorityRequest {
+ ProductRequest {
calling_product_id,
- key_handle: request.key_handle,
- message: request.message,
+ payload: request,
},
),
)
diff --git a/rust/crates/truapi-server/src/runtime/pairing_host.rs b/rust/crates/truapi-server/src/runtime/pairing_host.rs
index df9453c1a..9af8733da 100644
--- a/rust/crates/truapi-server/src/runtime/pairing_host.rs
+++ b/rust/crates/truapi-server/src/runtime/pairing_host.rs
@@ -10,6 +10,10 @@ use std::collections::HashMap;
#[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, Weak};
+use truapi::latest::{
+ HostAccountCreateProofRequest, HostAccountGetAliasRequest, HostAccountListRingVrfKeysRequest,
+ HostAccountRegisterRingVrfKeyRequest, HostAccountRingVrfSignRequest,
+};
use futures::channel::oneshot;
use parity_scale_codec::{Decode, Encode};
@@ -20,11 +24,10 @@ use zeroize::Zeroize;
use super::allowances::{self, AllowanceCacheKey, AllowanceResource};
use super::auth_state::AuthStateMachine;
use super::authority::{
- AccountAliasAuthorityRequest, AuthorityError, AuthoritySession, AutoSigningKey,
- BulletinAllowanceKey, CreateProofAuthorityRequest, CreateTransactionAuthorityRequest,
- ListRingVrfKeysAuthorityRequest, ProductAuthority, RegisterRingVrfKeyAuthorityRequest,
- RingVrfSignAuthorityRequest, SignPayloadAuthorityRequest, SignRawAuthorityRequest,
- StatementStoreAllowanceKey, authority_session, require_current_session,
+ AuthorityError, AuthoritySession, AutoSigningKey, BulletinAllowanceKey,
+ CreateTransactionAuthorityRequest, ProductAuthority, SignPayloadAuthorityRequest,
+ SignRawAuthorityRequest, StatementStoreAllowanceKey, authority_session,
+ require_current_session,
};
use super::connected_session_ui_info;
use super::identity::resolve_session_identity_with_chain;
@@ -43,7 +46,7 @@ use crate::host_logic::product_account::{
};
use crate::host_logic::session::{SessionInfo, SessionState, encode_persisted_session};
use crate::host_logic::session_store::SessionStoreChangeNotifier;
-use crate::host_logic::sso::messages::RingVrfError;
+use crate::host_logic::sso::messages::{ProductRequest, RingVrfError};
use crate::subscription::Spawner;
use futures::StreamExt;
@@ -62,59 +65,6 @@ use super::signing_host::ring_vrf::{
development_context_bytes, member_from_entropy, sign_from_entropy,
};
-/// Distinguishes all remote authority request entrypoints by wire label.
-#[derive(Clone, Copy, Debug, derive_more::Display)]
-pub(super) enum AuthorityRequestKind {
- /// `sign_payload` with a product account.
- #[display("sign-payload")]
- SignPayload,
- /// `sign_raw` with a product account.
- #[display("sign-raw")]
- SignRaw,
- /// `create_transaction` with a product account.
- #[display("create-transaction")]
- CreateTransaction,
- /// `sign_payload` through the legacy-account API.
- #[display("legacy-sign-payload")]
- LegacySignPayload,
- /// `sign_raw` through the legacy-account API.
- #[display("legacy-sign-raw")]
- LegacySignRaw,
- /// `create_transaction` through the legacy-account API.
- #[display("legacy-create-transaction")]
- LegacyCreateTransaction,
-}
-
-impl From<&SignPayloadAuthorityRequest> for AuthorityRequestKind {
- fn from(request: &SignPayloadAuthorityRequest) -> Self {
- match request {
- SignPayloadAuthorityRequest::Product(_) => Self::SignPayload,
- SignPayloadAuthorityRequest::LegacyAccount { .. } => Self::LegacySignPayload,
- }
- }
-}
-
-impl From<&SignRawAuthorityRequest> for AuthorityRequestKind {
- fn from(request: &SignRawAuthorityRequest) -> Self {
- match request {
- SignRawAuthorityRequest::Product(_) => Self::SignRaw,
- SignRawAuthorityRequest::LegacyAccount { .. } => Self::LegacySignRaw,
- }
- }
-}
-
-impl From<&CreateTransactionAuthorityRequest> for AuthorityRequestKind {
- fn from(request: &CreateTransactionAuthorityRequest) -> Self {
- match request {
- CreateTransactionAuthorityRequest::Product(_) => Self::CreateTransaction,
- CreateTransactionAuthorityRequest::LegacyAccount { .. } => {
- Self::LegacyCreateTransaction
- }
- CreateTransactionAuthorityRequest::IdentityAccount(_) => Self::LegacyCreateTransaction,
- }
- }
-}
-
struct LoginInFlight {
waiters: Vec>>,
}
@@ -2024,7 +1974,7 @@ impl PairingHost {
fn mirror_ring_vrf_registration(
&self,
session: SessionInfo,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) {
let weak_self = self.weak_self.clone();
(self.spawner)(Box::pin(async move {
@@ -2136,21 +2086,23 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: AccountAliasAuthorityRequest,
+ request: ProductRequest,
) -> Result {
let private_session = self.current_private_session(session)?;
- if request.calling_product_id == request.key_handle.dot_ns_identifier
+ if request.calling_product_id == request.payload.key_handle.dot_ns_identifier
&& let Some(entropy) = self
.local_ring_vrf_entropy_for_ring(
&private_session,
- &request.key_handle,
- &request.ring_location,
+ &request.payload.key_handle,
+ &request.payload.ring_location,
)
.await?
{
- self.ring_resolver.validate(&request.ring_location).await?;
+ self.ring_resolver
+ .validate(&request.payload.ring_location)
+ .await?;
self.current_private_session(session)?;
- let context = development_context_bytes(&request.context);
+ let context = development_context_bytes(&request.payload.context);
let alias = alias_from_entropy(&entropy, &context)?;
return Ok(v01::ContextualAlias {
context,
@@ -2165,26 +2117,30 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: CreateProofAuthorityRequest,
+ request: ProductRequest,
) -> Result {
- Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle)?;
+ Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.payload.key_handle)?;
let private_session = self.current_private_session(session)?;
if let Some(entropy) = self
.local_ring_vrf_entropy_for_ring(
&private_session,
- &request.key_handle,
- &request.ring_location,
+ &request.payload.key_handle,
+ &request.payload.ring_location,
)
.await?
{
let member = member_from_entropy(&entropy)?;
let resolved = self
.ring_resolver
- .resolve(&request.ring_location, &[MemberCandidate { member }])
+ .resolve(
+ &request.payload.ring_location,
+ &[MemberCandidate { member }],
+ )
.await?;
self.current_private_session(session)?;
- let context = development_context_bytes(&request.context);
- let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?;
+ let context = development_context_bytes(&request.payload.context);
+ let (proof, alias) =
+ create_proof(&entropy, &resolved, &context, &request.payload.message)?;
return Ok(v01::HostAccountCreateProofResponse {
proof,
contextual_alias: v01::ContextualAlias {
@@ -2203,7 +2159,7 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) -> Result {
let private_session = self.current_private_session(session)?;
let handle = v01::ProductAccountId {
@@ -2212,25 +2168,25 @@ impl PairingHost {
reason: error.to_string(),
},
)?,
- derivation_index: request.index.clone(),
+ derivation_index: request.payload.index.clone(),
};
if let Some(auto_signing) = self
.auto_signing_key(&private_session, &request.calling_product_id)
.await
.map_err(RingVrfError::from)?
{
- self.ring_resolver.validate(&request.ring).await?;
+ self.ring_resolver.validate(&request.payload.ring).await?;
self.current_private_session(session)?;
let entropy = Zeroizing::new(derive_ring_vrf_entropy_from_domain(
auto_signing.ring_vrf_domain_entropy(),
- &request.index,
+ &request.payload.index,
));
let public_key = member_from_entropy(&entropy)?;
self.ring_vrf_registry
.register(
private_session.public_key,
handle,
- request.ring.clone(),
+ request.payload.ring.clone(),
public_key,
)
.await?;
@@ -2242,7 +2198,12 @@ impl PairingHost {
.remote_register_ring_vrf_key(cx, &private_session, request.clone())
.await?;
self.ring_vrf_registry
- .register(private_session.public_key, handle, request.ring, public_key)
+ .register(
+ private_session.public_key,
+ handle,
+ request.payload.ring,
+ public_key,
+ )
.await?;
self.current_private_session(session)?;
Ok(public_key)
@@ -2252,10 +2213,10 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: ListRingVrfKeysAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
let private_session = self.current_private_session(session)?;
- let owner = normalize_product_identifier(&request.owner).map_err(|error| {
+ let owner = normalize_product_identifier(&request.payload.owner).map_err(|error| {
RingVrfError::Unknown {
reason: error.to_string(),
}
@@ -2267,13 +2228,13 @@ impl PairingHost {
.await?
{
self.current_private_session(session)?;
- apply_ring_vrf_disclosure(&mut entries, request.disclosure);
+ apply_ring_vrf_disclosure(&mut entries, request.payload.disclosure);
return Ok(entries);
}
- let requested_disclosure = request.disclosure;
+ let requested_disclosure = request.payload.disclosure;
let mut remote_request = request;
if remote_request.calling_product_id == owner {
- remote_request.disclosure = v01::RingVrfKeyDisclosure::PublicKey;
+ remote_request.payload.disclosure = v01::RingVrfKeyDisclosure::PublicKey;
}
let mut entries = self
.remote_list_ring_vrf_keys(cx, &private_session, remote_request)
@@ -2294,16 +2255,16 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RingVrfSignAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
- Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle)?;
+ Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.payload.key_handle)?;
let private_session = self.current_private_session(session)?;
if let Some(entropy) = self
- .local_ring_vrf_entropy(&private_session, &request.key_handle)
+ .local_ring_vrf_entropy(&private_session, &request.payload.key_handle)
.await?
{
self.current_private_session(session)?;
- return sign_from_entropy(&entropy, &request.message);
+ return sign_from_entropy(&entropy, &request.payload.message);
}
self.remote_ring_vrf_sign(cx, &private_session, request)
.await
@@ -2516,7 +2477,7 @@ impl ProductAuthority for PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: AccountAliasAuthorityRequest,
+ request: ProductRequest,
) -> Result {
PairingHost::account_alias(self, cx, session, request).await
}
@@ -2525,7 +2486,7 @@ impl ProductAuthority for PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: CreateProofAuthorityRequest,
+ request: ProductRequest,
) -> Result {
PairingHost::create_proof(self, cx, session, request).await
}
@@ -2534,7 +2495,7 @@ impl ProductAuthority for PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) -> Result {
PairingHost::register_ring_vrf_key(self, cx, session, request).await
}
@@ -2543,7 +2504,7 @@ impl ProductAuthority for PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: ListRingVrfKeysAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
PairingHost::list_ring_vrf_keys(self, cx, session, request).await
}
@@ -2552,7 +2513,7 @@ impl ProductAuthority for PairingHost {
&self,
cx: &CallContext,
session: &AuthoritySession,
- request: RingVrfSignAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
PairingHost::ring_vrf_sign(self, cx, session, request).await
}
diff --git a/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs b/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs
index a19a7084b..b7bc2d386 100644
--- a/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs
+++ b/rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs
@@ -1,73 +1,32 @@
//! SSO statement-store channel to the paired remote signing host.
use super::super::authority::{
- AccountAliasAuthorityRequest, AuthorityCancelError, AuthorityError, BulletinAllowanceKey,
- CreateProofAuthorityRequest, CreateTransactionAuthorityRequest,
- ListRingVrfKeysAuthorityRequest, RegisterRingVrfKeyAuthorityRequest,
- RingVrfSignAuthorityRequest, SignPayloadAuthorityRequest, SignRawAuthorityRequest,
- StatementStoreAllowanceKey,
+ AuthorityCancelError, AuthorityError, BulletinAllowanceKey, CreateTransactionAuthorityRequest,
+ SignPayloadAuthorityRequest, SignRawAuthorityRequest, StatementStoreAllowanceKey,
};
use super::super::sso_remote::{
RemoteResponseWait, SSO_LOCAL_DISCONNECT_REASON, SSO_PEER_DISCONNECT_REASON,
- SsoRemoteResponseError, SsoSessionKey, fresh_statement_expiry, sso_message_id,
+ SsoRemoteResponseError, SsoSessionKey, fresh_statement_expiry, reply_matcher, sso_message_id,
statement_subscription_stream, subscribe_statement_topic, wait_for_sso_remote_response,
};
use super::super::statement_store_rpc::{self, StatementStoreRpc};
-use super::AuthorityRequestKind;
use super::PairingHost;
use crate::host_logic::session::{SessionInfo, SessionState, SsoSessionInfo};
use crate::host_logic::sso::messages::{
- OnExistingAllowancePolicy, RemoteMessage, RemoteMessageData, RingVrfError,
- SsoAllocatedResource, SsoAllocationOutcome, SsoRemoteResponse, SsoSessionStatement,
- alias_request_message, build_outgoing_request_statement, create_transaction_legacy_message,
- create_transaction_message, decode_sso_session_statement, list_ring_vrf_keys_message,
- product_subtree_request_message, proof_request_message, register_ring_vrf_key_message,
- resource_allocation_message, ring_vrf_sign_message, sign_payload_message,
- sign_raw_legacy_message, sign_raw_message, sign_vrf_message, v1,
+ CreateTransactionLegacyPayload, CreateTransactionPayload, CreateTransactionRequest,
+ CreateTransactionWithLegacyAccountRequest, OnExistingAllowancePolicy, ProductRequest,
+ ProductSubtreeRequest, RemoteMessage, RemoteMessageData, ResourceAllocationRequest,
+ RingVrfError, SignRawWithLegacyAccountRequest, SignRequest, SsoAllocatedResource,
+ SsoAllocationOutcome, SsoSessionStatement, build_outgoing_request_statement,
+ decode_sso_session_statement, v1,
};
+use crate::host_logic::sso::wire::SsoRequest;
use crate::host_logic::statement_store::parse_new_statements_result;
use futures::FutureExt;
use futures::future::{AbortHandle, Abortable};
use tracing::{debug, instrument, warn};
-use truapi::{CallContext, latest, v01};
-
-const UNEXPECTED_SSO_SIGNING_RESPONSE: &str = "Unexpected SSO response for signing request";
-const UNEXPECTED_SSO_TRANSACTION_RESPONSE: &str = "Unexpected SSO response for transaction request";
-const UNEXPECTED_SSO_ALIAS_RESPONSE: &str = "Unexpected SSO response for account alias request";
-const UNEXPECTED_SSO_PROOF_RESPONSE: &str = "Unexpected SSO response for ring-VRF proof request";
-const UNEXPECTED_SSO_REGISTER_RING_VRF_KEY_RESPONSE: &str =
- "Unexpected SSO response for ring-VRF key registration request";
-const UNEXPECTED_SSO_LIST_RING_VRF_KEYS_RESPONSE: &str =
- "Unexpected SSO response for ring-VRF key listing request";
-const UNEXPECTED_SSO_RING_VRF_SIGN_RESPONSE: &str =
- "Unexpected SSO response for ring-VRF signing request";
-
-fn unexpected_response_reason(context: &str, response_kind: &str) -> String {
- format!("{context}: {response_kind}")
-}
-
-#[derive(Clone, Copy, Debug, derive_more::Display)]
-enum RemoteAction {
- #[display("{_0}")]
- Signing(AuthorityRequestKind),
- #[display("account-alias")]
- RingVrfAlias,
- #[display("ring-vrf-proof")]
- RingVrfProof,
- #[display("register-ring-vrf-key")]
- RegisterRingVrfKey,
- #[display("list-ring-vrf-keys")]
- ListRingVrfKeys,
- #[display("ring-vrf-sign")]
- RingVrfSign,
- #[display("sign-vrf")]
- SignVrf,
- #[display("resource-allocation")]
- ResourceAllocation,
- #[display("product-subtree")]
- ProductSubtree,
-}
+use truapi::{CallContext, latest};
/// Active peer-disconnect watcher for one SSO session; aborts on drop.
pub(super) struct SsoDisconnectMonitor {
@@ -82,32 +41,6 @@ impl Drop for SsoDisconnectMonitor {
}
impl PairingHost {
- async fn submit_sign_request(
- &self,
- cx: &CallContext,
- session: &SessionInfo,
- action: AuthorityRequestKind,
- message: RemoteMessage,
- ) -> Result {
- let response = self
- .submit_remote_message(cx, session, RemoteAction::Signing(action), message)
- .await?;
- let response_kind = response.kind();
- let SsoRemoteResponse::Sign(response) = response else {
- return Err(SsoRemoteResponseError::Failure(unexpected_response_reason(
- UNEXPECTED_SSO_SIGNING_RESPONSE,
- response_kind,
- )));
- };
- response
- .payload
- .map(|payload| latest::HostSignPayloadResponse {
- signature: payload.signature,
- signed_transaction: payload.signed_transaction,
- })
- .map_err(SsoRemoteResponseError::Failure)
- }
-
fn stop_disconnect_monitor(&self) {
self.disconnect_monitor
.lock()
@@ -207,15 +140,17 @@ impl PairingHost {
Ok(())
}
- /// Submit an SSO remote message and wait for the signing-host response.
- #[instrument(skip_all, fields(runtime.method = "sso.remote_message.submit", action = %action))]
- async fn submit_remote_message(
+ /// Send `request` to the paired signing host and await its typed answer.
+ ///
+ /// The outer error is the transport's; the inner result is the peer's
+ /// payload for this request type.
+ #[instrument(skip_all, fields(runtime.method = "sso.remote_message.submit", action = R::NAME))]
+ async fn call(
&self,
cx: &CallContext,
session: &SessionInfo,
- action: RemoteAction,
- message: RemoteMessage,
- ) -> Result {
+ request: R,
+ ) -> Result {
let sso = session
.sso
.as_ref()
@@ -225,11 +160,11 @@ impl PairingHost {
if !session_matches_key(&self.session_state, key) {
return Err(SsoRemoteResponseError::LocalDisconnected);
}
- let message_id = message.message_id.clone();
+ let message_id = sso_message_id();
let statement = build_outgoing_request_statement(
sso,
message_id.clone(),
- vec![message],
+ vec![RemoteMessage::request(message_id.clone(), request)],
fresh_statement_expiry(),
)
.map_err(SsoRemoteResponseError::Failure)?;
@@ -265,18 +200,21 @@ impl PairingHost {
})
}
.boxed();
- let action = action.to_string();
+ let action = R::NAME;
debug!(action, %message_id, "submitted SSO remote message, awaiting response");
- let result = wait_for_sso_remote_response(RemoteResponseWait {
- own_statements: statement_subscription_stream(own_subscription, "own"),
- peer_statements: statement_subscription_stream(peer_subscription, "peer"),
- submit,
- session: sso,
- statement_request_id: &message_id,
- remote_message_id: &message_id,
- cancel: cx.cancel(),
- disconnect: Some(disconnect),
- })
+ let result = wait_for_sso_remote_response(
+ RemoteResponseWait {
+ own_statements: statement_subscription_stream(own_subscription, "own"),
+ peer_statements: statement_subscription_stream(peer_subscription, "peer"),
+ submit,
+ session: sso,
+ statement_request_id: &message_id,
+ remote_message_id: &message_id,
+ cancel: cx.cancel(),
+ disconnect: Some(disconnect),
+ },
+ reply_matcher::(&message_id),
+ )
.await;
let result = result.map_err(|reason| match reason {
SsoRemoteResponseError::Cancelled(err) if !cx.request_id().is_empty() => {
@@ -291,7 +229,7 @@ impl PairingHost {
if matches!(&result, Err(SsoRemoteResponseError::PeerDisconnected)) {
self.handle_signing_host_disconnected(key).await;
}
- result
+ result.map(|response| response.payload)
}
/// Resolve a product's hard-subtree public key, asking the Account Holder
@@ -308,24 +246,10 @@ impl PairingHost {
if let Some(public_key) = self.known_product_subtree(session, cache_key.clone()).await {
return Ok(public_key);
}
-
- let message_id = sso_message_id();
- let message = product_subtree_request_message(message_id, product_id);
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ProductSubtree, message)
+ let public_key = self
+ .call(cx, session, ProductSubtreeRequest { product_id })
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ProductSubtree(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for product subtree request",
- response_kind,
- ),
- });
- };
- let public_key = response
- .product_public_key
+ .map_err(remote_authority_error)?
.map_err(remote_authority_error)?;
if !self
.persist_product_subtree_if_current(session, lifecycle_epoch, cache_key, public_key)
@@ -342,39 +266,38 @@ impl PairingHost {
cx: &CallContext,
session: &SessionInfo,
calling_product_id: String,
- request: v01::HostAccountSignVrfRequest,
- ) -> Result {
- let message_id = sso_message_id();
- let message = sign_vrf_message(message_id, calling_product_id, request);
- let response = self
- .submit_remote_message(cx, session, RemoteAction::SignVrf, message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::SignVrf(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for VRF signing request",
- response_kind,
- ),
- });
- };
- response.payload.map_err(|err| match err {
- v01::HostAccountSignVrfError::NotConnected => AuthorityError::Disconnected,
- v01::HostAccountSignVrfError::Rejected => AuthorityError::Rejected,
- v01::HostAccountSignVrfError::Unknown { reason } => AuthorityError::Unknown { reason },
+ request: latest::HostAccountSignVrfRequest,
+ ) -> Result {
+ self.call(
+ cx,
+ session,
+ ProductRequest {
+ calling_product_id,
+ payload: request,
+ },
+ )
+ .await
+ .map_err(remote_authority_error)?
+ .map_err(|err| match err {
+ latest::HostAccountSignVrfError::NotConnected => AuthorityError::Disconnected,
+ latest::HostAccountSignVrfError::Rejected => AuthorityError::Rejected,
+ latest::HostAccountSignVrfError::Unknown { reason } => {
+ AuthorityError::Unknown { reason }
+ }
})
}
/// Forward a payload-signing request to the paired signing host.
+ #[instrument(skip_all, fields(account_kind = match &request {
+ SignPayloadAuthorityRequest::Product(_) => "product",
+ SignPayloadAuthorityRequest::LegacyAccount { .. } => "legacy",
+ }))]
pub(super) async fn remote_sign_payload(
&self,
cx: &CallContext,
session: &SessionInfo,
request: SignPayloadAuthorityRequest,
) -> Result {
- let action = AuthorityRequestKind::from(&request);
- let message_id = sso_message_id();
let request = match request {
SignPayloadAuthorityRequest::Product(request) => request,
SignPayloadAuthorityRequest::LegacyAccount {
@@ -385,101 +308,105 @@ impl PairingHost {
payload: request.payload,
},
};
- let message = sign_payload_message(message_id, request);
- self.submit_sign_request(cx, session, action, message)
+ self.call(cx, session, SignRequest::Payload(Box::new(request)))
.await
+ .map_err(remote_authority_error)?
.map_err(remote_authority_error)
}
/// Forward a raw-signing request to the paired signing host.
+ #[instrument(skip_all, fields(account_kind = match &request {
+ SignRawAuthorityRequest::Product(_) => "product",
+ SignRawAuthorityRequest::LegacyAccount { .. } => "legacy",
+ }))]
pub(super) async fn remote_sign_raw(
&self,
cx: &CallContext,
session: &SessionInfo,
request: SignRawAuthorityRequest,
) -> Result {
- let action = AuthorityRequestKind::from(&request);
- let message_id = sso_message_id();
- let (message, expects_legacy_response) = match request {
- SignRawAuthorityRequest::Product(request) => {
- (sign_raw_message(message_id, request), false)
- }
- SignRawAuthorityRequest::LegacyAccount { account, request } => (
- sign_raw_legacy_message(message_id, account, request.payload),
- true,
- ),
- };
- let response = self
- .submit_remote_message(cx, session, RemoteAction::Signing(action), message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- match (expects_legacy_response, response) {
- (false, SsoRemoteResponse::Sign(response)) => response
- .payload
- .map(|payload| latest::HostSignPayloadResponse {
- signature: payload.signature,
- signed_transaction: payload.signed_transaction,
- })
+ match request {
+ SignRawAuthorityRequest::Product(request) => self
+ .call(cx, session, SignRequest::Raw(request))
+ .await
+ .map_err(remote_authority_error)?
.map_err(remote_authority_error),
- (true, SsoRemoteResponse::SignRawLegacy(response)) => response
- .signature
- .map(|signature| latest::HostSignPayloadResponse {
+ SignRawAuthorityRequest::LegacyAccount { account, request } => {
+ let signature = self
+ .call(
+ cx,
+ session,
+ SignRawWithLegacyAccountRequest {
+ account,
+ data: request.payload,
+ },
+ )
+ .await
+ .map_err(remote_authority_error)?
+ .map_err(remote_authority_error)?;
+ Ok(latest::HostSignPayloadResponse {
signature,
signed_transaction: None,
})
- .map_err(remote_authority_error),
- _ => Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(UNEXPECTED_SSO_SIGNING_RESPONSE, response_kind),
- }),
+ }
}
}
/// Forward a transaction-creation request to the paired signing host.
+ #[instrument(skip_all, fields(account_kind = match &request {
+ CreateTransactionAuthorityRequest::Product(_) => "product",
+ CreateTransactionAuthorityRequest::LegacyAccount { .. } => "legacy",
+ CreateTransactionAuthorityRequest::IdentityAccount(_) => "identity",
+ }))]
pub(super) async fn remote_create_transaction(
&self,
cx: &CallContext,
session: &SessionInfo,
request: CreateTransactionAuthorityRequest,
) -> Result {
- let action = AuthorityRequestKind::from(&request);
- let message_id = sso_message_id();
- let message = match request {
- CreateTransactionAuthorityRequest::Product(request) => {
- create_transaction_message(message_id, request)
+ let signed = match request {
+ CreateTransactionAuthorityRequest::Product(payload) => {
+ self.call(
+ cx,
+ session,
+ CreateTransactionRequest {
+ payload: CreateTransactionPayload::V1(payload),
+ },
+ )
+ .await
}
CreateTransactionAuthorityRequest::LegacyAccount {
product_account,
request,
- } => create_transaction_message(
- message_id,
- latest::ProductAccountTxPayload {
- signer: product_account,
- genesis_hash: request.genesis_hash,
- call_data: request.call_data,
- extensions: request.extensions,
- tx_ext_version: request.tx_ext_version,
- },
- ),
- CreateTransactionAuthorityRequest::IdentityAccount(request) => {
- create_transaction_legacy_message(message_id, request)
+ } => {
+ self.call(
+ cx,
+ session,
+ CreateTransactionRequest {
+ payload: CreateTransactionPayload::V1(latest::ProductAccountTxPayload {
+ signer: product_account,
+ genesis_hash: request.genesis_hash,
+ call_data: request.call_data,
+ extensions: request.extensions,
+ tx_ext_version: request.tx_ext_version,
+ }),
+ },
+ )
+ .await
+ }
+ CreateTransactionAuthorityRequest::IdentityAccount(payload) => {
+ self.call(
+ cx,
+ session,
+ CreateTransactionWithLegacyAccountRequest {
+ payload: CreateTransactionLegacyPayload::V1(payload),
+ },
+ )
+ .await
}
};
- let response = self
- .submit_remote_message(cx, session, RemoteAction::Signing(action), message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::CreateTransaction(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- UNEXPECTED_SSO_TRANSACTION_RESPONSE,
- response_kind,
- ),
- });
- };
- response
- .signed_transaction
+ signed
+ .map_err(remote_authority_error)?
.map(|transaction| latest::HostCreateTransactionResponse { transaction })
.map_err(remote_authority_error)
}
@@ -489,27 +416,11 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &SessionInfo,
- request: AccountAliasAuthorityRequest,
+ request: ProductRequest,
) -> Result {
- let message_id = sso_message_id();
- let message = alias_request_message(
- message_id,
- request.calling_product_id,
- request.key_handle,
- request.context,
- request.ring_location,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::RingVrfAlias, message)
+ self.call(cx, session, request)
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::RingVrfAlias(response) = response else {
- return Err(RingVrfError::Unknown {
- reason: unexpected_response_reason(UNEXPECTED_SSO_ALIAS_RESPONSE, response_kind),
- });
- };
- response.payload
+ .map_err(ring_vrf_transport_error)?
}
/// Forward a ring-VRF proof request to the paired signing host.
@@ -517,28 +428,11 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &SessionInfo,
- request: CreateProofAuthorityRequest,
+ request: ProductRequest,
) -> Result {
- let message_id = sso_message_id();
- let message = proof_request_message(
- message_id,
- request.calling_product_id,
- request.key_handle,
- request.context,
- request.ring_location,
- request.message,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::RingVrfProof, message)
+ self.call(cx, session, request)
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::RingVrfProof(response) = response else {
- return Err(RingVrfError::Unknown {
- reason: unexpected_response_reason(UNEXPECTED_SSO_PROOF_RESPONSE, response_kind),
- });
- };
- response.payload
+ .map_err(ring_vrf_transport_error)?
}
/// Forward a ring-VRF key registration request to the paired signing host.
@@ -546,29 +440,11 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &SessionInfo,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) -> Result {
- let message_id = sso_message_id();
- let message = register_ring_vrf_key_message(
- message_id,
- request.calling_product_id,
- request.index,
- request.ring,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::RegisterRingVrfKey, message)
+ self.call(cx, session, request)
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::RegisterRingVrfKey(response) = response else {
- return Err(RingVrfError::Unknown {
- reason: unexpected_response_reason(
- UNEXPECTED_SSO_REGISTER_RING_VRF_KEY_RESPONSE,
- response_kind,
- ),
- });
- };
- response.payload
+ .map_err(ring_vrf_transport_error)?
}
/// Forward a ring-VRF key listing request to the paired signing host.
@@ -576,29 +452,11 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &SessionInfo,
- request: ListRingVrfKeysAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
- let message_id = sso_message_id();
- let message = list_ring_vrf_keys_message(
- message_id,
- request.calling_product_id,
- request.owner,
- request.disclosure,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ListRingVrfKeys, message)
+ self.call(cx, session, request)
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ListRingVrfKeys(response) = response else {
- return Err(RingVrfError::Unknown {
- reason: unexpected_response_reason(
- UNEXPECTED_SSO_LIST_RING_VRF_KEYS_RESPONSE,
- response_kind,
- ),
- });
- };
- response.payload
+ .map_err(ring_vrf_transport_error)?
}
/// Forward a direct ring-VRF signing request to the paired signing host.
@@ -606,29 +464,11 @@ impl PairingHost {
&self,
cx: &CallContext,
session: &SessionInfo,
- request: RingVrfSignAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
- let message_id = sso_message_id();
- let message = ring_vrf_sign_message(
- message_id,
- request.calling_product_id,
- request.key_handle,
- request.message,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::RingVrfSign, message)
+ self.call(cx, session, request)
.await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::RingVrfSign(response) = response else {
- return Err(RingVrfError::Unknown {
- reason: unexpected_response_reason(
- UNEXPECTED_SSO_RING_VRF_SIGN_RESPONSE,
- response_kind,
- ),
- });
- };
- response.payload
+ .map_err(ring_vrf_transport_error)?
}
/// Ask the paired signing host to allocate product resources, caching any
@@ -641,27 +481,19 @@ impl PairingHost {
request: latest::HostRequestResourceAllocationRequest,
) -> Result {
let lifecycle_epoch = self.current_session_lifecycle_epoch();
- let message_id = sso_message_id();
- let message = resource_allocation_message(
- message_id,
- product_id.clone(),
- request.resources,
- OnExistingAllowancePolicy::Increase,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ResourceAllocation, message)
+ let outcomes = self
+ .call(
+ cx,
+ session,
+ ResourceAllocationRequest {
+ calling_product_id: product_id.clone(),
+ resources: request.resources,
+ on_existing: OnExistingAllowancePolicy::Increase,
+ },
+ )
.await
+ .map_err(remote_authority_error)?
.map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ResourceAllocation(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for resource allocation request",
- response_kind,
- ),
- });
- };
- let outcomes = response.payload.map_err(remote_authority_error)?;
self.cache_allowance_outcomes(cx, session, lifecycle_epoch, &product_id, &outcomes)
.await?;
Ok(latest::HostRequestResourceAllocationResponse {
@@ -669,6 +501,41 @@ impl PairingHost {
})
}
+ /// Allocate exactly one allowance for the product and return its material.
+ async fn remote_allowance_slot(
+ &self,
+ cx: &CallContext,
+ session: &SessionInfo,
+ product_id: &str,
+ resource: latest::AllocatableResource,
+ on_existing: OnExistingAllowancePolicy,
+ ) -> Result {
+ let name = allowance_name(&resource);
+ let outcomes = self
+ .call(
+ cx,
+ session,
+ ResourceAllocationRequest {
+ calling_product_id: product_id.to_string(),
+ resources: vec![resource],
+ on_existing,
+ },
+ )
+ .await
+ .map_err(remote_authority_error)?
+ .map_err(remote_authority_error)?;
+ match outcomes.into_iter().next() {
+ Some(SsoAllocationOutcome::Allocated(resource)) => Ok(resource),
+ Some(SsoAllocationOutcome::Rejected) => Err(AuthorityError::Rejected),
+ Some(SsoAllocationOutcome::NotAvailable) => Err(AuthorityError::Unavailable {
+ reason: format!("{name} is not available"),
+ }),
+ None => Err(AuthorityError::Unknown {
+ reason: format!("Empty {name} response"),
+ }),
+ }
+ }
+
/// Statement-store allowance key for the product, served from the cache
/// or allocated by the paired signing host.
pub(super) async fn remote_statement_store_allowance_key(
@@ -684,38 +551,17 @@ impl PairingHost {
{
return Ok(cached);
}
-
- let message_id = sso_message_id();
- let message = resource_allocation_message(
- message_id,
- product_id.clone(),
- vec![latest::AllocatableResource::StatementStoreAllowance],
- OnExistingAllowancePolicy::Ignore,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ResourceAllocation, message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ResourceAllocation(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for statement-store allowance request",
- response_kind,
- ),
- });
- };
- let mut outcomes = response
- .payload
- .map_err(remote_authority_error)?
- .into_iter();
- let outcome = outcomes.next().ok_or_else(|| AuthorityError::Unknown {
- reason: "Empty statement-store allowance response".to_string(),
- })?;
- match outcome {
- SsoAllocationOutcome::Allocated(SsoAllocatedResource::StatementStoreAllowance {
- slot_account_key,
- }) => {
+ match self
+ .remote_allowance_slot(
+ cx,
+ session,
+ &product_id,
+ latest::AllocatableResource::StatementStoreAllowance,
+ OnExistingAllowancePolicy::Ignore,
+ )
+ .await?
+ {
+ SsoAllocatedResource::StatementStoreAllowance { slot_account_key } => {
self.cache_statement_store_allowance_key(
session,
lifecycle_epoch,
@@ -724,16 +570,7 @@ impl PairingHost {
)
.await
}
- SsoAllocationOutcome::Allocated(other) => Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected statement-store allowance response resource",
- other.kind(),
- ),
- }),
- SsoAllocationOutcome::Rejected => Err(AuthorityError::Rejected),
- SsoAllocationOutcome::NotAvailable => Err(AuthorityError::Unavailable {
- reason: "statement-store allowance is not available".to_string(),
- }),
+ other => Err(unexpected_resource("statement-store allowance", &other)),
}
}
@@ -752,61 +589,18 @@ impl PairingHost {
{
return Ok(cached);
}
-
- let message_id = sso_message_id();
- let message = resource_allocation_message(
- message_id,
- product_id.clone(),
- vec![latest::AllocatableResource::BulletinAllowance],
+ self.allocate_bulletin_allowance_key(
+ cx,
+ session,
+ lifecycle_epoch,
+ product_id,
OnExistingAllowancePolicy::Ignore,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ResourceAllocation, message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ResourceAllocation(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for bulletin allowance request",
- response_kind,
- ),
- });
- };
- let mut outcomes = response
- .payload
- .map_err(remote_authority_error)?
- .into_iter();
- let outcome = outcomes.next().ok_or_else(|| AuthorityError::Unknown {
- reason: "Empty bulletin allowance response".to_string(),
- })?;
- match outcome {
- SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance {
- slot_account_key,
- }) => {
- self.cache_bulletin_allowance_key(
- session,
- lifecycle_epoch,
- &product_id,
- slot_account_key,
- )
- .await
- }
- SsoAllocationOutcome::Allocated(other) => Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected bulletin allowance response resource",
- other.kind(),
- ),
- }),
- SsoAllocationOutcome::Rejected => Err(AuthorityError::Rejected),
- SsoAllocationOutcome::NotAvailable => Err(AuthorityError::Unavailable {
- reason: "bulletin allowance is not available".to_string(),
- }),
- }
+ )
+ .await
}
/// Evict the cached Bulletin allowance key and allocate a fresh one with
- /// an increased allowance.
+ /// an increased allowance, so a stale or exhausted slot is never reused.
pub(super) async fn remote_refresh_bulletin_allowance_key(
&self,
cx: &CallContext,
@@ -814,43 +608,37 @@ impl PairingHost {
product_id: String,
) -> Result {
let lifecycle_epoch = self.current_session_lifecycle_epoch();
- // Drop the cached (and persisted) key so a stale/exhausted slot is not
- // reused, then request a fresh allocation with `Increase` so the
- // wallet grants a new allowance rather than echoing the old slot.
self.evict_bulletin_allowance_key(session, lifecycle_epoch, &product_id)
.await?;
-
- let message_id = sso_message_id();
- let message = resource_allocation_message(
- message_id,
- product_id.clone(),
- vec![latest::AllocatableResource::BulletinAllowance],
+ self.allocate_bulletin_allowance_key(
+ cx,
+ session,
+ lifecycle_epoch,
+ product_id,
OnExistingAllowancePolicy::Increase,
- );
- let response = self
- .submit_remote_message(cx, session, RemoteAction::ResourceAllocation, message)
- .await
- .map_err(remote_authority_error)?;
- let response_kind = response.kind();
- let SsoRemoteResponse::ResourceAllocation(response) = response else {
- return Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected SSO response for bulletin allowance refresh",
- response_kind,
- ),
- });
- };
- let mut outcomes = response
- .payload
- .map_err(remote_authority_error)?
- .into_iter();
- let outcome = outcomes.next().ok_or_else(|| AuthorityError::Unknown {
- reason: "Empty bulletin allowance refresh response".to_string(),
- })?;
- match outcome {
- SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance {
- slot_account_key,
- }) => {
+ )
+ .await
+ }
+
+ async fn allocate_bulletin_allowance_key(
+ &self,
+ cx: &CallContext,
+ session: &SessionInfo,
+ lifecycle_epoch: u64,
+ product_id: String,
+ on_existing: OnExistingAllowancePolicy,
+ ) -> Result {
+ match self
+ .remote_allowance_slot(
+ cx,
+ session,
+ &product_id,
+ latest::AllocatableResource::BulletinAllowance,
+ on_existing,
+ )
+ .await?
+ {
+ SsoAllocatedResource::BulletinAllowance { slot_account_key } => {
self.cache_bulletin_allowance_key(
session,
lifecycle_epoch,
@@ -859,16 +647,7 @@ impl PairingHost {
)
.await
}
- SsoAllocationOutcome::Allocated(other) => Err(AuthorityError::Unknown {
- reason: unexpected_response_reason(
- "Unexpected bulletin allowance refresh resource",
- other.kind(),
- ),
- }),
- SsoAllocationOutcome::Rejected => Err(AuthorityError::Rejected),
- SsoAllocationOutcome::NotAvailable => Err(AuthorityError::Unavailable {
- reason: "bulletin allowance is not available".to_string(),
- }),
+ other => Err(unexpected_resource("bulletin allowance", &other)),
}
}
@@ -936,6 +715,26 @@ pub(super) fn session_matches_key(session_state: &SessionState, key: SsoSessionK
})
}
+fn ring_vrf_transport_error(reason: SsoRemoteResponseError) -> RingVrfError {
+ remote_authority_error(reason).into()
+}
+
+fn allowance_name(resource: &latest::AllocatableResource) -> &'static str {
+ match resource {
+ latest::AllocatableResource::StatementStoreAllowance => "statement-store allowance",
+ latest::AllocatableResource::BulletinAllowance => "bulletin allowance",
+ _ => "resource",
+ }
+}
+
+/// Reason for an allocation that came back as a different resource kind; names
+/// only the kind so no key material reaches logs.
+fn unexpected_resource(label: &str, resource: &SsoAllocatedResource) -> AuthorityError {
+ AuthorityError::Unknown {
+ reason: format!("Unexpected {label} response resource: {}", resource.kind()),
+ }
+}
+
fn remote_authority_error(reason: impl Into) -> AuthorityError {
match reason.into() {
SsoRemoteResponseError::Cancelled(err) => AuthorityError::Cancelled(
@@ -972,16 +771,18 @@ async fn wait_for_sso_peer_disconnect(
let page = parse_new_statements_result("sso-peer-disconnect-monitor".to_string(), &value)
.map_err(|err| err.to_string())?;
for statement in page.statements {
- if matches!(
- decode_sso_session_statement(
- &session,
- &statement,
- "truapi:sso-peer-disconnect-monitor",
- "truapi:sso-peer-disconnect-monitor",
- )?,
- Some(SsoSessionStatement::Disconnected)
- ) {
- return Ok(());
+ let Some(SsoSessionStatement::RemoteMessages(messages)) = decode_sso_session_statement(
+ &session,
+ &statement,
+ "truapi:sso-peer-disconnect-monitor",
+ )?
+ else {
+ continue;
+ };
+ for message in messages {
+ if message? == v1::RemoteMessage::Disconnected {
+ return Ok(());
+ }
}
}
}
@@ -1001,35 +802,24 @@ impl From for latest::AllocationOutcome {
#[cfg(test)]
mod tests {
use super::*;
- use crate::host_logic::sso::messages::ResourceAllocationResponse;
#[test]
- fn unexpected_response_reasons_include_only_safe_discriminants() {
- let private_key = [0xA5; 64];
+ fn unexpected_resource_reasons_include_only_safe_discriminants() {
let resource = SsoAllocatedResource::AutoSigning {
- product_root_private_key: private_key,
+ product_root_private_key: [0xA5; 64],
ring_vrf_domain_entropy: [0x5A; 32],
};
- let resource_reason = unexpected_response_reason(
- "Unexpected statement-store allowance response resource",
- resource.kind(),
- );
- assert_eq!(
- resource_reason,
- "Unexpected statement-store allowance response resource: auto-signing"
- );
- assert!(!resource_reason.contains("165, 165"));
- let response = SsoRemoteResponse::ResourceAllocation(ResourceAllocationResponse {
- responding_to: "secret-test".to_string(),
- payload: Ok(vec![SsoAllocationOutcome::Allocated(resource)]),
- });
- let response_reason =
- unexpected_response_reason(UNEXPECTED_SSO_SIGNING_RESPONSE, response.kind());
+ let AuthorityError::Unknown { reason } =
+ unexpected_resource("statement-store allowance", &resource)
+ else {
+ panic!("expected an unknown authority error");
+ };
+
assert_eq!(
- response_reason,
- "Unexpected SSO response for signing request: resource-allocation"
+ reason,
+ "Unexpected statement-store allowance response resource: auto-signing"
);
- assert!(!response_reason.contains("165, 165"));
+ assert!(!reason.contains("165, 165"));
}
}
diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs
index c61543307..ece69f522 100644
--- a/rust/crates/truapi-server/src/runtime/signing_host.rs
+++ b/rust/crates/truapi-server/src/runtime/signing_host.rs
@@ -18,9 +18,14 @@ mod local_activation;
pub(super) mod ring_vrf;
mod sso_replay;
mod sso_responder;
+mod sso_service;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
+use truapi::latest::{
+ HostAccountCreateProofRequest, HostAccountGetAliasRequest, HostAccountListRingVrfKeysRequest,
+ HostAccountRegisterRingVrfKeyRequest, HostAccountRingVrfSignRequest,
+};
use parity_scale_codec::Encode;
use subxt::utils::{AccountId32, MultiSignature};
@@ -29,15 +34,12 @@ use subxt::utils::{AccountId32, MultiSignature};
pub use allowance_renewal::StatementRenewalTarget;
pub(crate) use local_activation::LocalActivation;
pub use sso_responder::{PairedSsoPeer, ResponderExit};
-pub(crate) use sso_responder::{
- answer_remote_message, establish_pairing, respond_to_pairing, resume_pairing,
-};
+pub(crate) use sso_responder::{establish_pairing, respond_to_pairing, resume_pairing};
+pub(crate) use sso_service::SigningHostSsoService;
use super::authority::{
- AccountAliasAuthorityRequest, AuthorityError, AuthoritySession, BulletinAllowanceKey,
- CreateProofAuthorityRequest, CreateTransactionAuthorityRequest,
- ListRingVrfKeysAuthorityRequest, ProductAuthority, RegisterRingVrfKeyAuthorityRequest,
- RingVrfSignAuthorityRequest, SignPayloadAuthorityRequest, SignRawAuthorityRequest,
+ AuthorityError, AuthoritySession, BulletinAllowanceKey, CreateTransactionAuthorityRequest,
+ ProductAuthority, SignPayloadAuthorityRequest, SignRawAuthorityRequest,
StatementStoreAllowanceKey, authority_session_validation_id,
};
use super::ring_vrf_registry::RingVrfRegistryStore;
@@ -57,7 +59,7 @@ use crate::host_logic::product_account::{
derive_full_person_ring_vrf_entropy, derive_lite_person_ring_vrf_entropy,
};
use crate::host_logic::session::{SessionInfo, SessionState};
-use crate::host_logic::sso::messages::{OnExistingAllowancePolicy, RingVrfError};
+use crate::host_logic::sso::messages::{OnExistingAllowancePolicy, ProductRequest, RingVrfError};
use crate::host_logic::transaction::{extrinsic_payload_extensions, extrinsic_payload_preimage};
use crate::runtime::auth_state::AuthStateMachine;
#[cfg(not(target_arch = "wasm32"))]
@@ -843,13 +845,13 @@ impl ProductAuthority for SigningHost {
&self,
_cx: &CallContext,
session: &AuthoritySession,
- request: AccountAliasAuthorityRequest,
+ request: ProductRequest,
) -> Result {
self.require_current_session(session)?;
match super::account_access_authorization(
self.services.platform.as_ref(),
&request.calling_product_id,
- &request.key_handle.dot_ns_identifier,
+ &request.payload.key_handle.dot_ns_identifier,
)
.await
{
@@ -865,10 +867,16 @@ impl ProductAuthority for SigningHost {
}
}
let entropy = self
- .resolve_ring_vrf_key_for_ring(session, &request.key_handle, &request.ring_location)
+ .resolve_ring_vrf_key_for_ring(
+ session,
+ &request.payload.key_handle,
+ &request.payload.ring_location,
+ )
.await?;
- self.ring_resolver.validate(&request.ring_location).await?;
- let context = development_context_bytes(&request.context);
+ self.ring_resolver
+ .validate(&request.payload.ring_location)
+ .await?;
+ let context = development_context_bytes(&request.payload.context);
let alias = alias_from_entropy(&entropy, &context)?;
Ok(v01::ContextualAlias {
context,
@@ -880,23 +888,27 @@ impl ProductAuthority for SigningHost {
&self,
_cx: &CallContext,
session: &AuthoritySession,
- request: CreateProofAuthorityRequest,
+ request: ProductRequest,
) -> Result {
self.require_current_session(session)?;
- Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle)?;
+ Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.payload.key_handle)?;
let entropy = self
- .resolve_ring_vrf_key_for_ring(session, &request.key_handle, &request.ring_location)
+ .resolve_ring_vrf_key_for_ring(
+ session,
+ &request.payload.key_handle,
+ &request.payload.ring_location,
+ )
.await?;
let candidate = self.ring_vrf_member_candidate(&entropy)?;
let resolved = self
.ring_resolver
- .resolve(&request.ring_location, &[candidate])
+ .resolve(&request.payload.ring_location, &[candidate])
.await?;
// Reject a stale request if the local session disconnected or changed
// while its chain snapshot was being resolved.
self.require_current_session(session)?;
- let context = development_context_bytes(&request.context);
- let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?;
+ let context = development_context_bytes(&request.payload.context);
+ let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.payload.message)?;
Ok(v01::HostAccountCreateProofResponse {
proof,
contextual_alias: v01::ContextualAlias {
@@ -912,10 +924,10 @@ impl ProductAuthority for SigningHost {
&self,
_cx: &CallContext,
session: &AuthoritySession,
- request: RegisterRingVrfKeyAuthorityRequest,
+ request: ProductRequest,
) -> Result {
self.require_current_session(session)?;
- self.ring_resolver.validate(&request.ring).await?;
+ self.ring_resolver.validate(&request.payload.ring).await?;
let handle = v01::ProductAccountId {
dot_ns_identifier: normalize_product_identifier(&request.calling_product_id).map_err(
@@ -923,12 +935,12 @@ impl ProductAuthority for SigningHost {
reason: err.to_string(),
},
)?,
- derivation_index: request.index,
+ derivation_index: request.payload.index,
};
let entropy = self.ring_vrf_entropy(session, &handle)?;
let public_key = member_from_entropy(&entropy)?;
self.ring_vrf_registry
- .register(session.public_key, handle, request.ring, public_key)
+ .register(session.public_key, handle, request.payload.ring, public_key)
.await?;
Ok(public_key)
}
@@ -937,13 +949,14 @@ impl ProductAuthority for SigningHost {
&self,
_cx: &CallContext,
session: &AuthoritySession,
- request: ListRingVrfKeysAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
self.require_current_session(session)?;
- let owner =
- normalize_product_identifier(&request.owner).map_err(|err| RingVrfError::Unknown {
+ let owner = normalize_product_identifier(&request.payload.owner).map_err(|err| {
+ RingVrfError::Unknown {
reason: err.to_string(),
- })?;
+ }
+ })?;
if request.calling_product_id != owner {
match super::account_access_authorization(
self.services.platform.as_ref(),
@@ -969,7 +982,7 @@ impl ProductAuthority for SigningHost {
.ring_vrf_registry
.owner_entries(session.public_key, &owner)
.await?;
- if request.disclosure == v01::RingVrfKeyDisclosure::Anonymized {
+ if request.payload.disclosure == v01::RingVrfKeyDisclosure::Anonymized {
for entry in &mut entries {
entry.public_key = None;
}
@@ -981,14 +994,14 @@ impl ProductAuthority for SigningHost {
&self,
_cx: &CallContext,
session: &AuthoritySession,
- request: RingVrfSignAuthorityRequest,
+ request: ProductRequest,
) -> Result, RingVrfError> {
self.require_current_session(session)?;
- Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.key_handle)?;
+ Self::require_owned_ring_vrf_key(&request.calling_product_id, &request.payload.key_handle)?;
let entropy = self
- .resolve_registered_ring_vrf_key(session, &request.key_handle)
+ .resolve_registered_ring_vrf_key(session, &request.payload.key_handle)
.await?;
- sign_from_entropy(&entropy, &request.message)
+ sign_from_entropy(&entropy, &request.payload.message)
}
async fn allocate_resources(
@@ -1006,6 +1019,7 @@ impl ProductAuthority for SigningHost {
sso_responder::allocate_statement_store_allowance(
&self.services,
self,
+ session,
&product_id,
OnExistingAllowancePolicy::Increase,
)
@@ -1016,6 +1030,7 @@ impl ProductAuthority for SigningHost {
sso_responder::allocate_bulletin_allowance(
&self.services,
self,
+ session,
&product_id,
OnExistingAllowancePolicy::Increase,
)
@@ -1026,6 +1041,7 @@ impl ProductAuthority for SigningHost {
sso_responder::allocate_smart_contract_allowance(
&self.services,
self,
+ session,
&product_id,
index,
OnExistingAllowancePolicy::Increase,
@@ -1059,6 +1075,7 @@ impl ProductAuthority for SigningHost {
let secret = sso_responder::allocate_statement_store_allowance(
&self.services,
self,
+ session,
&product_id,
OnExistingAllowancePolicy::Ignore,
)
@@ -1077,6 +1094,7 @@ impl ProductAuthority for SigningHost {
let secret = sso_responder::allocate_bulletin_allowance(
&self.services,
self,
+ session,
&product_id,
OnExistingAllowancePolicy::Ignore,
)
@@ -1095,6 +1113,7 @@ impl ProductAuthority for SigningHost {
let secret = sso_responder::allocate_bulletin_allowance(
&self.services,
self,
+ session,
&product_id,
OnExistingAllowancePolicy::Increase,
)
@@ -1161,7 +1180,7 @@ fn sign_extrinsic_payload(
.sign_simple(SR25519_SIGNING_CONTEXT, &preimage, &keypair.public)
.to_bytes();
let signature = MultiSignature::Sr25519(raw_signature);
- let signed_transaction = payload.with_signed_transaction.unwrap_or(false).then(|| {
+ let signed_transaction = payload.with_signed_transaction.0.unwrap_or(false).then(|| {
let extensions = extrinsic_payload_extensions(&payload)
.expect("preimage construction already validated signed extensions");
build_signed_extrinsic_v4_with_signature(
@@ -1287,9 +1306,7 @@ mod tests {
use std::sync::Arc;
use super::super::authority::{
- AccountAliasAuthorityRequest, AuthorityError, AuthoritySession,
- CreateProofAuthorityRequest, CreateTransactionAuthorityRequest,
- RegisterRingVrfKeyAuthorityRequest, RingVrfSignAuthorityRequest,
+ AuthorityError, AuthoritySession, CreateTransactionAuthorityRequest,
SignPayloadAuthorityRequest, SignRawAuthorityRequest,
};
use super::super::{ProductAuthority, ProductRuntimeHost, RuntimeServices, SigningHostRole};
@@ -1304,12 +1321,17 @@ mod tests {
derive_identity_keypair, derive_product_keypair, derive_ring_vrf_entropy,
derive_root_keypair_from_entropy, index_bytes,
};
+ use crate::host_logic::sso::messages::ProductRequest;
use crate::host_logic::transaction::{
extrinsic_payload_extensions, extrinsic_payload_preimage,
};
use crate::runtime::statement_allowance::collection::PersonhoodCollection;
use crate::test_support::{StubPlatform, test_spawner};
use truapi::api::{Account, Entropy, ResourceAllocation, Signing};
+ use truapi::latest::{
+ HostAccountCreateProofRequest, HostAccountGetAliasRequest,
+ HostAccountRegisterRingVrfKeyRequest, HostAccountRingVrfSignRequest,
+ };
use truapi::versioned::account::{HostAccountGetError, HostAccountGetRequest};
use truapi::versioned::entropy::HostDeriveEntropyRequest;
use truapi::versioned::resource_allocation::{
@@ -1470,10 +1492,12 @@ mod tests {
futures::executor::block_on(authority.register_ring_vrf_key(
&CallContext::default(),
session,
- RegisterRingVrfKeyAuthorityRequest {
+ ProductRequest {
calling_product_id: "peopl.dot".to_string(),
- index: v01::DerivationIndex::Index(0),
- ring: ring.clone(),
+ payload: HostAccountRegisterRingVrfKeyRequest {
+ index: v01::DerivationIndex::Index(0),
+ ring: ring.clone(),
+ },
},
))
.expect("full person key registration succeeds");
@@ -1580,23 +1604,27 @@ mod tests {
let alias = futures::executor::block_on(authority.account_alias(
&cx,
&session,
- AccountAliasAuthorityRequest {
+ ProductRequest {
calling_product_id: "peopl.dot".to_string(),
- key_handle: full_person_key_handle(),
- context: context.clone(),
- ring_location: ring_location.clone(),
+ payload: HostAccountGetAliasRequest {
+ key_handle: full_person_key_handle(),
+ context: context.clone(),
+ ring_location: ring_location.clone(),
+ },
},
))
.expect("alias succeeds");
let proof = futures::executor::block_on(authority.create_proof(
&cx,
&session,
- CreateProofAuthorityRequest {
+ ProductRequest {
calling_product_id: "peopl.dot".to_string(),
- key_handle: full_person_key_handle(),
- context,
- ring_location,
- message: b"prove me".to_vec(),
+ payload: HostAccountCreateProofRequest {
+ key_handle: full_person_key_handle(),
+ context,
+ ring_location,
+ message: b"prove me".to_vec(),
+ },
},
))
.expect("proof succeeds");
@@ -1621,16 +1649,18 @@ mod tests {
let error = futures::executor::block_on(authority.account_alias(
&CallContext::default(),
&session,
- AccountAliasAuthorityRequest {
+ ProductRequest {
calling_product_id: "peopl.dot".to_string(),
- key_handle: full_person_key_handle(),
- context: v01::ProductProofContext {
- product_id: "myapp.dot".to_string(),
- suffix: v01::DerivationIndex::Index(0),
- },
- ring_location: v01::RingLocation {
- chain_id: registered_ring.chain_id,
- junctions: vec![],
+ payload: HostAccountGetAliasRequest {
+ key_handle: full_person_key_handle(),
+ context: v01::ProductProofContext {
+ product_id: "myapp.dot".to_string(),
+ suffix: v01::DerivationIndex::Index(0),
+ },
+ ring_location: v01::RingLocation {
+ chain_id: registered_ring.chain_id,
+ junctions: vec![],
+ },
},
},
))
@@ -1662,10 +1692,12 @@ mod tests {
let error = futures::executor::block_on(authority.ring_vrf_sign(
&CallContext::default(),
&session,
- RingVrfSignAuthorityRequest {
+ ProductRequest {
calling_product_id: "myapp.dot".to_string(),
- key_handle: handle,
- message: b"reject mismatched registry state".to_vec(),
+ payload: HostAccountRingVrfSignRequest {
+ key_handle: handle,
+ message: b"reject mismatched registry state".to_vec(),
+ },
},
))
.unwrap_err();
@@ -1695,11 +1727,13 @@ mod tests {
let alias = futures::executor::block_on(authority.account_alias(
&cx,
&session,
- AccountAliasAuthorityRequest {
+ ProductRequest {
calling_product_id: "myapp.dot".to_string(),
- key_handle: full_person_key_handle(),
- context: context.clone(),
- ring_location: ring_location.clone(),
+ payload: HostAccountGetAliasRequest {
+ key_handle: full_person_key_handle(),
+ context: context.clone(),
+ ring_location: ring_location.clone(),
+ },
},
));
assert_eq!(alias, Err(RingVrfError::Rejected));
@@ -1707,12 +1741,14 @@ mod tests {
let proof = futures::executor::block_on(authority.create_proof(
&cx,
&session,
- CreateProofAuthorityRequest {
+ ProductRequest {
calling_product_id: "myapp.dot".to_string(),
- key_handle: full_person_key_handle(),
- context,
- ring_location,
- message: b"prove me".to_vec(),
+ payload: HostAccountCreateProofRequest {
+ key_handle: full_person_key_handle(),
+ context,
+ ring_location,
+ message: b"prove me".to_vec(),
+ },
},
));
assert_eq!(proof, Err(RingVrfError::NotAllowlisted));
@@ -1738,16 +1774,18 @@ mod tests {
.expect("activation succeeds");
let session = authority.current_session().expect("active session");
let cx = CallContext::default();
- let request = AccountAliasAuthorityRequest {
+ let request = ProductRequest {
calling_product_id: "myapp.dot".to_string(),
- key_handle: full_person_key_handle(),
- context: v01::ProductProofContext {
- product_id: "other.dot".to_string(),
- suffix: v01::DerivationIndex::Index(0),
+ payload: HostAccountGetAliasRequest {
+ key_handle: full_person_key_handle(),
+ context: v01::ProductProofContext {
+ product_id: "other.dot".to_string(),
+ suffix: v01::DerivationIndex::Index(0),
+ },
+ ring_location: full_person_ring_location(),
},
- ring_location: full_person_ring_location(),
};
- register_full_person_key(&authority, &session, &request.ring_location);
+ register_full_person_key(&authority, &session, &request.payload.ring_location);
futures::executor::block_on(authority.account_alias(&cx, &session, request.clone()))
.expect("first alias succeeds");
@@ -2143,7 +2181,7 @@ mod tests {
"CheckNonce".to_string(),
"ChargeTransactionPayment".to_string(),
];
- payload.with_signed_transaction = Some(true);
+ payload.with_signed_transaction = parity_scale_codec::OptionBool(Some(true));
let preimage = extrinsic_payload_preimage(&payload).expect("preimage builds");
let product_response = futures::executor::block_on(authority.sign_payload(
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 393232f7a..d72167d0a 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
@@ -17,49 +17,38 @@ use std::time::Instant;
use parity_scale_codec::Encode;
use tracing::{debug, instrument, warn};
-use truapi::{CallContext, latest as api, v01};
-use truapi_platform::{
- CreateTransactionReview, ResourceAllocationReview, SignPayloadReview, SignRawReview,
- UserConfirmationReview,
-};
+use truapi::v01;
-use super::SigningHost;
use super::sso_replay::{ReplayExecution, SsoReplayScope, execute_once};
+use super::{SigningHost, SigningHostSsoService};
#[cfg(not(target_arch = "wasm32"))]
use crate::chain_runtime::RuntimeFailure;
use crate::host_logic::entropy::root_entropy_source;
#[cfg(not(target_arch = "wasm32"))]
use crate::host_logic::product_account::derive_sr25519_hard_path;
use crate::host_logic::product_account::{
- ProductAccountError, derive_identity_keypair, derive_ring_vrf_domain_entropy,
- derive_root_keypair_from_entropy, product_public_key_to_address,
+ ProductAccountError, derive_identity_keypair, derive_root_keypair_from_entropy,
};
use crate::host_logic::session::SsoSessionInfo;
use crate::host_logic::sso::messages::{
- self, CreateTransactionPayload, IncomingSsoRequest, OnExistingAllowancePolicy, RemoteMessage,
- RemoteMessageData, ResourceAllocationResponse, RingVrfAliasResponse, RingVrfError,
- RingVrfProofResponse, RingVrfSignResponse, SignRawLegacyResponse, SignVrfResponse,
- SigningPayloadResponseData, SigningRequest, SigningResponse, SsoAllocatableResource,
- SsoAllocatedResource, SsoAllocationOutcome, SsoResponseCode, build_outgoing_request_statement,
- build_signed_session_response_statement, decode_incoming_sso_request, v1,
+ IncomingSsoRequest, OnExistingAllowancePolicy, RemoteMessageData, SsoResponseCode,
+ build_outgoing_request_statement, build_signed_session_response_statement,
+ decode_incoming_sso_request, v1,
};
use crate::host_logic::sso::pairing::{
ResponderIdentity, VersionedHandshakeProposal, bootstrap_topic, decode_pairing_deeplink,
derive_identity_chat_private_key, derive_x25519_keypair_from_entropy,
encrypt_v2_handshake_response, establish_responder_session_info, v2, x25519_public_key,
};
+use crate::host_logic::sso::wire::ResponseOutcome;
use crate::host_logic::statement_store::{
build_signed_statement, current_unix_secs as statement_current_unix_secs,
parse_new_statements_result,
};
-use crate::runtime::authority::{
- AccountAliasAuthorityRequest, AuthorityError, CreateProofAuthorityRequest,
- CreateTransactionAuthorityRequest, ListRingVrfKeysAuthorityRequest, ProductAuthority,
- RegisterRingVrfKeyAuthorityRequest, RingVrfSignAuthorityRequest, SignPayloadAuthorityRequest,
- SignRawAuthorityRequest,
-};
+use crate::runtime::authority::{AuthorityError, AuthoritySession};
use crate::runtime::services::RuntimeServices;
use crate::runtime::sso_remote::fresh_statement_expiry;
+use crate::runtime::sso_service::Dispatch;
#[cfg(not(target_arch = "wasm32"))]
use crate::runtime::statement_allowance::StatementAllowanceError;
use crate::runtime::statement_store_rpc;
@@ -258,8 +247,8 @@ pub(crate) async fn establish_pairing(
}
async fn establish_pairing_session(
- services: &Arc,
- signing_host: &Arc,
+ services: &RuntimeServices,
+ signing_host: &SigningHost,
deeplink: &str,
) -> Result {
let peer = PairedSsoPeer::from_deeplink(deeplink)?;
@@ -364,6 +353,7 @@ async fn serve_session(
session: SsoSessionInfo,
replay_scope: SsoReplayScope,
) -> Result {
+ let service = SigningHostSsoService::new(signing_host.clone());
let rpc_client = services
.statement_store
.client("sso-responder session")
@@ -414,14 +404,16 @@ async fn serve_session(
for message in &incoming.messages {
let cli_summary = format!(
"Incoming SSO request · {}\nstatement_request_id={}\nremote_message_id={}",
- message, incoming.request_id, message.message_id
+ message.name(),
+ incoming.request_id,
+ message.message_id
);
tracing::event!(
target: "truapi_server::sso_transcript",
tracing::Level::DEBUG,
cli_summary = cli_summary.as_str(),
cli_event = "request_received",
- request = %message,
+ request = message.name(),
statement_request_id = %incoming.request_id,
remote_message_id = %message.message_id,
);
@@ -436,7 +428,7 @@ async fn serve_session(
&request_id,
expires_at_unix_secs,
statement_current_unix_secs(),
- || serve_request(&services, &signing_host, &session, incoming),
+ || serve_request(&services, &service, &session, incoming),
)
.await?;
let exit = match execution {
@@ -456,33 +448,30 @@ async fn serve_session(
/// Ack one inbound request statement and answer its batched messages.
async fn serve_request(
- services: &Arc,
- signing_host: &Arc,
+ services: &RuntimeServices,
+ service: &SigningHostSsoService,
session: &SsoSessionInfo,
incoming: IncomingSsoRequest,
) -> Result, String> {
acknowledge_request(services, session, &incoming.request_id).await?;
for message in incoming.messages {
- let RemoteMessageData::V1(request) = message.data;
- if matches!(request, v1::RemoteMessage::Disconnected) {
- debug!("pairing host disconnected the SSO session");
- return Ok(Some(ResponderExit::PeerDisconnected));
- }
- let request_name = request.to_string();
+ let request_name = message.name();
let responding_to = message.message_id.clone();
let started = Instant::now();
- let Some(answer) =
- answer_remote_message(services, signing_host, message.message_id, request).await
- else {
- continue;
+ let (response, outcome) = match service.dispatch(service.current_session(), message).await {
+ Dispatch::Response(answer) => (answer.message, answer.outcome),
+ Dispatch::Disconnected => {
+ debug!("pairing host disconnected the SSO session");
+ return Ok(Some(ResponderExit::PeerDisconnected));
+ }
+ Dispatch::NotARequest(name) => {
+ warn!(name, "peer sent a response variant as a request");
+ continue;
+ }
};
- let response = answer.response;
let response_message_id = response.message_id.clone();
- let response_result = answer
- .response_result
- .unwrap_or_else(|| remote_response_result(&response.data));
- let statement_request_id = format!("resp:{}", response.message_id);
+ let statement_request_id = format!("resp:{response_message_id}");
let statement = build_outgoing_request_statement(
session,
statement_request_id,
@@ -498,11 +487,11 @@ async fn serve_request(
Ok(()) => {
let cli_summary = response_cli_summary(
"SSO response sent",
- &request_name,
+ request_name,
&incoming.request_id,
&responding_to,
&response_message_id,
- &response_result,
+ &outcome,
elapsed_ms,
);
tracing::event!(
@@ -510,23 +499,23 @@ async fn serve_request(
tracing::Level::DEBUG,
cli_summary = cli_summary.as_str(),
cli_event = "response_sent",
- request = request_name.as_str(),
+ request = request_name,
statement_request_id = %incoming.request_id,
responding_to = %responding_to,
%response_message_id,
- outcome = response_result.outcome,
- reason = response_result.reason.as_deref().unwrap_or_default(),
+ outcome = outcome.outcome,
+ reason = outcome.reason.as_deref().unwrap_or_default(),
elapsed_ms = elapsed_ms as u64,
);
}
Err(reason) => {
- let failure = ResponseResult {
+ let failure = ResponseOutcome {
outcome: "publish_failed",
reason: Some(reason.clone()),
};
let cli_summary = response_cli_summary(
"SSO response failed",
- &request_name,
+ request_name,
&incoming.request_id,
&responding_to,
&response_message_id,
@@ -538,7 +527,7 @@ async fn serve_request(
tracing::Level::WARN,
cli_summary = cli_summary.as_str(),
cli_event = "response_failed",
- request = request_name.as_str(),
+ request = request_name,
statement_request_id = %incoming.request_id,
responding_to = %responding_to,
%response_message_id,
@@ -554,7 +543,7 @@ async fn serve_request(
}
async fn acknowledge_request(
- services: &Arc,
+ services: &RuntimeServices,
session: &SsoSessionInfo,
request_id: &str,
) -> Result<(), String> {
@@ -583,185 +572,13 @@ fn duplicate_request_exit(incoming: &IncomingSsoRequest) -> Option,
-}
-
-/// Result of answering one remote message: the response envelope and an
-/// optional pre-classified outcome for logging.
-pub(crate) struct AnsweredRemoteMessage {
- /// Response to post back over the session transport.
- pub(crate) response: RemoteMessage,
- /// Pre-classified outcome summary for SSO transcript logging (outcome code and error reason).
- response_result: Option,
-}
-
-struct ResourceAllocationAnswer {
- payload: Result, String>,
- item_failures: Vec,
-}
-
-fn remote_response_result(message: &RemoteMessageData) -> ResponseResult {
- let RemoteMessageData::V1(message) = message;
- let error = match message {
- v1::RemoteMessage::SignResponse(response) => response.payload.as_ref().err().cloned(),
- v1::RemoteMessage::RingVrfAliasResponse(response) => {
- response.payload.as_ref().err().map(ring_vrf_error_reason)
- }
- v1::RemoteMessage::RingVrfProofResponse(response) => {
- response.payload.as_ref().err().map(ring_vrf_error_reason)
- }
- v1::RemoteMessage::RegisterRingVrfKeyResponse(response) => {
- response.payload.as_ref().err().map(ring_vrf_error_reason)
- }
- v1::RemoteMessage::ListRingVrfKeysResponse(response) => {
- response.payload.as_ref().err().map(ring_vrf_error_reason)
- }
- v1::RemoteMessage::RingVrfSignResponse(response) => {
- response.payload.as_ref().err().map(ring_vrf_error_reason)
- }
- v1::RemoteMessage::ResourceAllocationResponse(response) => {
- return resource_allocation_payload_result(&response.payload, &[]);
- }
- v1::RemoteMessage::CreateTransactionResponse(response) => {
- response.signed_transaction.as_ref().err().cloned()
- }
- v1::RemoteMessage::SignRawLegacyResponse(response) => {
- response.signature.as_ref().err().cloned()
- }
- v1::RemoteMessage::SignVrfResponse(response) => {
- response.payload.as_ref().err().map(sign_vrf_error_reason)
- }
- _ => None,
- };
- ResponseResult {
- outcome: if error.is_some() { "error" } else { "ok" },
- reason: error,
- }
-}
-
-fn resource_allocation_payload_result(
- payload: &Result, String>,
- item_failures: &[String],
-) -> ResponseResult {
- let outcomes = match payload {
- Ok(outcomes) => outcomes,
- Err(reason) => {
- return ResponseResult {
- outcome: "error",
- reason: Some(reason.clone()),
- };
- }
- };
- if outcomes.is_empty() {
- return ResponseResult {
- outcome: "ok",
- reason: None,
- };
- }
-
- let allocated = outcomes
- .iter()
- .filter(|outcome| matches!(outcome, SsoAllocationOutcome::Allocated(_)))
- .count();
- let rejected = outcomes
- .iter()
- .filter(|outcome| matches!(outcome, SsoAllocationOutcome::Rejected))
- .count();
- let unavailable = outcomes
- .iter()
- .filter(|outcome| matches!(outcome, SsoAllocationOutcome::NotAvailable))
- .count();
- let total = outcomes.len();
-
- if allocated == total {
- return ResponseResult {
- outcome: "ok",
- reason: None,
- };
- }
- if allocated > 0 {
- let mut reason = format!("{allocated} of {total} requested resources allocated");
- if rejected > 0 {
- reason.push_str(&format!("; {rejected} rejected"));
- }
- if unavailable > 0 {
- reason.push_str(&format!("; {unavailable} unavailable"));
- }
- return allocation_result_with_failures(
- ResponseResult {
- outcome: "partial",
- reason: Some(reason),
- },
- item_failures,
- );
- }
- if rejected > 0 {
- let reason = if rejected == total {
- if total == 1 {
- "Requested resource was rejected".to_string()
- } else {
- format!("All {total} requested resources were rejected")
- }
- } else {
- format!("No resources allocated; {rejected} rejected; {unavailable} unavailable")
- };
- return allocation_result_with_failures(
- ResponseResult {
- outcome: "rejected",
- reason: Some(reason),
- },
- item_failures,
- );
- }
-
- allocation_result_with_failures(
- ResponseResult {
- outcome: "not_available",
- reason: Some(if total == 1 {
- "Requested resource is not available".to_string()
- } else {
- format!("None of the {total} requested resources are available")
- }),
- },
- item_failures,
- )
-}
-
-fn allocation_result_with_failures(
- mut result: ResponseResult,
- item_failures: &[String],
-) -> ResponseResult {
- if !item_failures.is_empty() {
- let details = item_failures.join("; ").replace(['\r', '\n'], " ");
- result.reason = Some(match result.reason {
- Some(summary) => format!("{summary}: {details}"),
- None => details,
- });
- }
- result
-}
-
-fn ring_vrf_error_reason(error: &RingVrfError) -> String {
- match error {
- RingVrfError::RingNotFound => "RingNotFound".to_string(),
- RingVrfError::NotMember => "NotMember".to_string(),
- RingVrfError::KeyNotRegistered => "KeyNotRegistered".to_string(),
- RingVrfError::KeyNotInRing => "KeyNotInRing".to_string(),
- RingVrfError::NotAllowlisted => "NotAllowlisted".to_string(),
- RingVrfError::Rejected => "Rejected".to_string(),
- RingVrfError::Unknown { reason } => format!("Unknown: {reason}"),
- }
-}
-
fn response_cli_summary(
heading: &str,
request_name: &str,
statement_request_id: &str,
responding_to: &str,
response_message_id: &str,
- result: &ResponseResult,
+ result: &ResponseOutcome,
elapsed_ms: u128,
) -> String {
let mut summary = format!(
@@ -775,273 +592,11 @@ fn response_cli_summary(
summary
}
-/// Answer one application-level request message; `None` for message kinds
-/// that take no response (responses echoed by the peer, unknown variants).
-pub(crate) async fn answer_remote_message(
- services: &Arc,
- signing_host: &Arc,
- message_id: String,
- request: v1::RemoteMessage,
-) -> Option {
- let response_id = format!("{message_id}:response");
- let mut response_result = None;
- let data = match request {
- v1::RemoteMessage::SignRequest(request) => v1::RemoteMessage::SignResponse(
- sign_response(services, signing_host, &message_id, *request).await,
- ),
- v1::RemoteMessage::RingVrfAliasRequest(request) => {
- let payload = account_alias_response(signing_host, request).await;
- v1::RemoteMessage::RingVrfAliasResponse(RingVrfAliasResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::RingVrfProofRequest(request) => {
- let payload = create_proof_response(signing_host, request).await;
- v1::RemoteMessage::RingVrfProofResponse(RingVrfProofResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::RegisterRingVrfKeyRequest(request) => {
- let payload = register_ring_vrf_key_response(signing_host, request).await;
- v1::RemoteMessage::RegisterRingVrfKeyResponse(messages::RegisterRingVrfKeyResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::ListRingVrfKeysRequest(request) => {
- let payload = list_ring_vrf_keys_response(signing_host, request).await;
- v1::RemoteMessage::ListRingVrfKeysResponse(messages::ListRingVrfKeysResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::RingVrfSignRequest(request) => {
- let payload = ring_vrf_sign_response(signing_host, request).await;
- v1::RemoteMessage::RingVrfSignResponse(RingVrfSignResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::ResourceAllocationRequest(request) => {
- let answer = resource_allocation_response(services, signing_host, request).await;
- if let Err(reason) = &answer.payload {
- warn!(%reason, "resource allocation request failed");
- }
- response_result = Some(resource_allocation_payload_result(
- &answer.payload,
- &answer.item_failures,
- ));
- v1::RemoteMessage::ResourceAllocationResponse(ResourceAllocationResponse {
- responding_to: message_id,
- payload: answer.payload,
- })
- }
- v1::RemoteMessage::CreateTransactionRequest(request) => {
- let CreateTransactionPayload::V1(payload) = request.payload;
- let signed_transaction = create_transaction_response(
- services,
- signing_host,
- CreateTransactionReview::Product(payload.clone()),
- CreateTransactionAuthorityRequest::Product(payload),
- )
- .await;
- v1::RemoteMessage::CreateTransactionResponse(messages::CreateTransactionResponse {
- responding_to: message_id,
- signed_transaction,
- })
- }
- v1::RemoteMessage::CreateTransactionLegacyRequest(request) => {
- let messages::CreateTransactionLegacyPayload::V1(payload) = request.payload;
- let signed_transaction = create_transaction_response(
- services,
- signing_host,
- CreateTransactionReview::LegacyAccount(payload.clone()),
- CreateTransactionAuthorityRequest::IdentityAccount(payload),
- )
- .await;
- v1::RemoteMessage::CreateTransactionResponse(messages::CreateTransactionResponse {
- responding_to: message_id,
- signed_transaction,
- })
- }
- v1::RemoteMessage::SignRawLegacyRequest(request) => {
- let signature = sign_raw_legacy_response(services, signing_host, request).await;
- v1::RemoteMessage::SignRawLegacyResponse(SignRawLegacyResponse {
- responding_to: message_id,
- signature,
- })
- }
- v1::RemoteMessage::SignVrfRequest(request) => {
- let payload = sign_vrf_response(signing_host, message_id.clone(), request).await;
- v1::RemoteMessage::SignVrfResponse(SignVrfResponse {
- responding_to: message_id,
- payload,
- })
- }
- v1::RemoteMessage::ProductSubtreeRequest(request) => {
- let product_public_key = match signing_host.current_session() {
- Some(session) => signing_host
- .product_subtree_public_key(
- &CallContext::with_request_id(message_id.clone()),
- &session,
- request.product_id,
- )
- .await
- .map_err(|err| err.to_string()),
- None => Err("signing host is disconnected".to_string()),
- };
- v1::RemoteMessage::ProductSubtreeResponse(messages::ProductSubtreeResponse {
- responding_to: message_id,
- product_public_key,
- })
- }
- v1::RemoteMessage::Disconnected
- | v1::RemoteMessage::SignResponse(_)
- | v1::RemoteMessage::RingVrfAliasResponse(_)
- | v1::RemoteMessage::RingVrfProofResponse(_)
- | v1::RemoteMessage::RegisterRingVrfKeyResponse(_)
- | v1::RemoteMessage::ListRingVrfKeysResponse(_)
- | v1::RemoteMessage::RingVrfSignResponse(_)
- | v1::RemoteMessage::ResourceAllocationResponse(_)
- | v1::RemoteMessage::CreateTransactionResponse(_)
- | v1::RemoteMessage::SignRawLegacyResponse(_)
- | v1::RemoteMessage::ProductSubtreeResponse(_)
- | v1::RemoteMessage::SignVrfResponse(_) => return None,
- };
- Some(AnsweredRemoteMessage {
- response: RemoteMessage {
- message_id: response_id,
- data: RemoteMessageData::V1(data),
- },
- response_result,
- })
-}
-
-async fn resource_allocation_response(
- services: &Arc,
- signing_host: &Arc,
- request: messages::ResourceAllocationRequest,
-) -> ResourceAllocationAnswer {
- let review = UserConfirmationReview::ResourceAllocation(ResourceAllocationReview {
- calling_product_id: request.calling_product_id.clone(),
- resources: request
- .resources
- .iter()
- .map(public_allocatable_resource)
- .collect(),
- });
- match services.platform.confirm_user_action(review).await {
- Ok(true) => {}
- Ok(false) => {
- return ResourceAllocationAnswer {
- payload: Ok(vec![
- SsoAllocationOutcome::Rejected;
- request.resources.len()
- ]),
- item_failures: Vec::new(),
- };
- }
- Err(err) => {
- return ResourceAllocationAnswer {
- payload: Err(format!("confirmation failed: {}", err.reason)),
- item_failures: Vec::new(),
- };
- }
- }
-
- 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)
- })
- }
- 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,
- },
- ))
- })(),
- };
- match outcome {
- Ok(outcome) => outcomes.push(outcome),
- Err(err) => {
- let reason = err.to_string();
- warn!(%reason, "resource allocation item failed");
- item_failures.push(reason);
- outcomes.push(SsoAllocationOutcome::NotAvailable);
- }
- }
- }
- ResourceAllocationAnswer {
- payload: Ok(outcomes),
- item_failures,
- }
-}
-
-fn public_allocatable_resource(resource: &SsoAllocatableResource) -> api::AllocatableResource {
- match resource {
- SsoAllocatableResource::StatementStoreAllowance => {
- api::AllocatableResource::StatementStoreAllowance
- }
- SsoAllocatableResource::BulletinAllowance => api::AllocatableResource::BulletinAllowance,
- SsoAllocatableResource::SmartContractAllowance(index) => {
- api::AllocatableResource::SmartContractAllowance(index.clone())
- }
- SsoAllocatableResource::AutoSigning => api::AllocatableResource::AutoSigning,
- }
-}
-
#[cfg(not(target_arch = "wasm32"))]
pub(super) async fn allocate_statement_store_allowance(
- services: &Arc,
+ services: &RuntimeServices,
signing_host: &SigningHost,
+ session: &AuthoritySession,
product_id: &str,
policy: OnExistingAllowancePolicy,
) -> Result, AllowanceAllocationError> {
@@ -1051,14 +606,12 @@ pub(super) async fn allocate_statement_store_allowance(
register_statement_account_pooled, scan_collections,
};
+ signing_host.require_current_session(session)?;
let entropy = signing_host.root_entropy()?;
let allowance =
derive_sr25519_hard_path(&entropy, &["allowance", "statement-store", product_id])?;
let target = allowance.public.to_bytes();
- let session = signing_host
- .current_session()
- .ok_or(AuthorityError::Disconnected)?;
- let candidates = signing_host.reserved_person_collection_candidates(&session)?;
+ let candidates = signing_host.reserved_person_collection_candidates(session)?;
let client = services
.statement_store
.chain_client("statement-store allowance")
@@ -1096,6 +649,7 @@ pub(super) async fn allocate_statement_store_allowance(
%collection,
"statement-store allowance already allocated"
);
+ signing_host.require_current_session(session)?;
return Ok(allowance.secret.to_bytes().to_vec());
}
@@ -1107,6 +661,7 @@ pub(super) async fn allocate_statement_store_allowance(
resource: "statement-store",
});
}
+ signing_host.require_current_session(session)?;
let outcome = register_statement_account_pooled(
rpc,
&chain.metadata,
@@ -1151,6 +706,7 @@ pub(super) async fn allocate_statement_store_allowance(
);
}
}
+ signing_host.require_current_session(session)?;
if let Err(reason) = allowance_renewal::track(
signing_host,
vec![StatementRenewalTarget::ProductStatementAllowance {
@@ -1161,13 +717,15 @@ pub(super) async fn allocate_statement_store_allowance(
{
warn!(%product_id, %reason, "failed to record statement-store renewal target");
}
+ signing_host.require_current_session(session)?;
Ok(allowance.secret.to_bytes().to_vec())
}
#[cfg(not(target_arch = "wasm32"))]
pub(super) async fn allocate_bulletin_allowance(
- services: &Arc,
+ services: &RuntimeServices,
signing_host: &SigningHost,
+ session: &AuthoritySession,
product_id: &str,
policy: OnExistingAllowancePolicy,
) -> Result, AllowanceAllocationError> {
@@ -1177,6 +735,7 @@ pub(super) async fn allocate_bulletin_allowance(
wait_bulletin_authorization,
};
+ signing_host.require_current_session(session)?;
let entropy = signing_host.root_entropy()?;
let allowance = derive_sr25519_hard_path(&entropy, &["allowance", "bulletin", product_id])?;
let target = allowance.public.to_bytes();
@@ -1195,6 +754,7 @@ pub(super) async fn allocate_bulletin_allowance(
if matches!(policy, OnExistingAllowancePolicy::Ignore)
&& current_allowance.is_some_and(|allowance| allowance.available())
{
+ signing_host.require_current_session(session)?;
return Ok(allowance.secret.to_bytes().to_vec());
}
@@ -1205,10 +765,7 @@ pub(super) async fn allocate_bulletin_allowance(
let people_rpc = people_client.rpc();
let chain = services.chain_context.get(&people_client).await?;
let network_suffix = statement_allowance::slot::read_network_suffix(people_rpc).await?;
- let session = signing_host
- .current_session()
- .ok_or(AuthorityError::Disconnected)?;
- let candidates = signing_host.reserved_person_collection_candidates(&session)?;
+ let candidates = signing_host.reserved_person_collection_candidates(session)?;
// Statement-store slots and PGAS claims are each bounded by a per-collection
// constant, so their budgets are meant to be spent per collection. Long-term
// storage is bounded by `Resources.LongTermStorageClaimsPerPeriod` alone, with
@@ -1232,6 +789,7 @@ pub(super) async fn allocate_bulletin_allowance(
current_unix_secs()?,
period_duration,
)?;
+ signing_host.require_current_session(session)?;
let outcome = claim_long_term_storage(statement_allowance::LongTermStorageClaim {
rpc: people_rpc,
metadata: &chain.metadata,
@@ -1269,13 +827,15 @@ pub(super) async fn allocate_bulletin_allowance(
remained_transactions = authorization.remained_transactions,
"Bulletin authorization visible"
);
+ signing_host.require_current_session(session)?;
Ok(allowance.secret.to_bytes().to_vec())
}
#[cfg(target_arch = "wasm32")]
pub(super) async fn allocate_statement_store_allowance(
- _services: &Arc,
+ _services: &RuntimeServices,
_signing_host: &SigningHost,
+ _session: &AuthoritySession,
_product_id: &str,
_policy: OnExistingAllowancePolicy,
) -> Result, AllowanceAllocationError> {
@@ -1297,8 +857,9 @@ pub(super) async fn allocate_statement_store_allowance(
/// whatever chain a stale hash happens to reach.
#[cfg(not(target_arch = "wasm32"))]
pub(super) async fn allocate_smart_contract_allowance(
- services: &Arc,
+ services: &RuntimeServices,
signing_host: &SigningHost,
+ session: &AuthoritySession,
product_id: &str,
derivation_index: v01::DerivationIndex,
policy: OnExistingAllowancePolicy,
@@ -1308,9 +869,7 @@ pub(super) async fn allocate_smart_contract_allowance(
use crate::host_logic::features;
use crate::runtime::statement_allowance::{self, ChainClient, find_including_rings, pgas};
- let session = signing_host
- .current_session()
- .ok_or(AuthorityError::Disconnected)?;
+ signing_host.require_current_session(session)?;
// PGAS credits the product account the caller named.
let target = signing_host
@@ -1347,6 +906,7 @@ pub(super) async fn allocate_smart_contract_allowance(
&& pgas::holds_a_full_claim(asset_hub_client.rpc(), &asset_hub.metadata, &target).await?
{
debug!(%product_id, "PGAS allowance already funded; leaving it alone");
+ signing_host.require_current_session(session)?;
return Ok(());
}
let network_suffix =
@@ -1359,7 +919,7 @@ pub(super) async fn allocate_smart_contract_allowance(
let people_rpc = people_client.rpc();
let people = services.chain_context.get(&people_client).await?;
- let candidates = signing_host.reserved_person_collection_candidates(&session)?;
+ let candidates = signing_host.reserved_person_collection_candidates(session)?;
// A single claim needs one collection, so take the strongest membership the
// person actually holds rather than assuming light personhood.
let membership = find_including_rings(people_rpc, &people.metadata, &candidates, u32::MAX)
@@ -1368,6 +928,7 @@ pub(super) async fn allocate_smart_contract_allowance(
.next()
.ok_or(AllowanceAllocationError::MissingPersonhoodMembership { resource: "PGAS" })?;
+ signing_host.require_current_session(session)?;
let outcome = pgas::claim_pgas(pgas::PgasClaim {
asset_hub_rpc: asset_hub_client.rpc(),
asset_hub: &asset_hub,
@@ -1387,14 +948,16 @@ pub(super) async fn allocate_smart_contract_allowance(
block = %outcome.block_hash,
"claimed PGAS allowance"
);
+ signing_host.require_current_session(session)?;
Ok(())
}
/// PGAS claims need chain access the wasm host does not have.
#[cfg(target_arch = "wasm32")]
pub(super) async fn allocate_smart_contract_allowance(
- _services: &Arc,
+ _services: &RuntimeServices,
_signing_host: &SigningHost,
+ _session: &AuthoritySession,
_product_id: &str,
_derivation_index: v01::DerivationIndex,
_policy: OnExistingAllowancePolicy,
@@ -1404,8 +967,9 @@ pub(super) async fn allocate_smart_contract_allowance(
#[cfg(target_arch = "wasm32")]
pub(super) async fn allocate_bulletin_allowance(
- _services: &Arc,
+ _services: &RuntimeServices,
_signing_host: &SigningHost,
+ _session: &AuthoritySession,
_product_id: &str,
_policy: OnExistingAllowancePolicy,
) -> Result, AllowanceAllocationError> {
@@ -1422,284 +986,23 @@ pub(super) fn current_unix_secs() -> Result {
.map_err(|_| AllowanceAllocationError::SystemClockBeforeUnixEpoch)
}
-/// Confirm and serve a payload or raw signing request.
-async fn sign_response(
- services: &Arc,
- signing_host: &Arc,
- message_id: &str,
- request: SigningRequest,
-) -> SigningResponse {
- let payload = serve_sign_request(services, signing_host, request).await;
- if let Err(reason) = &payload {
- warn!(%reason, "sign request failed");
- }
- SigningResponse {
- responding_to: message_id.to_string(),
- payload,
- }
-}
-
-async fn serve_sign_request(
- services: &Arc,
- signing_host: &Arc,
- request: SigningRequest,
-) -> Result {
- let session = signing_host
- .current_session()
- .ok_or_else(|| "signing host session is not active".to_string())?;
- let cx = CallContext::default();
- let response = match request {
- SigningRequest::Payload(request) => {
- let request: api::HostSignPayloadRequest = (*request).into();
- confirm(
- services,
- UserConfirmationReview::SignPayload(SignPayloadReview::Product(request.clone())),
- )
- .await?;
- signing_host
- .sign_payload(&cx, &session, SignPayloadAuthorityRequest::Product(request))
- .await
- }
- SigningRequest::Raw(request) => {
- let request: api::HostSignRawRequest = request.into();
- confirm(
- services,
- UserConfirmationReview::SignRaw(SignRawReview::Product(request.clone())),
- )
- .await?;
- signing_host
- .sign_raw(&cx, &session, SignRawAuthorityRequest::Product(request))
- .await
- }
- }
- .map_err(|err| err.to_string())?;
- Ok(SigningPayloadResponseData {
- signature: response.signature,
- signed_transaction: response.signed_transaction,
- })
-}
-
-async fn sign_raw_legacy_response(
- services: &Arc,
- signing_host: &Arc,
- request: messages::SignRawLegacyRequest,
-) -> Result, String> {
- let public_request = api::HostSignRawWithLegacyAccountRequest {
- signer: product_public_key_to_address(request.account),
- payload: request.data.into(),
- };
- confirm(
- services,
- UserConfirmationReview::SignRaw(SignRawReview::LegacyAccount(public_request.clone())),
- )
- .await?;
- let session = signing_host
- .current_session()
- .ok_or_else(|| "signing host session is not active".to_string())?;
- signing_host
- .sign_raw(
- &CallContext::default(),
- &session,
- SignRawAuthorityRequest::LegacyAccount {
- account: request.account,
- request: public_request,
- },
- )
- .await
- .map(|response| response.signature)
- .map_err(|err| err.to_string())
-}
-
-fn sign_vrf_error_reason(error: &v01::HostAccountSignVrfError) -> String {
- match error {
- v01::HostAccountSignVrfError::NotConnected => "NotConnected".to_string(),
- v01::HostAccountSignVrfError::Rejected => "Rejected".to_string(),
- v01::HostAccountSignVrfError::Unknown { reason } => reason.clone(),
- }
-}
-
-async fn sign_vrf_response(
- signing_host: &Arc,
- message_id: String,
- request: messages::SignVrfRequest,
-) -> Result {
- let session = signing_host
- .current_session()
- .ok_or(v01::HostAccountSignVrfError::NotConnected)?;
- signing_host
- .sign_vrf(
- &CallContext::with_request_id(message_id),
- &session,
- request.calling_product_id,
- request.payload,
- )
- .await
- .map_err(|err| match err {
- AuthorityError::Disconnected => v01::HostAccountSignVrfError::NotConnected,
- AuthorityError::Rejected => v01::HostAccountSignVrfError::Rejected,
- AuthorityError::Cancelled(err) => v01::HostAccountSignVrfError::Unknown {
- reason: err.to_string(),
- },
- AuthorityError::Unavailable { reason }
- | AuthorityError::NotSupported { reason }
- | AuthorityError::Unknown { reason } => {
- v01::HostAccountSignVrfError::Unknown { reason }
- }
- })
-}
-
-/// Confirm and serve a transaction-creation request.
-async fn create_transaction_response(
- services: &Arc,
- signing_host: &Arc,
- review: CreateTransactionReview,
- request: CreateTransactionAuthorityRequest,
-) -> Result, String> {
- let session = signing_host
- .current_session()
- .ok_or_else(|| "signing host session is not active".to_string())?;
- confirm(services, UserConfirmationReview::CreateTransaction(review)).await?;
- let cx = CallContext::default();
- signing_host
- .create_transaction(&cx, &session, request)
- .await
- .map(|response| response.transaction)
- .map_err(|err| err.to_string())
-}
-
-async fn account_alias_response(
- signing_host: &Arc,
- request: messages::RingVrfAliasRequest,
-) -> Result {
- let session = signing_host
- .current_session()
- .ok_or_else(disconnected_ring_vrf)?;
- let cx = CallContext::default();
- signing_host
- .account_alias(
- &cx,
- &session,
- AccountAliasAuthorityRequest {
- calling_product_id: request.calling_product_id,
- key_handle: request.key_handle,
- context: request.context,
- ring_location: request.ring_location,
- },
- )
- .await
-}
-
-async fn create_proof_response(
- signing_host: &Arc,
- request: messages::RingVrfProofRequest,
-) -> Result {
- let session = signing_host
- .current_session()
- .ok_or_else(disconnected_ring_vrf)?;
- let cx = CallContext::default();
- signing_host
- .create_proof(
- &cx,
- &session,
- CreateProofAuthorityRequest {
- calling_product_id: request.calling_product_id,
- key_handle: request.key_handle,
- context: request.context,
- ring_location: request.ring_location,
- message: request.message,
- },
- )
- .await
-}
-
-async fn register_ring_vrf_key_response(
- signing_host: &Arc,
- request: messages::RegisterRingVrfKeyRequest,
-) -> Result {
- let session = signing_host
- .current_session()
- .ok_or_else(disconnected_ring_vrf)?;
- signing_host
- .register_ring_vrf_key(
- &CallContext::default(),
- &session,
- RegisterRingVrfKeyAuthorityRequest {
- calling_product_id: request.calling_product_id,
- index: request.index,
- ring: request.ring,
- },
- )
- .await
-}
-
-async fn list_ring_vrf_keys_response(
- signing_host: &Arc,
- request: messages::ListRingVrfKeysRequest,
-) -> Result, RingVrfError> {
- let session = signing_host
- .current_session()
- .ok_or_else(disconnected_ring_vrf)?;
- signing_host
- .list_ring_vrf_keys(
- &CallContext::default(),
- &session,
- ListRingVrfKeysAuthorityRequest {
- calling_product_id: request.calling_product_id,
- owner: request.owner,
- disclosure: request.disclosure,
- },
- )
- .await
-}
-
-async fn ring_vrf_sign_response(
- signing_host: &Arc,
- request: messages::RingVrfSignRequest,
-) -> Result, RingVrfError> {
- let session = signing_host
- .current_session()
- .ok_or_else(disconnected_ring_vrf)?;
- signing_host
- .ring_vrf_sign(
- &CallContext::default(),
- &session,
- RingVrfSignAuthorityRequest {
- calling_product_id: request.calling_product_id,
- key_handle: request.key_handle,
- message: request.message,
- },
- )
- .await
-}
-
-fn disconnected_ring_vrf() -> RingVrfError {
- RingVrfError::Unknown {
- reason: "signing host session is not active".to_string(),
- }
-}
-
-/// Run the platform confirmation seam; rejection and failure both refuse the
-/// operation with an opaque reason (host-spec B.7).
-async fn confirm(
- services: &Arc,
- review: UserConfirmationReview,
-) -> Result<(), String> {
- match services.platform.confirm_user_action(review).await {
- Ok(true) => Ok(()),
- Ok(false) => Err("Rejected".to_string()),
- Err(err) => Err(format!("confirmation failed: {}", err.reason)),
- }
-}
-
#[cfg(test)]
mod tests {
use super::super::LocalActivation;
use super::*;
use crate::host_logic::extrinsic::tests::split_v4;
+ use crate::host_logic::product_account::derive_ring_vrf_domain_entropy;
+ use crate::host_logic::sso::messages::{
+ self, GetAccountAliasResponse, RemoteMessage, RingVrfError, SsoAllocatedResource,
+ SsoAllocationOutcome,
+ };
+ use crate::host_logic::sso::wire::ResponseOutcome;
use crate::host_logic::statement_store::decode_verified_statement_data;
+ use crate::runtime::authority::ProductAuthority;
use crate::runtime::services::RuntimeServices;
use crate::test_support::{StubPlatform, test_spawner};
use std::sync::Arc;
+ use truapi::latest as api;
use truapi_platform::{HostInfo, Platform, PlatformInfo, SigningHostConfig};
const ENTROPY: [u8; 16] = [0xab; 16];
@@ -1806,10 +1109,12 @@ mod tests {
// unbounded test would hang instead of reporting. The bound is generous
// because it is catching a hang, not asserting latency.
let secret = futures::executor::block_on(async {
+ let session = signing_host.current_session().unwrap();
futures::select! {
result = allocate_statement_store_allowance(
&services,
&signing_host,
+ &session,
product_id,
OnExistingAllowancePolicy::Ignore,
)
@@ -1989,55 +1294,78 @@ mod tests {
);
}
- fn response_payload(answer: AnsweredRemoteMessage) -> v1::RemoteMessage {
- let RemoteMessageData::V1(data) = answer.response.data;
+ fn answer(
+ signing_host: &Arc,
+ message_id: &str,
+ request: v1::RemoteMessage,
+ ) -> v1::RemoteMessage {
+ let service = SigningHostSsoService::new(signing_host.clone());
+ let message = RemoteMessage {
+ message_id: message_id.to_string(),
+ data: RemoteMessageData::V1(request),
+ };
+ let Dispatch::Response(answer) =
+ futures::executor::block_on(service.dispatch(service.current_session(), message))
+ else {
+ panic!("expected a response");
+ };
+ let RemoteMessageData::V1(data) = answer.message.data;
data
}
#[test]
- fn account_alias_requires_confirmation_for_cross_product_request() {
- let (services, signing_host) = signing_fixture(Arc::new(StubPlatform::default()));
-
- let response = futures::executor::block_on(answer_remote_message(
- &services,
- &signing_host,
- "alias-1".to_string(),
- v1::RemoteMessage::RingVrfAliasRequest(messages::RingVrfAliasRequest {
- calling_product_id: "myapp.dot".to_string(),
- key_handle: api::ProductAccountId {
- dot_ns_identifier: "peopl.dot".to_string(),
- derivation_index: api::DerivationIndex::Index(0),
- },
- context: api::ProductProofContext {
- product_id: "other.dot".to_string(),
- suffix: api::DerivationIndex::Index(0),
+ fn dispatch_without_session_distinguishes_requests_responses_and_disconnects() {
+ let (_, signing_host) = signing_fixture(Arc::new(StubPlatform::default()));
+ let service = SigningHostSsoService::new(signing_host);
+ let dispatch = |data| {
+ futures::executor::block_on(service.dispatch(
+ None,
+ RemoteMessage {
+ message_id: "m-1".to_string(),
+ data: RemoteMessageData::V1(data),
},
- ring_location: api::RingLocation {
- chain_id: [0; 32],
- junctions: vec![],
- },
- }),
- ))
- .expect("response is emitted");
+ ))
+ };
- let v1::RemoteMessage::RingVrfAliasResponse(response) = response_payload(response) else {
- panic!("expected alias response");
+ assert_eq!(
+ dispatch(v1::RemoteMessage::Disconnected),
+ Dispatch::Disconnected
+ );
+ assert_eq!(
+ dispatch(v1::RemoteMessage::ProductSubtreeResponse(
+ messages::Response {
+ responding_to: "m-1".to_string(),
+ payload: Ok([7; 32]),
+ }
+ )),
+ Dispatch::NotARequest("ProductSubtreeResponse"),
+ );
+ let Dispatch::Response(answer) = dispatch(v1::RemoteMessage::ProductSubtreeRequest(
+ messages::ProductSubtreeRequest {
+ product_id: "myapp.dot".to_string(),
+ },
+ )) else {
+ panic!("expected a disconnected error response");
};
- assert_eq!(response.payload.unwrap_err(), RingVrfError::Rejected);
+ let RemoteMessageData::V1(v1::RemoteMessage::ProductSubtreeResponse(response)) =
+ answer.message.data
+ else {
+ panic!("expected the request's response variant");
+ };
+ assert_eq!(response.responding_to, "m-1");
+ assert_eq!(
+ response.payload,
+ Err("signing host session is not active".to_string())
+ );
}
#[test]
fn response_summary_reports_protocol_errors_without_multiline_output() {
- let response = RemoteMessageData::V1(v1::RemoteMessage::RingVrfAliasResponse(
- RingVrfAliasResponse {
- responding_to: "alias-1".to_string(),
- payload: Err(RingVrfError::Unknown {
- reason: "chain RPC\ntimed out".to_string(),
- }),
- },
- ));
+ let payload: GetAccountAliasResponse = Err(RingVrfError::Unknown {
+ reason: "chain RPC\ntimed out".to_string(),
+ });
- let result = remote_response_result(&response);
+ let result = ResponseOutcome::from_payload(&payload);
let summary = response_cli_summary(
"SSO response sent",
"get_account_alias",
@@ -2060,85 +1388,114 @@ mod tests {
);
}
+ #[cfg(not(target_arch = "wasm32"))]
#[test]
- fn resource_allocation_summary_reflects_per_resource_outcomes() {
- let result =
- resource_allocation_payload_result(&Ok(vec![SsoAllocationOutcome::Rejected]), &[]);
- assert_eq!(result.outcome, "rejected");
- assert_eq!(
- result.reason.as_deref(),
- Some("Requested resource was rejected")
+ fn allocation_failure_details_reach_the_response_transcript() {
+ let (_, signing_host) = signing_fixture(Arc::new(StubPlatform {
+ resource_allocation_confirmed: true,
+ chain_connect_error: Some("allocation node unavailable"),
+ ..StubPlatform::default()
+ }));
+ let service = SigningHostSsoService::new(signing_host);
+ let request = RemoteMessage::request(
+ "allocation-1".to_string(),
+ messages::ResourceAllocationRequest {
+ calling_product_id: "myapp.dot".to_string(),
+ resources: vec![
+ api::AllocatableResource::AutoSigning,
+ api::AllocatableResource::BulletinAllowance,
+ api::AllocatableResource::StatementStoreAllowance,
+ ],
+ on_existing: OnExistingAllowancePolicy::Ignore,
+ },
);
-
- let result = resource_allocation_payload_result(
- &Ok(vec![
- SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance {
- slot_account_key: vec![1; 64],
- }),
- SsoAllocationOutcome::Rejected,
+ let Dispatch::Response(answer) =
+ futures::executor::block_on(service.dispatch(service.current_session(), request))
+ else {
+ panic!("expected an allocation response");
+ };
+ let RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationResponse(response)) =
+ answer.message.data
+ else {
+ panic!("expected an allocation response");
+ };
+ assert!(matches!(
+ response.payload.unwrap().as_slice(),
+ [
+ SsoAllocationOutcome::Allocated(SsoAllocatedResource::AutoSigning { .. }),
SsoAllocationOutcome::NotAvailable,
- ]),
- &[],
- );
- assert_eq!(result.outcome, "partial");
+ SsoAllocationOutcome::NotAvailable,
+ ]
+ ));
+ assert_eq!(answer.outcome.outcome, "partial");
+ let reason = answer.outcome.reason.as_deref().unwrap();
+ assert!(reason.starts_with("1 of 3 requested resources allocated; 2 unavailable"));
assert_eq!(
- result.reason.as_deref(),
- Some("1 of 3 requested resources allocated; 1 rejected; 1 unavailable")
- );
-
- let result = resource_allocation_payload_result(
- &Ok(vec![SsoAllocationOutcome::NotAvailable]),
- &["timed out waiting for Bulletin authorization".to_string()],
+ reason.matches("allocation node unavailable").count(),
+ 2,
+ "{reason}"
);
- assert_eq!(result.outcome, "not_available");
- assert_eq!(
- result.reason.as_deref(),
- Some(
- "Requested resource is not available: timed out waiting for Bulletin authorization"
- )
+ assert!(!reason.contains(['\r', '\n']));
+ let cli = response_cli_summary(
+ "SSO response sent",
+ "resource_allocation",
+ "allocation-1",
+ "allocation-1",
+ &answer.message.message_id,
+ &answer.outcome,
+ 0,
);
+ assert!(cli.contains(&format!("reason={reason}")));
}
#[test]
- fn response_summary_classifies_resource_allocation_batches() {
- let response = RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationResponse(
- ResourceAllocationResponse {
- responding_to: "allocation-1".to_string(),
- payload: Ok(vec![
- SsoAllocationOutcome::Rejected,
- SsoAllocationOutcome::NotAvailable,
- ]),
- },
- ));
-
- let result = remote_response_result(&response);
+ fn account_alias_requires_confirmation_for_cross_product_request() {
+ let (_, signing_host) = signing_fixture(Arc::new(StubPlatform::default()));
- assert_eq!(result.outcome, "rejected");
- assert_eq!(
- result.reason.as_deref(),
- Some("No resources allocated; 1 rejected; 1 unavailable")
+ let response = answer(
+ &signing_host,
+ "alias-1",
+ v1::RemoteMessage::GetAccountAliasRequest(messages::ProductRequest {
+ calling_product_id: "myapp.dot".to_string(),
+ payload: api::HostAccountGetAliasRequest {
+ key_handle: api::ProductAccountId {
+ dot_ns_identifier: "peopl.dot".to_string(),
+ derivation_index: api::DerivationIndex::Index(0),
+ },
+ context: api::ProductProofContext {
+ product_id: "other.dot".to_string(),
+ suffix: api::DerivationIndex::Index(0),
+ },
+ ring_location: api::RingLocation {
+ chain_id: [0; 32],
+ junctions: vec![],
+ },
+ },
+ }),
);
+
+ let v1::RemoteMessage::GetAccountAliasResponse(response) = response else {
+ panic!("expected alias response");
+ };
+ assert_eq!(response.payload.unwrap_err(), RingVrfError::Rejected);
}
#[test]
fn resource_allocation_requires_confirmation_before_allocation() {
let platform = Arc::new(StubPlatform::default());
- let (services, signing_host) = signing_fixture(platform.clone());
+ let (_, signing_host) = signing_fixture(platform.clone());
- let response = futures::executor::block_on(answer_remote_message(
- &services,
+ let response = answer(
&signing_host,
- "alloc-1".to_string(),
+ "alloc-1",
v1::RemoteMessage::ResourceAllocationRequest(messages::ResourceAllocationRequest {
calling_product_id: "myapp.dot".to_string(),
- resources: vec![SsoAllocatableResource::StatementStoreAllowance],
+ resources: vec![api::AllocatableResource::StatementStoreAllowance],
on_existing: messages::OnExistingAllowancePolicy::Ignore,
}),
- ))
- .expect("response is emitted");
+ );
- let v1::RemoteMessage::ResourceAllocationResponse(response) = response_payload(response)
- else {
+ let v1::RemoteMessage::ResourceAllocationResponse(response) = response else {
panic!("expected resource allocation response");
};
assert_eq!(
@@ -2162,7 +1519,7 @@ mod tests {
resource_allocation_confirmed: true,
..StubPlatform::default()
});
- let (services, signing_host) = signing_fixture(platform);
+ let (_, signing_host) = signing_fixture(platform);
let expected_secret = signing_host
.product_subtree_secret("myapp.dot")
.expect("product subtree secret derives");
@@ -2170,20 +1527,17 @@ mod tests {
derive_ring_vrf_domain_entropy(&ENTROPY, "myapp.dot")
.expect("ring-VRF domain entropy derives");
- let response = futures::executor::block_on(answer_remote_message(
- &services,
+ let response = answer(
&signing_host,
- "alloc-auto-signing".to_string(),
+ "alloc-auto-signing",
v1::RemoteMessage::ResourceAllocationRequest(messages::ResourceAllocationRequest {
calling_product_id: "myapp.dot".to_string(),
- resources: vec![SsoAllocatableResource::AutoSigning],
+ resources: vec![api::AllocatableResource::AutoSigning],
on_existing: messages::OnExistingAllowancePolicy::Ignore,
}),
- ))
- .expect("response is emitted");
+ );
- let v1::RemoteMessage::ResourceAllocationResponse(response) = response_payload(response)
- else {
+ let v1::RemoteMessage::ResourceAllocationResponse(response) = response else {
panic!("expected resource allocation response");
};
assert_eq!(
@@ -2197,9 +1551,76 @@ mod tests {
);
}
+ fn allocation_after_session_change(replacement: Option>) {
+ use futures::{FutureExt, channel::oneshot};
+
+ let (release, gate) = oneshot::channel();
+ let platform = Arc::new(StubPlatform {
+ resource_allocation_confirmed: true,
+ resource_allocation_confirmation_gate: std::sync::Mutex::new(Some(gate)),
+ ..StubPlatform::default()
+ });
+ let (_, signing_host) = signing_fixture(platform.clone());
+ let service = SigningHostSsoService::new(signing_host.clone());
+ let message = RemoteMessage::request(
+ "alloc-stale".to_string(),
+ messages::ResourceAllocationRequest {
+ calling_product_id: "myapp.dot".to_string(),
+ resources: vec![api::AllocatableResource::AutoSigning],
+ on_existing: OnExistingAllowancePolicy::Ignore,
+ },
+ );
+
+ futures::executor::block_on(async {
+ let answer = service.dispatch(service.current_session(), message);
+ futures::pin_mut!(answer);
+ assert!(answer.as_mut().now_or_never().is_none());
+ assert_eq!(
+ platform.resource_allocation_reviews.lock().unwrap().len(),
+ 1
+ );
+
+ signing_host.disconnect().await;
+ if let Some(entropy) = replacement {
+ signing_host.activate_local_session(entropy).await.unwrap();
+ }
+ release.send(()).unwrap();
+
+ let Dispatch::Response(answer) = answer.await else {
+ panic!("expected an allocation response");
+ };
+ let RemoteMessageData::V1(v1::RemoteMessage::ResourceAllocationResponse(response)) =
+ answer.message.data
+ else {
+ panic!("expected an allocation response");
+ };
+ assert_eq!(response.responding_to, "alloc-stale");
+ assert!(
+ response.payload.is_err(),
+ "stale consent must not release keys"
+ );
+ assert!(platform.sent_rpc.lock().unwrap().is_empty());
+ });
+ }
+
+ #[test]
+ fn resource_consent_cannot_authorize_a_replacement_account() {
+ allocation_after_session_change(Some(vec![0xcd; 16]));
+ }
+
+ #[test]
+ fn resource_consent_cannot_survive_same_account_reactivation() {
+ allocation_after_session_change(Some(ENTROPY.to_vec()));
+ }
+
+ #[test]
+ fn resource_consent_cannot_survive_disconnect() {
+ allocation_after_session_change(None);
+ }
+
#[test]
fn legacy_transaction_request_uses_the_controlled_identity_account() {
- let (services, signing_host) = signing_fixture(Arc::new(StubPlatform {
+ let (_, signing_host) = signing_fixture(Arc::new(StubPlatform {
create_transaction_confirmed: true,
..StubPlatform::default()
}));
@@ -2216,25 +1637,20 @@ mod tests {
tx_ext_version: 0,
};
- let response = futures::executor::block_on(answer_remote_message(
- &services,
+ let response = answer(
&signing_host,
- "legacy-tx-1".to_string(),
- v1::RemoteMessage::CreateTransactionLegacyRequest(
- messages::CreateTransactionLegacyRequest {
+ "legacy-tx-1",
+ v1::RemoteMessage::CreateTransactionWithLegacyAccountRequest(
+ messages::CreateTransactionWithLegacyAccountRequest {
payload: messages::CreateTransactionLegacyPayload::V1(payload),
},
),
- ))
- .expect("response is emitted");
+ );
- let v1::RemoteMessage::CreateTransactionResponse(response) = response_payload(response)
- else {
+ let v1::RemoteMessage::CreateTransactionResponse(response) = response else {
panic!("expected create transaction response");
};
- let transaction = response
- .signed_transaction
- .expect("identity transaction succeeds");
+ let transaction = response.payload.expect("identity transaction succeeds");
let (account, signature, tail) = split_v4(&transaction);
assert_eq!(account, identity.public.to_bytes());
assert_eq!(tail, vec![1, 0x00, 0x00]);
@@ -2249,18 +1665,16 @@ mod tests {
#[test]
fn product_subtree_request_is_consent_free_and_hard_derived() {
- let (services, signing_host) = signing_fixture(Arc::new(StubPlatform::default()));
- let response = futures::executor::block_on(answer_remote_message(
- &services,
+ let (_, signing_host) = signing_fixture(Arc::new(StubPlatform::default()));
+ let response = answer(
&signing_host,
- "subtree-1".to_string(),
+ "subtree-1",
v1::RemoteMessage::ProductSubtreeRequest(messages::ProductSubtreeRequest {
product_id: "browse.dot".to_string(),
}),
- ))
- .expect("response is emitted");
+ );
- let v1::RemoteMessage::ProductSubtreeResponse(response) = response_payload(response) else {
+ let v1::RemoteMessage::ProductSubtreeResponse(response) = response else {
panic!("expected product subtree response");
};
let root =
@@ -2271,7 +1685,7 @@ mod tests {
.public
.to_bytes();
assert_eq!(response.responding_to, "subtree-1");
- assert_eq!(response.product_public_key, Ok(expected));
+ assert_eq!(response.payload, Ok(expected));
}
#[test]
diff --git a/rust/crates/truapi-server/src/runtime/signing_host/sso_service.rs b/rust/crates/truapi-server/src/runtime/signing_host/sso_service.rs
new file mode 100644
index 000000000..f47cfba98
--- /dev/null
+++ b/rust/crates/truapi-server/src/runtime/signing_host/sso_service.rs
@@ -0,0 +1,552 @@
+//! The signing host's answers to paired hosts: consent prompts, then the
+//! local authority.
+
+use std::sync::Arc;
+
+use tracing::warn;
+use truapi::latest as api;
+use truapi_platform::{
+ CreateTransactionReview, ResourceAllocationReview, SignPayloadReview, SignRawReview,
+ UserConfirmationReview,
+};
+
+use super::SigningHost;
+use super::sso_responder::{
+ AllowanceAllocationError, allocate_bulletin_allowance, allocate_smart_contract_allowance,
+ allocate_statement_store_allowance,
+};
+use crate::host_logic::product_account::{
+ derive_ring_vrf_domain_entropy, product_public_key_to_address,
+};
+use crate::host_logic::sso::messages::{
+ CreateAccountProofResponse, CreateTransactionLegacyPayload, CreateTransactionPayload,
+ CreateTransactionRequest, CreateTransactionResponse, CreateTransactionWithLegacyAccountRequest,
+ GetAccountAliasResponse, ListRingVrfKeysResponse, OnExistingAllowancePolicy, ProductRequest,
+ ProductSubtreeRequest, ProductSubtreeResponse, RegisterRingVrfKeyResponse,
+ ResourceAllocationRequest, ResourceAllocationResponse, RingVrfSignResponse,
+ SignRawWithLegacyAccountRequest, SignRawWithLegacyAccountResponse, SignRequest, SignResponse,
+ SignVrfResponse, SsoAllocatedResource, SsoAllocationOutcome,
+};
+use crate::host_logic::sso::wire::ResponseOutcome;
+use crate::runtime::authority::{
+ AuthoritySession, CreateTransactionAuthorityRequest, ProductAuthority,
+ SignPayloadAuthorityRequest, SignRawAuthorityRequest,
+};
+use crate::runtime::sso_service::{SsoReply, SsoRequestContext};
+
+/// SSO handlers served by a locally activated [`SigningHost`].
+pub(crate) struct SigningHostSsoService {
+ signing_host: Arc,
+}
+
+impl SigningHostSsoService {
+ /// Serve requests and prompt through the signing host's platform.
+ pub(crate) fn new(signing_host: Arc) -> Self {
+ Self { signing_host }
+ }
+
+ /// The signing session captured before dispatching one request.
+ pub(crate) fn current_session(&self) -> Option {
+ self.signing_host.current_session()
+ }
+
+ /// Run the platform confirmation seam; rejection and failure both refuse
+ /// the operation with an opaque reason (host-spec B.7).
+ async fn confirm(&self, review: UserConfirmationReview) -> Result<(), String> {
+ match self.signing_host.platform.confirm_user_action(review).await {
+ Ok(true) => Ok(()),
+ Ok(false) => Err("Rejected".to_string()),
+ Err(err) => Err(format!("confirmation failed: {}", err.reason)),
+ }
+ }
+
+ async fn serve_sign(
+ &self,
+ cx: &SsoRequestContext,
+ request: SignRequest,
+ ) -> Result {
+ match request {
+ SignRequest::Payload(request) => {
+ let request = *request;
+ self.confirm(UserConfirmationReview::SignPayload(
+ SignPayloadReview::Product(request.clone()),
+ ))
+ .await?;
+ self.signing_host
+ .sign_payload(
+ &cx.call,
+ &cx.session,
+ SignPayloadAuthorityRequest::Product(request),
+ )
+ .await
+ }
+ SignRequest::Raw(request) => {
+ self.confirm(UserConfirmationReview::SignRaw(SignRawReview::Product(
+ request.clone(),
+ )))
+ .await?;
+ self.signing_host
+ .sign_raw(
+ &cx.call,
+ &cx.session,
+ SignRawAuthorityRequest::Product(request),
+ )
+ .await
+ }
+ }
+ .map_err(|err| err.to_string())
+ }
+
+ async fn serve_create_transaction(
+ &self,
+ cx: &SsoRequestContext,
+ review: CreateTransactionReview,
+ request: CreateTransactionAuthorityRequest,
+ ) -> Result, String> {
+ self.confirm(UserConfirmationReview::CreateTransaction(review))
+ .await?;
+ self.signing_host
+ .create_transaction(&cx.call, &cx.session, request)
+ .await
+ .map(|response| response.transaction)
+ .map_err(|err| err.to_string())
+ }
+
+ async fn allocate(
+ &self,
+ session: &AuthoritySession,
+ calling_product_id: &str,
+ resource: api::AllocatableResource,
+ on_existing: OnExistingAllowancePolicy,
+ ) -> Result {
+ let signing_host = &self.signing_host;
+ let services = &signing_host.services;
+ match resource {
+ api::AllocatableResource::StatementStoreAllowance => {
+ allocate_statement_store_allowance(
+ services,
+ signing_host,
+ session,
+ calling_product_id,
+ on_existing,
+ )
+ .await
+ .map(|slot_account_key| {
+ SsoAllocationOutcome::Allocated(SsoAllocatedResource::StatementStoreAllowance {
+ slot_account_key,
+ })
+ })
+ }
+ api::AllocatableResource::BulletinAllowance => allocate_bulletin_allowance(
+ services,
+ signing_host,
+ session,
+ calling_product_id,
+ on_existing,
+ )
+ .await
+ .map(|slot_account_key| {
+ SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance {
+ slot_account_key,
+ })
+ }),
+ api::AllocatableResource::SmartContractAllowance(index) => {
+ allocate_smart_contract_allowance(
+ services,
+ signing_host,
+ session,
+ calling_product_id,
+ index,
+ on_existing,
+ )
+ .await
+ .map(|()| {
+ SsoAllocationOutcome::Allocated(SsoAllocatedResource::SmartContractAllowance)
+ })
+ }
+ api::AllocatableResource::AutoSigning => {
+ 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,
+ },
+ ))
+ }
+ }
+ }
+}
+
+fn allocation_reply(
+ payload: Result, String>,
+ failures: Vec,
+) -> SsoReply {
+ let mut outcome = resource_allocation_outcome(&payload);
+ if !failures.is_empty() {
+ let details = failures.join("; ").replace(['\r', '\n'], " ");
+ outcome.reason = Some(match outcome.reason {
+ Some(summary) => format!("{summary}: {details}"),
+ None => details,
+ });
+ }
+ SsoReply::from(payload).with_outcome(outcome)
+}
+
+/// Transcript outcome for an allocation batch: `ok` only when every requested
+/// resource was allocated; otherwise `rejected`, `partial`, or `not_available`
+/// with a count summary.
+fn resource_allocation_outcome(
+ payload: &Result, String>,
+) -> ResponseOutcome {
+ let outcomes = match payload {
+ Ok(outcomes) => outcomes,
+ Err(reason) => {
+ return ResponseOutcome {
+ outcome: "error",
+ reason: Some(reason.clone()),
+ };
+ }
+ };
+ let total = outcomes.len();
+ let count = |wanted: fn(&SsoAllocationOutcome) -> bool| {
+ outcomes.iter().filter(|outcome| wanted(outcome)).count()
+ };
+ let allocated = count(|outcome| matches!(outcome, SsoAllocationOutcome::Allocated(_)));
+ let rejected = count(|outcome| matches!(outcome, SsoAllocationOutcome::Rejected));
+ let unavailable = count(|outcome| matches!(outcome, SsoAllocationOutcome::NotAvailable));
+ if allocated == total {
+ return ResponseOutcome {
+ outcome: "ok",
+ reason: None,
+ };
+ }
+ if allocated > 0 {
+ let mut reason = format!("{allocated} of {total} requested resources allocated");
+ if rejected > 0 {
+ reason.push_str(&format!("; {rejected} rejected"));
+ }
+ if unavailable > 0 {
+ reason.push_str(&format!("; {unavailable} unavailable"));
+ }
+ return ResponseOutcome {
+ outcome: "partial",
+ reason: Some(reason),
+ };
+ }
+ if rejected > 0 {
+ let reason = if rejected == total {
+ if total == 1 {
+ "Requested resource was rejected".to_string()
+ } else {
+ format!("All {total} requested resources were rejected")
+ }
+ } else {
+ format!("No resources allocated; {rejected} rejected; {unavailable} unavailable")
+ };
+ return ResponseOutcome {
+ outcome: "rejected",
+ reason: Some(reason),
+ };
+ }
+ ResponseOutcome {
+ outcome: "not_available",
+ reason: Some(if total == 1 {
+ "Requested resource is not available".to_string()
+ } else {
+ format!("None of the {total} requested resources are available")
+ }),
+ }
+}
+
+#[truapi_macros::sso_service]
+impl SigningHostSsoService {
+ /// Sign a payload or raw bytes with a product account.
+ async fn sign(&self, cx: &SsoRequestContext, request: SignRequest) -> SignResponse {
+ let payload = self.serve_sign(cx, request).await;
+ if let Err(reason) = &payload {
+ warn!(%reason, "sign request failed");
+ }
+ payload
+ }
+
+ /// Derive a contextual alias for a registered ring-VRF key.
+ async fn get_account_alias(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> GetAccountAliasResponse {
+ self.signing_host
+ .account_alias(&cx.call, &cx.session, request)
+ .await
+ }
+
+ /// Allocate SSO-backed resources for a product.
+ async fn resource_allocation(
+ &self,
+ cx: &SsoRequestContext,
+ request: ResourceAllocationRequest,
+ ) -> ResourceAllocationResponse {
+ let mut failures = Vec::new();
+ let payload = async {
+ let review = UserConfirmationReview::ResourceAllocation(ResourceAllocationReview {
+ calling_product_id: request.calling_product_id.clone(),
+ resources: request.resources.clone(),
+ });
+ match self.signing_host.platform.confirm_user_action(review).await {
+ Ok(true) => {}
+ Ok(false) => {
+ return Ok(vec![
+ SsoAllocationOutcome::Rejected;
+ request.resources.len()
+ ]);
+ }
+ Err(err) => return Err(format!("confirmation failed: {}", err.reason)),
+ }
+
+ self.signing_host
+ .require_current_session(&cx.session)
+ .map_err(|err| err.to_string())?;
+ let mut outcomes = Vec::with_capacity(request.resources.len());
+ for resource in request.resources {
+ self.signing_host
+ .require_current_session(&cx.session)
+ .map_err(|err| err.to_string())?;
+ let outcome = self
+ .allocate(
+ &cx.session,
+ &request.calling_product_id,
+ resource,
+ request.on_existing,
+ )
+ .await;
+ self.signing_host
+ .require_current_session(&cx.session)
+ .map_err(|err| err.to_string())?;
+ outcomes.push(outcome.unwrap_or_else(|err| {
+ let reason = err.to_string();
+ warn!(%reason, "resource allocation item failed");
+ failures.push(reason);
+ SsoAllocationOutcome::NotAvailable
+ }));
+ }
+ Ok(outcomes)
+ }
+ .await;
+ if let Err(reason) = &payload {
+ warn!(%reason, "resource allocation request failed");
+ }
+ allocation_reply(payload, failures)
+ }
+
+ /// Build a signed transaction for a product account.
+ async fn create_transaction(
+ &self,
+ cx: &SsoRequestContext,
+ request: CreateTransactionRequest,
+ ) -> CreateTransactionResponse {
+ let CreateTransactionPayload::V1(payload) = request.payload;
+ self.serve_create_transaction(
+ cx,
+ CreateTransactionReview::Product(payload.clone()),
+ CreateTransactionAuthorityRequest::Product(payload),
+ )
+ .await
+ }
+
+ /// Build a signed transaction for the wallet's identity account.
+ async fn create_transaction_with_legacy_account(
+ &self,
+ cx: &SsoRequestContext,
+ request: CreateTransactionWithLegacyAccountRequest,
+ ) -> CreateTransactionResponse {
+ let CreateTransactionLegacyPayload::V1(payload) = request.payload;
+ self.serve_create_transaction(
+ cx,
+ CreateTransactionReview::LegacyAccount(payload.clone()),
+ CreateTransactionAuthorityRequest::IdentityAccount(payload),
+ )
+ .await
+ }
+
+ /// Sign raw data with a legacy account.
+ async fn sign_raw_with_legacy_account(
+ &self,
+ cx: &SsoRequestContext,
+ request: SignRawWithLegacyAccountRequest,
+ ) -> SignRawWithLegacyAccountResponse {
+ let public_request = api::HostSignRawWithLegacyAccountRequest {
+ signer: product_public_key_to_address(request.account),
+ payload: request.data,
+ };
+ self.confirm(UserConfirmationReview::SignRaw(
+ SignRawReview::LegacyAccount(public_request.clone()),
+ ))
+ .await?;
+ self.signing_host
+ .sign_raw(
+ &cx.call,
+ &cx.session,
+ SignRawAuthorityRequest::LegacyAccount {
+ account: request.account,
+ request: public_request,
+ },
+ )
+ .await
+ .map(|response| response.signature)
+ .map_err(|err| err.to_string())
+ }
+
+ /// Create a ring-VRF proof bound to a context and message.
+ async fn create_account_proof(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> CreateAccountProofResponse {
+ self.signing_host
+ .create_proof(&cx.call, &cx.session, request)
+ .await
+ }
+
+ /// Sign an RFC-0023 VRF transcript.
+ async fn sign_vrf(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> SignVrfResponse {
+ self.signing_host
+ .sign_vrf(
+ &cx.call,
+ &cx.session,
+ request.calling_product_id,
+ request.payload,
+ )
+ .await
+ .map_err(api::HostAccountSignVrfError::from)
+ }
+
+ /// Consent-free product hard-subtree public key.
+ async fn product_subtree(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductSubtreeRequest,
+ ) -> ProductSubtreeResponse {
+ self.signing_host
+ .product_subtree_public_key(&cx.call, &cx.session, request.product_id)
+ .await
+ .map_err(|err| err.to_string())
+ }
+
+ /// Register a ring-VRF key owned by the calling product.
+ async fn register_ring_vrf_key(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> RegisterRingVrfKeyResponse {
+ self.signing_host
+ .register_ring_vrf_key(&cx.call, &cx.session, request)
+ .await
+ }
+
+ /// List registered ring-VRF keys.
+ async fn list_ring_vrf_keys(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> ListRingVrfKeysResponse {
+ self.signing_host
+ .list_ring_vrf_keys(&cx.call, &cx.session, request)
+ .await
+ }
+
+ /// Sign bytes directly with a registered ring-VRF key.
+ async fn ring_vrf_sign(
+ &self,
+ cx: &SsoRequestContext,
+ request: ProductRequest,
+ ) -> RingVrfSignResponse {
+ self.signing_host
+ .ring_vrf_sign(&cx.call, &cx.session, request)
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn resource_allocation_summary_reflects_per_resource_outcomes() {
+ let result = resource_allocation_outcome(&Ok(vec![SsoAllocationOutcome::Allocated(
+ SsoAllocatedResource::SmartContractAllowance,
+ )]));
+ assert_eq!((result.outcome, result.reason), ("ok", None));
+
+ let result = resource_allocation_outcome(&Ok(vec![SsoAllocationOutcome::Rejected]));
+ assert_eq!(
+ (result.outcome, result.reason.as_deref()),
+ ("rejected", Some("Requested resource was rejected"))
+ );
+
+ let result = resource_allocation_outcome(&Ok(vec![
+ SsoAllocationOutcome::Allocated(SsoAllocatedResource::BulletinAllowance {
+ slot_account_key: vec![1; 64],
+ }),
+ SsoAllocationOutcome::Rejected,
+ SsoAllocationOutcome::NotAvailable,
+ ]));
+ assert_eq!(
+ (result.outcome, result.reason.as_deref()),
+ (
+ "partial",
+ Some("1 of 3 requested resources allocated; 1 rejected; 1 unavailable")
+ )
+ );
+
+ let result = resource_allocation_outcome(&Ok(vec![SsoAllocationOutcome::NotAvailable]));
+ assert_eq!(
+ (result.outcome, result.reason.as_deref()),
+ ("not_available", Some("Requested resource is not available"))
+ );
+ }
+
+ #[test]
+ fn mixed_unallocated_resources_report_rejection() {
+ let result = resource_allocation_outcome(&Ok(vec![
+ SsoAllocationOutcome::Rejected,
+ SsoAllocationOutcome::NotAvailable,
+ ]));
+
+ assert_eq!(
+ (result.outcome, result.reason.as_deref()),
+ (
+ "rejected",
+ Some("No resources allocated; 1 rejected; 1 unavailable")
+ )
+ );
+ }
+
+ #[test]
+ fn allocation_transcript_includes_single_line_item_failures() {
+ let answer = allocation_reply(
+ Ok(vec![SsoAllocationOutcome::NotAvailable]),
+ vec!["rpc\nfailed".to_string(), "provider\rdown".to_string()],
+ )
+ .finish(
+ "allocation-1",
+ crate::host_logic::sso::messages::v1::RemoteMessage::ResourceAllocationResponse,
+ );
+
+ assert_eq!(answer.outcome.outcome, "not_available");
+ assert_eq!(
+ answer.outcome.reason.as_deref(),
+ Some("Requested resource is not available: rpc failed; provider down")
+ );
+ }
+}
diff --git a/rust/crates/truapi-server/src/runtime/sso_remote.rs b/rust/crates/truapi-server/src/runtime/sso_remote.rs
index c4125090d..c444b6776 100644
--- a/rust/crates/truapi-server/src/runtime/sso_remote.rs
+++ b/rust/crates/truapi-server/src/runtime/sso_remote.rs
@@ -9,8 +9,9 @@ use std::sync::Mutex;
use super::statement_store_rpc;
use crate::host_logic::session::SsoSessionInfo;
use crate::host_logic::sso::messages::{
- SsoRemoteResponse, SsoSessionStatement, decode_sso_session_statement,
+ Response, SsoSessionStatement, decode_sso_session_statement, v1,
};
+use crate::host_logic::sso::wire::SsoRequest;
use crate::host_logic::statement_store::{current_unix_secs, parse_new_statements_result};
use futures::channel::oneshot;
@@ -244,12 +245,35 @@ fn disconnect_error(reason: String) -> SsoRemoteResponseError {
}
}
-/// Wait for the response matching `remote_message_id`, racing the statement
-/// streams against submit failure, cancellation, and disconnect signals.
+/// Matcher for [`wait_for_sso_remote_response`]: the response to the request
+/// sent as `message_id`. A response addressed to it but of another kind is an
+/// error, so a confused peer fails the call instead of stalling it.
+pub(super) fn reply_matcher(
+ message_id: &str,
+) -> impl Fn(v1::RemoteMessage) -> Option, String>> + '_ {
+ move |message| {
+ if message.responding_to() != Some(message_id) {
+ return None;
+ }
+ let kind = message.name();
+ Some(
+ R::response_from_message(message)
+ .ok_or_else(|| format!("Unexpected SSO response for {}: {kind}", R::NAME)),
+ )
+ }
+}
+
+/// Wait for the reply `matches` accepts, racing the statement streams against
+/// submit failure, cancellation, and disconnect signals.
+///
+/// `matches` sees every peer message in wire order: `None` skips a message
+/// meant for someone else, `Some(Err(reason))` fails the wait for a message
+/// addressed to this request but of the wrong kind.
#[instrument(skip_all, fields(runtime.method = "sso.remote_response.wait"))]
-pub(super) async fn wait_for_sso_remote_response(
+pub(super) async fn wait_for_sso_remote_response(
wait: RemoteResponseWait<'_>,
-) -> Result {
+ matches: impl Fn(v1::RemoteMessage) -> Option>,
+) -> Result {
let RemoteResponseWait {
own_statements,
peer_statements,
@@ -267,6 +291,7 @@ pub(super) async fn wait_for_sso_remote_response(
session,
statement_request_id,
remote_message_id,
+ &matches,
)
.fuse();
let disconnect = async move {
@@ -294,14 +319,15 @@ pub(super) async fn wait_for_sso_remote_response(
}
#[instrument(skip_all, fields(runtime.method = "sso.remote_response.wait_inner"))]
-async fn wait_for_sso_remote_response_inner(
+async fn wait_for_sso_remote_response_inner(
own_statements: StatementPageStream,
peer_statements: StatementPageStream,
submit: StatementSubmitFuture,
session: &SsoSessionInfo,
statement_request_id: &str,
remote_message_id: &str,
-) -> Result {
+ matches: &impl Fn(v1::RemoteMessage) -> Option>,
+) -> Result {
let mut own_statements = own_statements.fuse();
let mut peer_statements = peer_statements.fuse();
let mut submit = submit.fuse();
@@ -325,7 +351,7 @@ async fn wait_for_sso_remote_response_inner(
session,
&value,
statement_request_id,
- remote_message_id,
+ matches,
&mut request_accepted,
&mut pending_remote_response,
)? {
@@ -343,7 +369,7 @@ async fn wait_for_sso_remote_response_inner(
session,
&value,
statement_request_id,
- remote_message_id,
+ matches,
&mut request_accepted,
&mut pending_remote_response,
)? {
@@ -361,24 +387,19 @@ async fn wait_for_sso_remote_response_inner(
}
}
-fn handle_sso_remote_statement_page(
+fn handle_sso_remote_statement_page(
session: &SsoSessionInfo,
value: &Value,
statement_request_id: &str,
- remote_message_id: &str,
+ matches: &impl Fn(v1::RemoteMessage) -> Option>,
request_accepted: &mut bool,
- pending_remote_response: &mut Option,
-) -> Result, SsoRemoteResponseError> {
+ pending_remote_response: &mut Option