diff --git a/js/packages/truapi/README.md b/js/packages/truapi/README.md index 46d47676d..9a456eb04 100644 --- a/js/packages/truapi/README.md +++ b/js/packages/truapi/README.md @@ -72,6 +72,13 @@ sub.unsubscribe(); - **Sandbox bootstrap** (`@parity/truapi/sandbox`) that detects the host environment, builds the matching provider, and exposes a cached client — see below. +## Development escape hatches + +- **`development_createAccountProof(client, request)`** — `account.createAccountProof` + with `context` given as the exact 32-byte hex the proof is bound to, instead of a + product-namespaced `ProductProofContext`. Yet to be removed before a production + release; it lives entirely in `src/development.ts`. + ## Sandbox bootstrap `@parity/truapi/sandbox` wires up a client for browser-embedded hosts: it detects whether the app diff --git a/js/packages/truapi/src/development.test.ts b/js/packages/truapi/src/development.test.ts new file mode 100644 index 000000000..92a30514a --- /dev/null +++ b/js/packages/truapi/src/development.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, test } from "bun:test"; +import { okAsync } from "neverthrow"; +import { development_createAccountProof } from "./development.js"; +import type { HostAccountCreateProofRequest } from "./generated/index.js"; + +const context = `0x${"ab".repeat(32)}` as const; +const base = { + keyHandle: { dotNsIdentifier: "dim2.dot", derivationIndex: { tag: "Index", value: 0 } }, + ringLocation: { chainId: `0x${"00".repeat(32)}`, junctions: [] }, + message: "0x01", +} as const; + +function stub() { + const seen: HostAccountCreateProofRequest[] = []; + const client = { + account: { + createAccountProof(request: HostAccountCreateProofRequest) { + seen.push(request); + return okAsync({ + proof: "0x", + contextualAlias: { context, alias: "0x" }, + ringIndex: 0, + ringRevision: 0, + }); + }, + }, + }; + return { + seen, + client: client as unknown as Parameters[0], + }; +} + +describe("development_createAccountProof", () => { + test("forwards the request with the raw context marker", async () => { + const { seen, client } = stub(); + await development_createAccountProof(client, { ...base, context }); + expect(seen).toEqual([ + { ...base, context: { productId: "raw:", suffix: { tag: "Raw", value: context } } }, + ]); + }); + + test("rejects contexts that are not 32 bytes of hex", () => { + const { client } = stub(); + for (const bad of ["0x00", "ab".repeat(32), `0x${"zz".repeat(32)}`]) { + expect(() => + development_createAccountProof(client, { ...base, context: bad as `0x${string}` }), + ).toThrow(TypeError); + } + }); +}); diff --git a/js/packages/truapi/src/development.ts b/js/packages/truapi/src/development.ts new file mode 100644 index 000000000..41aec66af --- /dev/null +++ b/js/packages/truapi/src/development.ts @@ -0,0 +1,60 @@ +// TODO(development_createAccountProof): dev-only escape hatch, yet to be +// removed before a production release. Everything for it lives in this file, +// its test and one re-export in `index.ts`; delete those to remove it. +import type { + HostAccountCreateProofRequest, + HostAccountCreateProofResponse, + ProductProofContext, + TrUApiClient, + VersionedHostAccountCreateProofError, +} from "./generated/index.js"; +import type { ResultAsync } from "./generated/client.js"; +import type { CallErrorValue, HexString } from "./scale.js"; + +/** `productId` the signing host reads as "use the suffix bytes verbatim". */ +const RAW_PROOF_CONTEXT_PRODUCT_ID = "raw:"; + +/** Same as `HostAccountCreateProofRequest`, with the 32-byte context given raw. */ +export interface DevelopmentCreateProofRequest extends Omit< + HostAccountCreateProofRequest, + "context" +> { + /** The exact 32 bytes the proof is bound to, as `0x`-prefixed hex. */ + context: HexString; +} + +/** + * `account.createAccountProof` with a verbatim 32-byte proof context instead of + * a product-namespaced one. + * + */ +export function development_createAccountProof( + client: Pick, + request: DevelopmentCreateProofRequest, +): ResultAsync< + HostAccountCreateProofResponse, + CallErrorValue +> { + const { context, ...rest } = request; + return client.account.createAccountProof({ + ...rest, + context: rawProofContext(context), + }); +} + +function rawProofContext(context: HexString): ProductProofContext { + const digits = context.startsWith("0x") ? context.slice(2) : null; + if ( + digits === null || + digits.length !== 64 || + !/^[0-9a-fA-F]*$/.test(digits) + ) { + throw new TypeError( + `development_createAccountProof: context must be 32 bytes of 0x-prefixed hex, got ${JSON.stringify(context)}`, + ); + } + return { + productId: RAW_PROOF_CONTEXT_PRODUCT_ID, + suffix: { tag: "Raw", value: context }, + }; +} diff --git a/js/packages/truapi/src/index.ts b/js/packages/truapi/src/index.ts index 2648df85b..726ddd851 100644 --- a/js/packages/truapi/src/index.ts +++ b/js/packages/truapi/src/index.ts @@ -30,3 +30,4 @@ export * as scale from "./scale.js"; export type { Codec, HexString } from "./scale.js"; export * from "./generated/index.js"; export * from "./well-known-chains.js"; +export * from "./development.js"; diff --git a/rust/crates/truapi-host-cli/README.md b/rust/crates/truapi-host-cli/README.md index 60e18db2c..86f67920e 100644 --- a/rust/crates/truapi-host-cli/README.md +++ b/rust/crates/truapi-host-cli/README.md @@ -105,6 +105,13 @@ make headless install # build dependencies and install truapi-host once truapi-host signing-host ``` +### Raw proof contexts (development only) + +A product can bind a ring-VRF proof to 32 bytes of its choosing instead of a +product-namespaced context by calling `development_createAccountProof` from +`@parity/truapi`; the signing host honours it as is. Yet to be removed before a +production release. + ### Browser products `truapi-host dev` is one command for "run this product as if it were inside a diff --git a/rust/crates/truapi-server/src/runtime/pairing_host.rs b/rust/crates/truapi-server/src/runtime/pairing_host.rs index be64b953b..d68e5f231 100644 --- a/rust/crates/truapi-server/src/runtime/pairing_host.rs +++ b/rust/crates/truapi-server/src/runtime/pairing_host.rs @@ -56,8 +56,8 @@ use zeroize::Zeroizing; use super::ring_vrf_registry::{RingVrfRegistryStore, validate_owner_listing}; use super::signing_host::ring_vrf::{ - ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, context_bytes, - create_proof, member_from_entropy, sign_from_entropy, + ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, create_proof, + development_context_bytes, member_from_entropy, sign_from_entropy, }; /// Distinguishes all remote authority request entrypoints by wire label. @@ -2127,7 +2127,7 @@ impl PairingHost { { self.ring_resolver.validate(&request.ring_location).await?; self.current_private_session(session)?; - let context = context_bytes(&request.context); + let context = development_context_bytes(&request.context); let alias = alias_from_entropy(&entropy, &context)?; return Ok(v01::ContextualAlias { context, @@ -2160,7 +2160,7 @@ impl PairingHost { .resolve(&request.ring_location, &[MemberCandidate { member }]) .await?; self.current_private_session(session)?; - let context = context_bytes(&request.context); + let context = development_context_bytes(&request.context); let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?; return Ok(v01::HostAccountCreateProofResponse { proof, diff --git a/rust/crates/truapi-server/src/runtime/signing_host.rs b/rust/crates/truapi-server/src/runtime/signing_host.rs index 3469cef32..d2f0c82d2 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host.rs @@ -65,8 +65,8 @@ use crate::runtime::statement_allowance::CollectionCandidate; #[cfg(not(target_arch = "wasm32"))] use crate::runtime::statement_allowance::collection::PersonhoodCollection; use ring_vrf::{ - ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, context_bytes, - create_proof, member_from_entropy, sign_from_entropy, + ChainRingResolver, MemberCandidate, RingResolver, alias_from_entropy, create_proof, + development_context_bytes, member_from_entropy, sign_from_entropy, }; use sso_replay::SsoReplayLocks; @@ -839,7 +839,7 @@ impl ProductAuthority for SigningHost { .resolve_ring_vrf_key_for_ring(session, &request.key_handle, &request.ring_location) .await?; self.ring_resolver.validate(&request.ring_location).await?; - let context = context_bytes(&request.context); + let context = development_context_bytes(&request.context); let alias = alias_from_entropy(&entropy, &context)?; Ok(v01::ContextualAlias { context, @@ -866,7 +866,7 @@ impl ProductAuthority for SigningHost { // 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 = context_bytes(&request.context); + let context = development_context_bytes(&request.context); let (proof, alias) = create_proof(&entropy, &resolved, &context, &request.message)?; Ok(v01::HostAccountCreateProofResponse { proof, diff --git a/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs b/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs index 7e6e51b8e..11a49ab92 100644 --- a/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs +++ b/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs @@ -212,6 +212,27 @@ impl RingResolver for ChainRingResolver { } } +// TODO(development_createAccountProof): dev-only escape hatch, yet to be +// removed before a production release. Delete this module and point its +// callers in `signing_host.rs` and `pairing_host.rs` back at `context_bytes`. +mod development { + use truapi::v01::{DerivationIndex, ProductProofContext}; + + const RAW_CONTEXT_PRODUCT_ID: &str = "raw:"; + + /// [`super::context_bytes`], except that the `raw:` product id (which + /// dotNS cannot issue) makes the `Raw` suffix the context, verbatim. + pub(in crate::runtime) fn development_context_bytes(context: &ProductProofContext) -> [u8; 32] { + if context.product_id == RAW_CONTEXT_PRODUCT_ID + && let DerivationIndex::Raw(bytes) = context.suffix + { + return bytes; + } + super::context_bytes(context) + } +} +pub(in crate::runtime) use development::development_context_bytes; + pub(in crate::runtime) fn context_bytes(context: &ProductProofContext) -> [u8; 32] { let suffix = derivation_index_bytes(&context.suffix); let mut input = Vec::with_capacity(9 + context.product_id.len() + suffix.len());