Skip to content

Commit

Permalink
fix: validate doc author in recipe (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
8e8b2c authored Aug 2, 2024
1 parent fd8d453 commit 9ecf37f
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 29 deletions.
162 changes: 160 additions & 2 deletions crates/holoom_dna_tests/src/tests/username_registry/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use holochain::conductor::api::error::ConductorApiResult;
use holoom_types::{
recipe::{
ExecuteRecipePayload, JqInstructionArgumentNames, Recipe, RecipeArgument,
RecipeArgumentType, RecipeExecution, RecipeInstruction,
RecipeArgumentType, RecipeExecution, RecipeInstruction, RecipeInstructionExecution,
},
ExternalIdAttestation, OracleDocument,
};
Expand Down Expand Up @@ -158,7 +158,7 @@ async fn can_execute_basic_recipe() {
.await
.unwrap();

// Make both agents know recipe
// Make sure both agents know recipe
setup.consistency().await;

let authority_execution_record: Record = setup
Expand Down Expand Up @@ -200,4 +200,162 @@ async fn can_execute_basic_recipe() {
alice_execution.output,
String::from("{\"share\":0.8,\"msg\":\"Hello some-user-2\"}")
);

// Alice's untrusted document isn't able to censor the outcome

let _alice_foo_name_list_record: Record = setup
.alice_call(
"username_registry",
"create_oracle_document",
OracleDocument {
name: "foo".into(),
json_data: "[\"foo/5678\"]".into(),
},
)
.await
.unwrap();

let alice_execution_record2: Record = setup
.alice_call(
"username_registry",
"execute_recipe",
ExecuteRecipePayload {
recipe_ah: recipe_record.action_address().clone(),
arguments: vec![RecipeArgument::String {
value: "Hello".into(),
}],
},
)
.await
.unwrap();

let alice_execution2: RecipeExecution =
deserialize_record_entry(alice_execution_record2).unwrap();
// Result should be unchanged, rather than the dishonestly attempted alteration of:
// { "share": 1, "msg": "Hello some-user-2" }
assert_eq!(
alice_execution2.output,
String::from("{\"share\":0.8,\"msg\":\"Hello some-user-2\"}")
);
}

#[tokio::test(flavor = "multi_thread")]
async fn cannot_use_untrusted_docs() {
let setup = TestSetup::authority_only().await;

let single_doc_recipe_record: Record = setup
.authority_call(
"username_registry",
"create_recipe",
Recipe {
trusted_authors: vec![fake_agent_pub_key(0)],
arguments: vec![],
instructions: vec![
(
"doc_name".into(),
RecipeInstruction::Constant {
value: "\"foo\"".into(),
},
),
(
"$return".into(),
RecipeInstruction::GetLatestDocWithIdentifier {
var_name: "doc_name".into(),
},
),
],
},
)
.await
.unwrap();

let doc_record: Record = setup
.authority_call(
"username_registry",
"create_oracle_document",
OracleDocument {
name: "foo".into(),
json_data: "\"suspicious\"".into(),
},
)
.await
.unwrap();

let result: ConductorApiResult<Record> = setup
.authority_call(
"username_registry",
"create_recipe_execution",
RecipeExecution {
recipe_ah: single_doc_recipe_record.action_address().clone(),
arguments: Vec::new(),
instruction_executions: vec![
RecipeInstructionExecution::Constant,
RecipeInstructionExecution::GetLatestDocWithIdentifier {
doc_ah: doc_record.action_address().clone(),
},
],
output: "\"suspicious\"".into(),
},
)
.await;
match result {
Ok(_) => {
panic!("Execution should be invalid");
}
Err(err) => {
assert!(err.to_string().contains("Untrusted author"))
}
}

let doc_list_recipe_record: Record = setup
.authority_call(
"username_registry",
"create_recipe",
Recipe {
trusted_authors: vec![fake_agent_pub_key(0)],
arguments: vec![],
instructions: vec![
(
"doc_names".into(),
RecipeInstruction::Constant {
value: "[\"foo\"]".into(),
},
),
(
"$return".into(),
RecipeInstruction::GetDocsListedByVar {
var_name: "doc_names".into(),
},
),
],
},
)
.await
.unwrap();

let result: ConductorApiResult<Record> = setup
.authority_call(
"username_registry",
"create_recipe_execution",
RecipeExecution {
recipe_ah: doc_list_recipe_record.action_address().clone(),
arguments: Vec::new(),
instruction_executions: vec![
RecipeInstructionExecution::Constant,
RecipeInstructionExecution::GetDocsListedByVar {
doc_ahs: vec![doc_record.action_address().clone()],
},
],
output: "[\"suspicious\"]".into(),
},
)
.await;
match result {
Ok(_) => {
panic!("Execution should be invalid");
}
Err(err) => {
assert!(err.to_string().contains("Untrusted author"))
}
}
}
20 changes: 14 additions & 6 deletions crates/username_registry_coordinator/src/oracle_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ pub fn create_oracle_document(oracle_document: OracleDocument) -> ExternResult<R

Ok(record)
}
#[hdk_extern]
pub fn get_latest_oracle_document_ah_for_name(name: String) -> ExternResult<Option<ActionHash>> {
pub fn get_latest_oracle_document_ah_for_name(
name: String,
trusted_authors: &[AgentPubKey],
) -> ExternResult<Option<ActionHash>> {
let base_address = hash_identifier(name)?;
let mut links = get_links(
GetLinksInputBuilder::try_new(base_address, LinkTypes::NameToOracleDocument)?.build(),
)?;
links.sort_by_key(|link| link.timestamp);
let Some(link) = links.pop() else {
let Some(link) = links
.into_iter()
.filter(|link| trusted_authors.contains(&link.author))
.last()
else {
return Ok(None);
};
let action_hash = ActionHash::try_from(link.target).map_err(|_| {
Expand All @@ -54,9 +60,11 @@ pub fn get_oracle_document_link_ahs_for_name(name: String) -> ExternResult<Vec<A
Ok(action_hashes)
}

#[hdk_extern]
pub fn get_latest_oracle_document_for_name(name: String) -> ExternResult<Option<Record>> {
let Some(action_hash) = get_latest_oracle_document_ah_for_name(name)? else {
pub fn get_latest_oracle_document_for_name(
name: String,
trusted_authors: &[AgentPubKey],
) -> ExternResult<Option<Record>> {
let Some(action_hash) = get_latest_oracle_document_ah_for_name(name, trusted_authors)? else {
return Ok(None);
};
get(action_hash, GetOptions::network())
Expand Down
26 changes: 15 additions & 11 deletions crates/username_registry_coordinator/src/recipe_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ pub fn execute_recipe(payload: ExecuteRecipePayload) -> ExternResult<Record> {
&var_name
))));
};
get_latest_oracle_document_ah_for_name(identifier.as_ref().clone())?.ok_or(
wasm_error!(WasmErrorInner::Guest(format!(
"No OracleDocument for identifier '{}'",
&identifier
))),
)
get_latest_oracle_document_ah_for_name(
identifier.as_ref().clone(),
&recipe.trusted_authors,
)?
.ok_or(wasm_error!(WasmErrorInner::Guest(
format!("No OracleDocument for identifier '{}'", &identifier)
)))
})
.collect::<ExternResult<Vec<_>>>()?;
let doc_vals = doc_ahs
Expand Down Expand Up @@ -154,11 +155,14 @@ pub fn execute_recipe(payload: ExecuteRecipePayload) -> ExternResult<Record> {
&var_name
))));
};
let doc_record = get_latest_oracle_document_for_name(identifier.as_ref().clone())?
.ok_or(wasm_error!(WasmErrorInner::Guest(format!(
"No OracleDocument found for identifier '{}'",
identifier
))))?;
let doc_record = get_latest_oracle_document_for_name(
identifier.as_ref().clone(),
&recipe.trusted_authors,
)?
.ok_or(wasm_error!(WasmErrorInner::Guest(format!(
"No OracleDocument found for identifier '{}'",
identifier
))))?;
let doc_ah = doc_record.action_address().clone();
let doc: OracleDocument = deserialize_record_entry(doc_record)?;
let val = parse_single_json(&doc.json_data)?;
Expand Down
28 changes: 18 additions & 10 deletions crates/username_registry_validation/src/recipe_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,18 @@ pub fn validate_create_recipe_execution(
}
}

