From e405c75fb4fe3ffa16dd99cadcd8d2b62da9eabc Mon Sep 17 00:00:00 2001 From: 8e8b2c <138928994+8e8b2c@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:52:43 +0100 Subject: [PATCH] fix: constrain external ID attestation author in recipes (#117) --- crates/holoom_types/src/recipe.rs | 5 +- .../src/recipe_execution.rs | 4 +- .../src/recipe.rs | 2 +- .../src/recipe_execution.rs | 7 ++- .../recipe/can_execute_basic_recipe.test.ts | 8 ++- ...use_untrusted_external_id_attestor.test.ts | 60 +++++++++++++++++++ packages/types/src/types/RecipeInstruction.ts | 3 +- 7 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/tryorama/src/recipe/cannot_use_untrusted_external_id_attestor.test.ts diff --git a/crates/holoom_types/src/recipe.rs b/crates/holoom_types/src/recipe.rs index 064a223..9766669 100644 --- a/crates/holoom_types/src/recipe.rs +++ b/crates/holoom_types/src/recipe.rs @@ -37,7 +37,10 @@ pub enum RecipeInstruction { GetDocsListedByVar { var_name: String, }, - GetLatestCallerExternalId, + GetLatestCallerExternalId { + #[ts(type = "AgentPubKey")] + trusted_author: AgentPubKey, + }, GetCallerAgentPublicKey, } diff --git a/crates/username_registry_coordinator/src/recipe_execution.rs b/crates/username_registry_coordinator/src/recipe_execution.rs index 2ab3d14..f04ade2 100644 --- a/crates/username_registry_coordinator/src/recipe_execution.rs +++ b/crates/username_registry_coordinator/src/recipe_execution.rs @@ -117,11 +117,11 @@ pub fn execute_recipe(payload: ExecuteRecipePayload) -> ExternResult { RecipeInstructionExecution::GetDocsListedByVar { doc_ahs }; (val, instruction_execution) } - RecipeInstruction::GetLatestCallerExternalId => { + RecipeInstruction::GetLatestCallerExternalId { trusted_author } => { let mut attestation_records = get_external_id_attestations_for_agent( GetExternalIdAttestationsForAgentPayload { agent_pubkey: agent_info()?.agent_initial_pubkey, - trusted_authorities: recipe.trusted_authors.clone(), + trusted_authorities: vec![trusted_author], }, )?; let attestation_record = diff --git a/crates/username_registry_validation/src/recipe.rs b/crates/username_registry_validation/src/recipe.rs index 0b0ad5f..672c578 100644 --- a/crates/username_registry_validation/src/recipe.rs +++ b/crates/username_registry_validation/src/recipe.rs @@ -36,7 +36,7 @@ pub fn validate_create_recipe( let var_dependencies = match inst { RecipeInstruction::Constant { .. } | RecipeInstruction::GetCallerAgentPublicKey - | RecipeInstruction::GetLatestCallerExternalId => Vec::new(), + | RecipeInstruction::GetLatestCallerExternalId { .. } => Vec::new(), RecipeInstruction::GetDocsListedByVar { var_name } => vec![var_name], RecipeInstruction::GetLatestDocWithIdentifier { var_name } => vec![var_name], RecipeInstruction::Jq { diff --git a/crates/username_registry_validation/src/recipe_execution.rs b/crates/username_registry_validation/src/recipe_execution.rs index d0c62cb..e9adbc5 100644 --- a/crates/username_registry_validation/src/recipe_execution.rs +++ b/crates/username_registry_validation/src/recipe_execution.rs @@ -128,9 +128,14 @@ pub fn validate_create_recipe_execution( } ( RecipeInstructionExecution::GetLatestCallerExternalId { attestation_ah }, - RecipeInstruction::GetLatestCallerExternalId, + RecipeInstruction::GetLatestCallerExternalId { trusted_author }, ) => { let attestation_record = must_get_valid_record(attestation_ah)?; + if attestation_record.action().author() != &trusted_author { + return Ok(ValidateCallbackResult::Invalid( + "ExternalIdAttestation is by untrusted author".into(), + )); + } let attestation: ExternalIdAttestation = deserialize_record_entry(attestation_record)?; Val::obj(IndexMap::from([ diff --git a/packages/tryorama/src/recipe/can_execute_basic_recipe.test.ts b/packages/tryorama/src/recipe/can_execute_basic_recipe.test.ts index 1c42371..529763a 100644 --- a/packages/tryorama/src/recipe/can_execute_basic_recipe.test.ts +++ b/packages/tryorama/src/recipe/can_execute_basic_recipe.test.ts @@ -91,7 +91,13 @@ test("Can execute basic recipe", async () => { }, ], ["foos", { type: "GetDocsListedByVar", var_name: "foo_name_list" }], - ["caller_external_id", { type: "GetLatestCallerExternalId" }], + [ + "caller_external_id", + { + type: "GetLatestCallerExternalId", + trusted_author: authority.agentPubKey, + }, + ], [ "$return", { diff --git a/packages/tryorama/src/recipe/cannot_use_untrusted_external_id_attestor.test.ts b/packages/tryorama/src/recipe/cannot_use_untrusted_external_id_attestor.test.ts new file mode 100644 index 0000000..a6f01e6 --- /dev/null +++ b/packages/tryorama/src/recipe/cannot_use_untrusted_external_id_attestor.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "vitest"; +import { runScenario } from "@holochain/tryorama"; + +import { setupPlayer } from "../utils/setup-happ.js"; +import { encodeHashToBase64, fakeAgentPubKey } from "@holochain/client"; + +test("Cannot use untrusted external id attestor", async () => { + await runScenario(async (scenario) => { + const [alice, aliceCoordinators] = await setupPlayer(scenario); + + // Recipe that simply returns an ExternalIdAttestation via + // `GetLatestCallerExternalId` + const externalIdRecipeRecord = + await aliceCoordinators.usernameRegistry.createRecipe({ + trusted_authors: [await fakeAgentPubKey(0)], + arguments: [], + instructions: [ + [ + "$return", + { + type: "GetLatestCallerExternalId", + trusted_author: await fakeAgentPubKey(0), + }, + ], + ], + }); + + // An ExternalIdAttestation that wasn't created by the trusted author + const externalIdAttestationRecord = + await aliceCoordinators.usernameRegistry.createExternalIdAttestation({ + internal_pubkey: alice.agentPubKey, + external_id: "whatever", + display_name: "whatever", + request_id: "1234", + }); + + // Cannot specify untrusted attestation in execution + await expect( + aliceCoordinators.usernameRegistry.createRecipeExecution({ + recipe_ah: externalIdRecipeRecord.signed_action.hashed.hash, + arguments: [], + instruction_executions: [ + { + GetLatestCallerExternalId: { + attestation_ah: + externalIdAttestationRecord.signed_action.hashed.hash, + }, + }, + ], + output: JSON.stringify({ + agent_pubkey: encodeHashToBase64(alice.agentPubKey), + external_id: "whatever", + display_name: "whatever", + }), + }) + ).rejects.toSatisfy((err: Error) => + err.message.includes("ExternalIdAttestation is by untrusted author") + ); + }); +}); diff --git a/packages/types/src/types/RecipeInstruction.ts b/packages/types/src/types/RecipeInstruction.ts index 57a999e..f32817b 100644 --- a/packages/types/src/types/RecipeInstruction.ts +++ b/packages/types/src/types/RecipeInstruction.ts @@ -1,3 +1,4 @@ +import { AgentPubKey } from "@holochain/client"; // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { JqInstructionArgumentNames } from "./JqInstructionArgumentNames"; @@ -6,5 +7,5 @@ export type RecipeInstruction = | { type: "GetLatestDocWithIdentifier"; var_name: string } | { type: "Jq"; input_var_names: JqInstructionArgumentNames; program: string } | { type: "GetDocsListedByVar"; var_name: string } - | { type: "GetLatestCallerExternalId" } + | { type: "GetLatestCallerExternalId"; trusted_author: AgentPubKey } | { type: "GetCallerAgentPublicKey" };