Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: verifiably locate signing offers against signer address (#74) #77

Merged
merged 1 commit into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/holoom_dna_tests/src/tests/username_registry/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod external_id_attestation;
mod oracle;
mod recipe;
mod user_metadata;
mod username_attestation;
mod wallet_attestation;
mod external_id_attestation;
16 changes: 13 additions & 3 deletions crates/holoom_types/src/evm_signing_offer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ pub enum EvmU256Item {
HoloAgent,
}

#[hdk_entry_helper]
#[derive(Clone, PartialEq, TS)]
#[derive(Clone, PartialEq, TS, Serialize, Deserialize, Debug)]
#[ts(export)]
pub struct EvmSigningOffer {
#[ts(type = "ActionHash")]
pub recipe_ah: ActionHash,
pub u256_items: Vec<EvmU256Item>,
}

#[hdk_entry_helper]
#[derive(Clone, PartialEq, TS)]
#[ts(export)]
pub struct SignedEvmSigningOffer {
#[ts(type = "Uint8Array")]
pub signer: EvmAddress,
#[ts(type = "[Uint8Array, Uint8Array, number]")]
pub signature: EvmSignature,
pub offer: EvmSigningOffer,
}

#[derive(Serialize, Deserialize, Debug, TS)]
#[ts(export)]
pub struct CreateEvmSigningOfferPayload {
pub identifier: String,
pub evm_signing_offer: EvmSigningOffer,
pub signed_offer: SignedEvmSigningOffer,
}

#[derive(Serialize, Deserialize, Debug, TS)]
Expand Down
47 changes: 36 additions & 11 deletions crates/username_registry_coordinator/src/evm_signing_offer.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
use hdk::prelude::*;
use holoom_types::{
evm_signing_offer::{
CreateEvmSigningOfferPayload, EvmSignatureOverRecipeExecutionRequest, EvmSigningOffer,
EvmU256, EvmU256Item, RejectEvmSignatureOverRecipeExecutionRequestPayload,
ResolveEvmSignatureOverRecipeExecutionRequestPayload,
CreateEvmSigningOfferPayload, EvmSignatureOverRecipeExecutionRequest, EvmU256, EvmU256Item,
RejectEvmSignatureOverRecipeExecutionRequestPayload,
ResolveEvmSignatureOverRecipeExecutionRequestPayload, SignedEvmSigningOffer,
},
recipe::RecipeExecution,
LocalHoloomSignal, RemoteHoloomSignal,
EvmAddress, LocalHoloomSignal, RemoteHoloomSignal,
};
use jaq_wrapper::{parse_single_json, Val};
use username_registry_integrity::{EntryTypes, LinkTypes};
use username_registry_utils::{deserialize_record_entry, hash_identifier};
use username_registry_utils::{deserialize_record_entry, hash_evm_address, hash_identifier};

#[hdk_extern]
fn create_evm_signing_offer(payload: CreateEvmSigningOfferPayload) -> ExternResult<Record> {
let action_hash = create_entry(EntryTypes::EvmSigningOffer(payload.evm_signing_offer))?;
fn create_signed_evm_signing_offer(payload: CreateEvmSigningOfferPayload) -> ExternResult<Record> {
let action_hash = create_entry(EntryTypes::SignedEvmSigningOffer(
payload.signed_offer.clone(),
))?;
create_link(
hash_identifier(payload.identifier)?,
action_hash.clone(),
LinkTypes::NameToSigningOffer,
(),
)?;
create_link(
hash_evm_address(payload.signed_offer.signer)?,
action_hash.clone(),
LinkTypes::EvmAddressToSigningOffer,
(),
)?;
get(action_hash, GetOptions::network())?.ok_or(wasm_error!(WasmErrorInner::Guest(
"Couldn't get newly created EvmSigningOffer Record".into()
)))
Expand All @@ -44,6 +52,22 @@ pub fn get_latest_evm_signing_offer_ah_for_name(name: String) -> ExternResult<Op
Ok(Some(action_hash))
}

#[hdk_extern]
pub fn get_signing_offer_ahs_for_evm_address(
evm_address: EvmAddress,
) -> ExternResult<Vec<ActionHash>> {
let base_address = hash_evm_address(evm_address)?;
let mut links = get_links(
GetLinksInputBuilder::try_new(base_address, LinkTypes::EvmAddressToSigningOffer)?.build(),
)?;
links.sort_by_key(|link| link.timestamp);
let ahs = links
.into_iter()
.filter_map(|link| ActionHash::try_from(link.target).ok())
.collect();
Ok(ahs)
}

#[hdk_extern]
fn send_request_for_evm_signature_over_recipe_execution(
request: EvmSignatureOverRecipeExecutionRequest,
Expand Down Expand Up @@ -78,13 +102,14 @@ fn ingest_evm_signature_over_recipe_execution_request(
let signing_offer_record = get(payload.signing_offer_ah, GetOptions::network())?.ok_or(
wasm_error!(WasmErrorInner::Guest("EvmSigningOffer not found".into())),
)?;
let signing_offer: EvmSigningOffer = deserialize_record_entry(signing_offer_record)?;
let signed_signing_offer: SignedEvmSigningOffer =
deserialize_record_entry(signing_offer_record)?;
let recipe_execution_record = get(payload.recipe_execution_ah, GetOptions::network())?.ok_or(
wasm_error!(WasmErrorInner::Guest("RecipeExecution not found".into())),
)?;
let recipe_execution: RecipeExecution = deserialize_record_entry(recipe_execution_record)?;

if recipe_execution.recipe_ah != signing_offer.recipe_ah {
if recipe_execution.recipe_ah != signed_signing_offer.offer.recipe_ah {
return Err(wasm_error!(WasmErrorInner::Guest(
"Executed Recipe doesn't match signing offer".into()
)));
Expand All @@ -94,14 +119,14 @@ fn ingest_evm_signature_over_recipe_execution_request(
"Recipe output isn't an array".into()
)))?;
};
if output_vec.len() != signing_offer.u256_items.len() {
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(signing_offer.u256_items.into_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)
.map_err(|_| wasm_error!(WasmErrorInner::Guest("Invalid hex string".into()))),
Expand Down
14 changes: 8 additions & 6 deletions crates/username_registry_integrity/src/entry_types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use hdi::prelude::*;
use holoom_types::{
evm_signing_offer::EvmSigningOffer,
evm_signing_offer::SignedEvmSigningOffer,
recipe::{Recipe, RecipeExecution},
ExternalIdAttestation, OracleDocument, UsernameAttestation, WalletAttestation,
};
Expand All @@ -17,7 +17,7 @@ pub enum EntryTypes {
OracleDocument(OracleDocument),
Recipe(Recipe),
RecipeExecution(RecipeExecution),
EvmSigningOffer(EvmSigningOffer),
SignedEvmSigningOffer(SignedEvmSigningOffer),
}

impl EntryTypes {
Expand Down Expand Up @@ -52,10 +52,12 @@ impl EntryTypes {
EntryCreationAction::Create(action),
recipe_execution,
),
EntryTypes::EvmSigningOffer(evm_signing_offer) => validate_create_evm_signing_offer(
EntryCreationAction::Create(action),
evm_signing_offer,
),
EntryTypes::SignedEvmSigningOffer(evm_signing_offer) => {
validate_create_signed_evm_signing_offer(
EntryCreationAction::Create(action),
evm_signing_offer,
)
}
}
}
}
18 changes: 18 additions & 0 deletions crates/username_registry_integrity/src/link_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub enum LinkTypes {
RelateOracleDocumentName,
NameToRecipe,
NameToSigningOffer,
EvmAddressToSigningOffer,
}

impl LinkTypes {
Expand Down Expand Up @@ -80,6 +81,14 @@ impl LinkTypes {
target_address,
tag,
),
LinkTypes::EvmAddressToSigningOffer => {
validate_create_link_evm_address_to_signing_offer(
action,
base_address,
target_address,
tag,
)
}
}
}

Expand Down Expand Up @@ -164,6 +173,15 @@ impl LinkTypes {
target_address,
tag,
),
LinkTypes::EvmAddressToSigningOffer => {
validate_delete_link_evm_address_to_signing_offer(
action,
original_action,
base_address,
target_address,
tag,
)
}
}
}
}
11 changes: 10 additions & 1 deletion crates/username_registry_utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use hdi::prelude::*;
use holoom_types::HoloomDnaProperties;
use holoom_types::{EvmAddress, HoloomDnaProperties};