let docs = doc_ahs
.iter()
.map(|doc_ah| {
let doc_record = must_get_valid_record(doc_ah.clone())?;
let doc: OracleDocument = deserialize_record_entry(doc_record)?;
Ok(doc)
// let val = parse_single_json(&doc.json_data)?;
// Ok(val)
})
.collect::<ExternResult<Vec<_>>>()?;
let mut docs = Vec::new();
for doc_ah in doc_ahs {
let doc_record = must_get_valid_record(doc_ah.clone())?;
if !recipe
.trusted_authors
.contains(doc_record.action().author())
{
return Ok(ValidateCallbackResult::Invalid("Untrusted author".into()));
}
let doc: OracleDocument = deserialize_record_entry(doc_record)?;
docs.push(doc);
}

let actual_names: Vec<String> = docs.iter().map(|doc| doc.name.clone()).collect();
if expected_names != actual_names {
Expand Down Expand Up @@ -161,6 +163,12 @@ pub fn validate_create_recipe_execution(
};
let expected_name = name.as_ref().clone();
let doc_record = must_get_valid_record(doc_ah)?;
if !recipe
.trusted_authors
.contains(doc_record.action().author())
{
return Ok(ValidateCallbackResult::Invalid("Untrusted author".into()));
}
let doc: OracleDocument = deserialize_record_entry(doc_record)?;

if doc.name != expected_name {
Expand Down

0 comments on commit 9ecf37f

Please sign in to comment.