Skip to content

Commit

Permalink
feat: establish authority trust at point of use (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
8e8b2c authored Sep 2, 2024
1 parent 2f607e2 commit 3e9e1c9
Show file tree
Hide file tree
Showing 65 changed files with 524 additions and 417 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Provides several micro-service-like nodeJS applications that can be attached to

## The Authority Agent

Each Holoom network is intialised with an authority agent specified in the network's DNA properties. The role of the authority agent is to maintain a registry of unique usernames, and is expected to be run on a server maintained by the instigator of the network. The authority agent is considered to be a partially trusted node in that users can validate its correct execution, but cannot prevent its downtime or censorship decisions.
Any agent can offer to act as an authority, whether it be on username uniqueness or ownership of an external ID. Where or not an authority is respected is determined at point of use. E.g. If a user were an interested in executing a recipe for a specific EVM signing offer, they would only be interested in external IDs attested by authorities specified in the trust list of that recipe.

## Testing

Expand Down
25 changes: 25 additions & 0 deletions crates/holoom_types/src/external_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct SendExternalIdAttestationRequestPayload {
pub request_id: String,
pub code_verifier: String,
pub code: String,
#[ts(type = "AgentPubKey")]
pub authority: AgentPubKey,
}

#[derive(Serialize, Deserialize, Debug, TS)]
Expand Down Expand Up @@ -47,3 +49,26 @@ pub struct RejectExternalIdRequestPayload {
pub requestor: AgentPubKey,
pub reason: String,
}

/// Input to `get_external_id_attestations_for_agent`
#[derive(Serialize, Deserialize, Debug, TS)]
#[ts(export)]
pub struct GetExternalIdAttestationsForAgentPayload {
/// The agent whose is the object of the attestations you wish to retrieve
#[ts(type = "AgentPubKey")]
pub agent_pubkey: AgentPubKey,
/// The authorities whose attestations you respect.
#[ts(type = "AgentPubKey[]")]
pub trusted_authorities: Vec<AgentPubKey>,
}

/// Input to `get_attestation_for_external_id`
#[derive(Serialize, Deserialize, Debug, TS)]
#[ts(export)]
pub struct GetAttestationForExternalIdPayload {
/// The external ID for which to want a corresponding attestation
pub external_id: String,
/// The authorities whose attestations you respect.
#[ts(type = "AgentPubKey[]")]
pub trusted_authorities: Vec<AgentPubKey>,
}
6 changes: 0 additions & 6 deletions crates/holoom_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,3 @@ pub enum RemoteHoloomSignal {
#[derive(Serialize, Deserialize, Debug, Clone, SerializedBytes, TS)]
#[ts(export)]
pub struct SignableBytes(pub Vec<u8>);

#[dna_properties]
#[derive(Clone)]
pub struct HoloomDnaProperties {
pub authority_agent: String,
}
23 changes: 23 additions & 0 deletions crates/holoom_types/src/username.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,26 @@ pub struct SignedUsername {
#[ts(type = "AgentPubKey")]
pub signer: AgentPubKey,
}

/// The input to `sign_username_and_request_attestation`
#[derive(Serialize, Deserialize, Debug, TS)]
#[ts(export)]
pub struct SignUsernameAndRequestAttestationInput {
/// The username for which you want a corresponding attestation
pub username: String,
/// The authorities whose attestations you respect.
#[ts(type = "AgentPubKey")]
pub authority: AgentPubKey,
}

/// The input to `get_username_attestation_for_agent`
#[derive(Serialize, Deserialize, Debug, TS)]
#[ts(export)]
pub struct GetUsernameAttestationForAgentPayload {
/// The agent whose is the object of the attestations you wish to retrieve
#[ts(type = "AgentPubKey")]
pub agent: AgentPubKey,
/// The authorities whose attestations you respect.
#[ts(type = "AgentPubKey[]")]
pub trusted_authorities: Vec<AgentPubKey>,
}
22 changes: 20 additions & 2 deletions crates/username_registry_coordinator/src/evm_signing_offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ use jaq_wrapper::{parse_single_json, Val};
use username_registry_integrity::{EntryTypes, LinkTypes};
use username_registry_utils::{deserialize_record_entry, hash_evm_address, hash_identifier};

/// To be called once by agents that intend to respond to EVM signature requests (as specified by
/// their authored signing offers). Adds a `CapGrant` for ingesting such requests.
#[hdk_extern]
pub fn evm_signature_provider_setup(_: ()) -> ExternResult<()> {
let zome_name = zome_info()?.name;
let functions = BTreeSet::from([(
zome_name.clone(),
FunctionName("ingest_evm_signature_over_recipe_execution_request".into()),
)]);
create_cap_grant(CapGrantEntry {
tag: "".into(),
access: ().into(),
functions: GrantedFunctions::Listed(functions),
})?;

Ok(())
}

#[hdk_extern]
fn create_signed_evm_signing_offer(payload: CreateEvmSigningOfferPayload) -> ExternResult<Record> {
let action_hash = create_entry(EntryTypes::SignedEvmSigningOffer(
Expand Down Expand Up @@ -122,13 +140,13 @@ fn ingest_evm_signature_over_recipe_execution_request(
if output_vec.len() != signed_signing_offer.offer.u256_items.len() {
return Err(wasm_error!(WasmErrorInner::Guest(
"Unexpected u256 count for signing".into()
)))?;
)));
}
let u256_array = output_vec
.iter()
.zip(signed_signing_offer.offer.u256_items.into_iter())
.map(|pair| match pair {
(Val::Str(hex_string), EvmU256Item::Hex) => EvmU256::from_str_radix(&hex_string, 16)
(Val::Str(hex_string), EvmU256Item::Hex) => EvmU256::from_str_radix(hex_string, 16)
.map_err(|_| wasm_error!(WasmErrorInner::Guest("Invalid hex string".into()))),
(Val::Int(value), EvmU256Item::Uint) => {
if *value < 0 {
Expand Down
66 changes: 48 additions & 18 deletions crates/username_registry_coordinator/src/external_id_attestation.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
use hdk::prelude::*;
use holoom_types::{
ConfirmExternalIdRequestPayload, ExternalIdAttestation,
IngestExternalIdAttestationRequestPayload, LocalHoloomSignal, RejectExternalIdRequestPayload,
RemoteHoloomSignal, SendExternalIdAttestationRequestPayload,
ConfirmExternalIdRequestPayload, ExternalIdAttestation, GetAttestationForExternalIdPayload,
GetExternalIdAttestationsForAgentPayload, IngestExternalIdAttestationRequestPayload,
LocalHoloomSignal, RejectExternalIdRequestPayload, RemoteHoloomSignal,
SendExternalIdAttestationRequestPayload,
};
use username_registry_integrity::{EntryTypes, LinkTypes, UnitEntryTypes};
use username_registry_utils::{get_authority_agent, hash_identifier};
use username_registry_utils::hash_identifier;

/// To be called once by agents that intend to act as an authority of `ExternalIdAttestation`s.
/// Adds a `CapGrant` for ingesting username attestation requests.
#[hdk_extern]
pub fn external_id_authority_setup(_: ()) -> ExternResult<()> {
let zome_name = zome_info()?.name;
let functions = BTreeSet::from([(
zome_name.clone(),
FunctionName("ingest_external_id_attestation_request".into()),
)]);
create_cap_grant(CapGrantEntry {
tag: "".into(),
access: ().into(),
functions: GrantedFunctions::Listed(functions),
})?;

Ok(())
}

/// Forwards an external ID attestation request to the specified authority.
#[hdk_extern]
pub fn send_external_id_attestation_request(
payload: SendExternalIdAttestationRequestPayload,
) -> ExternResult<()> {
let authority_agent = payload.authority;
let payload = IngestExternalIdAttestationRequestPayload {
request_id: payload.request_id,
code_verifier: payload.code_verifier,
code: payload.code,
};

let authority_agent = get_authority_agent()?;

let zome_name = zome_info()?.name;
let fn_name = FunctionName::from("ingest_external_id_attestation_request");
let resp = call_remote(authority_agent, zome_name, fn_name, None, payload)?;
Expand Down Expand Up @@ -129,13 +148,18 @@ pub fn get_external_id_attestation(external_id_ah: ActionHash) -> ExternResult<O
get(external_id_ah, GetOptions::network())
}

/// Gets any `ExternalIdAttestation`s known for the specified agent, ignoring and attestation by
/// non-trusted authorities.
#[hdk_extern]
pub fn get_external_id_attestations_for_agent(
agent_pubkey: AgentPubKey,
payload: GetExternalIdAttestationsForAgentPayload,
) -> ExternResult<Vec<Record>> {
let mut links = get_links(
GetLinksInputBuilder::try_new(agent_pubkey, LinkTypes::AgentToExternalIdAttestation)?
.build(),
GetLinksInputBuilder::try_new(
payload.agent_pubkey,
LinkTypes::AgentToExternalIdAttestation,
)?
.build(),
)?;
links.sort_by_key(|link| link.timestamp);
let maybe_records = links
Expand All @@ -152,14 +176,23 @@ pub fn get_external_id_attestations_for_agent(
Ok(maybe_records.into_iter().flatten().collect())
}

/// Gets the `ExternalIdAttestation` for the specified external ID, provided that the attestation
/// exists and was attested by a trusted author.
#[hdk_extern]
pub fn get_attestation_for_external_id(external_id: String) -> ExternResult<Option<Record>> {
let base = hash_identifier(external_id)?;
pub fn get_attestation_for_external_id(
payload: GetAttestationForExternalIdPayload,
) -> ExternResult<Option<Record>> {
let base = hash_identifier(payload.external_id)?;
let mut links = get_links(
GetLinksInputBuilder::try_new(base, LinkTypes::ExternalIdToAttestation)?.build(),
)?;
links.sort_by_key(|link| link.timestamp);
let Some(link) = links.pop() else {

let Some(link) = links
.into_iter()
.filter(|link| payload.trusted_authorities.contains(&link.author))
.last()
else {
return Ok(None);
};
let action_hash = ActionHash::try_from(link.target).map_err(|_| {
Expand All @@ -170,13 +203,10 @@ pub fn get_attestation_for_external_id(external_id: String) -> ExternResult<Opti
get(action_hash, GetOptions::network())
}

/// Gets a `ExternalIdAttestation` `Record`s authored by the calling agent. This is intended to be
/// called by agents acting a authorities.
#[hdk_extern]
pub fn get_all_external_id_ahs(_: ()) -> ExternResult<Vec<ActionHash>> {
if agent_info()?.agent_initial_pubkey != get_authority_agent()? {
return Err(wasm_error!(WasmErrorInner::Guest(
"Only callable by authority agent".into()
)));
}
pub fn get_all_authored_external_id_ahs(_: ()) -> ExternResult<Vec<ActionHash>> {
let entry_type: EntryType = UnitEntryTypes::ExternalIdAttestation
.try_into()
.expect("ExternalIdAttestation is an entry type");
Expand Down
19 changes: 0 additions & 19 deletions crates/username_registry_coordinator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,11 @@ pub mod username_attestation;
pub mod wallet_attestation;
use hdk::prelude::*;
use holoom_types::{LocalHoloomSignal, RemoteHoloomSignal};
use username_registry_utils::get_authority_agent;

#[hdk_extern]
pub fn init(_: ()) -> ExternResult<InitCallbackResult> {
let authority_agent = get_authority_agent()?;
let my_pubkey = agent_info()?.agent_initial_pubkey;
let mut functions = BTreeSet::new();
let zome_name = zome_info()?.name;
if my_pubkey == authority_agent {
functions.insert((zome_name.clone(), "ingest_signed_username".into()));
functions.insert((
zome_name.clone(),
"ingest_external_id_attestation_request".into(),
));
functions.insert((
zome_name.clone(),
"ingest_evm_signature_over_recipe_execution_request".into(),
));
}
functions.insert((zome_name, "recv_remote_signal".into()));
create_cap_grant(CapGrantEntry {
tag: "".into(),
Expand Down Expand Up @@ -64,8 +50,3 @@ fn recv_remote_signal(signal_io: ExternIO) -> ExternResult<()> {

Ok(())
}

#[hdk_extern]
fn get_authority(_: ()) -> ExternResult<AgentPubKey> {
get_authority_agent()
}
12 changes: 9 additions & 3 deletions crates/username_registry_coordinator/src/recipe_execution.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::{collections::HashMap, rc::Rc};

use hdk::prelude::*;
use holoom_types::{recipe::*, ExternalIdAttestation, OracleDocument};
use holoom_types::{
recipe::*, ExternalIdAttestation, GetExternalIdAttestationsForAgentPayload, OracleDocument,
};
use indexmap::IndexMap;
use jaq_wrapper::{compile_filter, parse_single_json, run_filter, Val};
use username_registry_integrity::EntryTypes;
Expand Down Expand Up @@ -116,8 +118,12 @@ pub fn execute_recipe(payload: ExecuteRecipePayload) -> ExternResult<Record> {
(val, instruction_execution)
}
RecipeInstruction::GetLatestCallerExternalId => {
let mut attestation_records =
get_external_id_attestations_for_agent(agent_info()?.agent_initial_pubkey)?;
let mut attestation_records = get_external_id_attestations_for_agent(
GetExternalIdAttestationsForAgentPayload {
agent_pubkey: agent_info()?.agent_initial_pubkey,
trusted_authorities: recipe.trusted_authors.clone(),
},
)?;
let attestation_record =
attestation_records
.pop()
Expand Down
59 changes: 41 additions & 18 deletions crates/username_registry_coordinator/src/username_attestation.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
use hdk::prelude::*;
use holoom_types::{SignedUsername, UsernameAttestation};
use holoom_types::{
GetUsernameAttestationForAgentPayload, SignUsernameAndRequestAttestationInput, SignedUsername,
UsernameAttestation,
};
use username_registry_integrity::*;
use username_registry_utils::get_authority_agent;

/// To be called once by agents that intend to act as an authority of `UsernameAttestation`s. Adds
/// a `CapGrant` for ingesting username attestation requests.
#[hdk_extern]
pub fn username_authority_setup(_: ()) -> ExternResult<()> {
let zome_name = zome_info()?.name;
let functions = BTreeSet::from([(
zome_name.clone(),
FunctionName("ingest_signed_username".into()),
)]);
create_cap_grant(CapGrantEntry {
tag: "".into(),
access: ().into(),
functions: GrantedFunctions::Listed(functions),
})?;

Ok(())
}

#[hdk_extern]
pub fn create_username_attestation(
Expand Down Expand Up @@ -36,12 +56,18 @@ pub fn delete_username_attestation(
delete_entry(original_username_attestation_hash)
}
#[hdk_extern]
pub fn get_username_attestation_for_agent(agent: AgentPubKey) -> ExternResult<Option<Record>> {
pub fn get_username_attestation_for_agent(
payload: GetUsernameAttestationForAgentPayload,
) -> ExternResult<Option<Record>> {
let links = get_links(
GetLinksInputBuilder::try_new(agent, LinkTypes::AgentToUsernameAttestations)?.build(),
GetLinksInputBuilder::try_new(payload.agent, LinkTypes::AgentToUsernameAttestations)?
.build(),
)?;

match links.first() {
match links
.into_iter()
.find(|link| payload.trusted_authorities.contains(&link.author))
{
Some(l) => get(
ActionHash::try_from(l.clone().target).unwrap(),
GetOptions::network(),
Expand All @@ -60,14 +86,10 @@ pub fn does_agent_have_username(agent: AgentPubKey) -> ExternResult<bool> {
Ok(count > 0)
}

/// Returns all UsernameAttestation records authored by the calling agent. This method is intended
/// to be called by agents acting as authorities.
#[hdk_extern]
pub fn get_all_username_attestations(_: ()) -> ExternResult<Vec<Record>> {
let my_pubkey = agent_info()?.agent_initial_pubkey;
if my_pubkey != get_authority_agent()? {
return Err(wasm_error!(WasmErrorInner::Host(
"Only callable by authority agent".into()
)));
}
pub fn get_all_authored_username_attestations(_: ()) -> ExternResult<Vec<Record>> {
let username_attestation_type: EntryType = UnitEntryTypes::UsernameAttestation.try_into()?;
let filter = ChainQueryFilter::new()
.include_entries(true)
Expand All @@ -76,22 +98,23 @@ pub fn get_all_username_attestations(_: ()) -> ExternResult<Vec<Record>> {
}

/// Called by the user who wishes to register a username. Returns a UsernameAttestation Record.
/// It is left to the caller to specify the authority from whom they want an attestation.
#[hdk_extern]
pub fn sign_username_to_attest(username: String) -> ExternResult<Record> {
pub fn sign_username_and_request_attestation(
input: SignUsernameAndRequestAttestationInput,
) -> ExternResult<Record> {
// TODO: devise scheme akin to signing a nonce
let my_pubkey = agent_info()?.agent_initial_pubkey;
let signature = sign(my_pubkey.clone(), &username)?;
let signature = sign(my_pubkey.clone(), &input.username)?;
let payload = SignedUsername {
username,
username: input.username,
signature,
signer: my_pubkey,
};

let authority_agent = get_authority_agent()?;

let zome_name = zome_info()?.name;
let fn_name = FunctionName::from("ingest_signed_username");
let resp = call_remote(authority_agent, zome_name, fn_name, None, payload)?;
let resp = call_remote(input.authority, zome_name, fn_name, None, payload)?;
match resp {
ZomeCallResponse::Ok(result) => result.decode().map_err(|err| wasm_error!(err)),
ZomeCallResponse::NetworkError(err) => Err(wasm_error!(WasmErrorInner::Guest(format!(
Expand Down
Loading

0 comments on commit 3e9e1c9

Please sign in to comment.