pub fn deserialize_record_entry<O>(record: Record) -> ExternResult<O>
where
Expand All @@ -24,6 +24,15 @@ pub fn hash_identifier(identifier: String) -> ExternResult<EntryHash> {
hash_entry(Entry::App(AppEntryBytes(bytes)))
}

pub fn hash_evm_address(evm_address: EvmAddress) -> ExternResult<EntryHash> {
#[derive(SerializedBytes, Serialize, Debug, Deserialize)]
struct SerializableEvmAddress(EvmAddress);

let bytes = SerializedBytes::try_from(SerializableEvmAddress(evm_address))
.map_err(|err| wasm_error!(err))?;
hash_entry(Entry::App(AppEntryBytes(bytes)))
}

pub fn get_authority_agent() -> ExternResult<AgentPubKey> {
let dna_props = HoloomDnaProperties::try_from_dna_properties()?;
AgentPubKey::try_from(dna_props.authority_agent).map_err(|_| {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use hdi::prelude::*;
use holoom_types::evm_signing_offer::SignedEvmSigningOffer;
use username_registry_utils::{deserialize_record_entry, hash_evm_address};

pub fn validate_create_link_evm_address_to_signing_offer(
action: CreateLink,
base_address: AnyLinkableHash,
target_address: AnyLinkableHash,
_tag: LinkTag,
) -> ExternResult<ValidateCallbackResult> {
let Ok(target_address) = ActionHash::try_from(target_address) else {
return Ok(ValidateCallbackResult::Invalid(
"target_address must be an ActionHash".into(),
));
};
let record = must_get_valid_record(target_address)?;
if &action.author != record.action().author() {
return Ok(ValidateCallbackResult::Invalid(
"link and target must have same author".into(),
));
}

let Ok(signed_offer) = deserialize_record_entry::<SignedEvmSigningOffer>(record) else {
return Ok(ValidateCallbackResult::Invalid(
"target_address must be a SignedEvmSigningOffer".into(),
));
};

if base_address != hash_evm_address(signed_offer.signer)?.into() {
return Ok(ValidateCallbackResult::Invalid(
"base_address must be hash of evm signer".into(),
));
}

Ok(ValidateCallbackResult::Valid)
}
pub fn validate_delete_link_evm_address_to_signing_offer(
_action: DeleteLink,
_original_action: CreateLink,
_base_address: AnyLinkableHash,
_target_address: AnyLinkableHash,
_tag: LinkTag,
) -> ExternResult<ValidateCallbackResult> {
Ok(ValidateCallbackResult::Invalid(
"Cannot delete EvmAddressToSigningOffer links".into(),
))
}
51 changes: 46 additions & 5 deletions crates/username_registry_validation/src/evm_signing_offer.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
use hdi::prelude::*;
use holoom_types::evm_signing_offer::EvmSigningOffer;
use holoom_types::{
evm_signing_offer::{EvmU256Item, SignedEvmSigningOffer},
recipe::Recipe,
};
use username_registry_utils::deserialize_record_entry;

pub fn validate_create_evm_signing_offer(
pub fn validate_create_signed_evm_signing_offer(
_action: EntryCreationAction,
_evm_signing_offer: EvmSigningOffer,
signed_evm_signing_offer: SignedEvmSigningOffer,
) -> ExternResult<ValidateCallbackResult> {
// TODO: check recipe exists
Ok(ValidateCallbackResult::Valid)
let recipe_record = must_get_valid_record(signed_evm_signing_offer.offer.recipe_ah.clone())?;
if deserialize_record_entry::<Recipe>(recipe_record).is_err() {
// This check seems brittle. See https://github.com/holochain-open-dev/holoom/issues/69
return Ok(ValidateCallbackResult::Invalid(
"recipe_ah doesn't point to a Recipe".into(),
));
}

// Ensure a stable byte order
#[derive(Serialize, Debug)]
struct OrderedSigningOffer(ActionHash, Vec<EvmU256Item>);
let ordered_offer = OrderedSigningOffer(
signed_evm_signing_offer.offer.recipe_ah,
signed_evm_signing_offer.offer.u256_items,
);

let message = ExternIO::encode(ordered_offer)
.expect("EvmSigningOffer implements Serialize")
.into_vec();

match signed_evm_signing_offer
.signature
.recover_address_from_msg(&message)
{
Ok(recovered_address) => {
if recovered_address == signed_evm_signing_offer.signer {
Ok(ValidateCallbackResult::Valid)
} else {
Ok(ValidateCallbackResult::Invalid(format!(
"Expected to recover {} from signature, but instead recovered {}",
signed_evm_signing_offer.signer.to_checksum(None),
recovered_address.to_checksum(None)
)))
}
}
Err(_) => Ok(ValidateCallbackResult::Invalid(
"Invalid signature over wallet binding message".into(),
)),
}
}
2 changes: 2 additions & 0 deletions crates/username_registry_validation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ pub mod name_to_recipe;
pub use name_to_recipe::*;
pub mod name_to_evm_signing_offer;
pub use name_to_evm_signing_offer::*;
pub mod evm_address_to_signing_offer;
pub use evm_address_to_signing_offer::*;
12 changes: 11 additions & 1 deletion packages/authority/src/evm-bytes-signer/bytes-signer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { bytesToBigInt, encodePacked, Hex, hexToBytes, keccak256 } from "viem";
import { privateKeyToAccount, PrivateKeyAccount } from "viem/accounts";
import { formatEvmSignature } from "./utils.js";
import { EvmSigningOffer } from "@holoom/types";
import { encode } from "@msgpack/msgpack";

export class BytesSigner {
readonly account: PrivateKeyAccount;
Expand All @@ -10,7 +12,15 @@ export class BytesSigner {
this.address = hexToBytes(this.account.address);
}

async sign(u256_array: Uint8Array[]) {
async sign_offer(offer: EvmSigningOffer) {
console.log("signing offer", offer);
// offer is encoded as a tuple to give stable ordering
const raw = encode([offer.recipe_ah, offer.u256_items]);
const hex = await this.account.signMessage({ message: { raw } });
return formatEvmSignature(hex);
}

async sign_u256_array(u256_array: Uint8Array[]) {
console.log("signing u256_array", u256_array);
const packed = encodePacked(
["uint256[]"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export class EvmBytesSignerClient {
async handleEvmSignatureRequested(signal: EvmSignatureRequested) {
console.log("handleEvmSignatureRequested");
try {
const signature = await this.bytesSigner.sign(signal.u256_array);
const signature = await this.bytesSigner.sign_u256_array(
signal.u256_array
);
// Will node complain about this orphaned promise?
this.confirmRequest({
request_id: signal.request_id,
Expand Down
Loading
Loading