Skip to content

Commit 4995402

Browse files
committed
Merge branch 'nm/simplified-association-state' into 04-03-association_log_verification
2 parents c9bcab9 + 092b147 commit 4995402

File tree

10 files changed

+1076
-11
lines changed

10 files changed

+1076
-11
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xmtp_id/Cargo.toml

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
[package]
2+
edition = "2021"
23
name = "xmtp_id"
34
version = "0.1.0"
4-
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
async-trait.workspace = true
10+
chrono.workspace = true
11+
futures.workspace = true
12+
hex.workspace = true
913
log.workspace = true
10-
tracing.workspace = true
11-
thiserror.workspace = true
12-
xmtp_cryptography.workspace = true
13-
xmtp_mls.workspace = true
14-
xmtp_proto.workspace = true
15-
openmls_traits.workspace = true
1614
openmls.workspace = true
1715
openmls_basic_credential.workspace = true
1816
openmls_rust_crypto.workspace = true
17+
openmls_traits.workspace = true
1918
prost.workspace = true
20-
chrono.workspace = true
19+
rand.workspace = true
2120
serde.workspace = true
22-
async-trait.workspace = true
23-
futures.workspace = true
24-
21+
sha2 = "0.10.8"
22+
thiserror.workspace = true
23+
tracing.workspace = true
24+
xmtp_cryptography.workspace = true
25+
xmtp_mls.workspace = true
26+
xmtp_proto.workspace = true
+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
use super::hashes::generate_xid;
2+
use super::member::{Member, MemberIdentifier, MemberKind};
3+
use super::signature::{Signature, SignatureError, SignatureKind};
4+
use super::state::AssociationState;
5+
6+
use thiserror::Error;
7+
8+
// const ALLOWED_CREATE_ENTITY_ROLES: [EntityRole; 2] = [EntityRole::LegacyKey, EntityRole::Address];
9+
10+
#[derive(Debug, Error, PartialEq)]
11+
pub enum AssociationError {
12+
#[error("Error creating association {0}")]
13+
Generic(String),
14+
#[error("Multiple create operations detected")]
15+
MultipleCreate,
16+
#[error("XID not yet created")]
17+
NotCreated,
18+
#[error("Signature validation failed {0}")]
19+
Signature(#[from] SignatureError),
20+
#[error("Missing existing member")]
21+
MissingExistingMember,
22+
#[error("Legacy key is only allowed to be associated using a legacy signature with nonce 0")]
23+
LegacySignatureReuse,
24+
#[error("The new member identifier does not match the signer")]
25+
NewMemberIdSignatureMismatch,
26+
#[error("Signature not allowed for role {0:?} {1:?}")]
27+
SignatureNotAllowed(MemberKind, SignatureKind),
28+
#[error("Replay detected")]
29+
Replay,
30+
}
31+
32+
pub trait IdentityAction {
33+
fn update_state(
34+
&self,
35+
existing_state: Option<AssociationState>,
36+
) -> Result<AssociationState, AssociationError>;
37+
fn signatures(&self) -> Vec<Vec<u8>>;
38+
fn replay_check(&self, state: &AssociationState) -> Result<(), AssociationError> {
39+
let signatures = self.signatures();
40+
for signature in signatures {
41+
if state.has_seen(&signature) {
42+
return Err(AssociationError::Replay);
43+
}
44+
}
45+
46+
Ok(())
47+
}
48+
}
49+
50+
pub struct CreateInbox {
51+
pub nonce: u64,
52+
pub account_address: String,
53+
pub initial_address_signature: Box<dyn Signature>,
54+
}
55+
56+
impl IdentityAction for CreateInbox {
57+
fn update_state(
58+
&self,
59+
existing_state: Option<AssociationState>,
60+
) -> Result<AssociationState, AssociationError> {
61+
if existing_state.is_some() {
62+
return Err(AssociationError::MultipleCreate);
63+
}
64+
65+
let account_address = self.account_address.clone();
66+
let recovered_signer = self.initial_address_signature.recover_signer()?;
67+
if recovered_signer.ne(&MemberIdentifier::Address(account_address.clone())) {
68+
return Err(AssociationError::MissingExistingMember);
69+
}
70+
71+
Ok(AssociationState::new(account_address, self.nonce))
72+
}
73+
74+
fn signatures(&self) -> Vec<Vec<u8>> {
75+
vec![self.initial_address_signature.bytes()]
76+
}
77+
}
78+
79+
pub struct AddAssociation {
80+
pub client_timestamp_ns: u64,
81+
pub new_member_signature: Box<dyn Signature>,
82+
pub new_member_identifier: MemberIdentifier,
83+
pub existing_member_signature: Box<dyn Signature>,
84+
}
85+
86+
impl IdentityAction for AddAssociation {
87+
fn update_state(
88+
&self,
89+
maybe_existing_state: Option<AssociationState>,
90+
) -> Result<AssociationState, AssociationError> {
91+
let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
92+
self.replay_check(&existing_state)?;
93+
94+
let new_member_address = self.new_member_signature.recover_signer()?;
95+
if new_member_address.ne(&self.new_member_identifier) {
96+
return Err(AssociationError::NewMemberIdSignatureMismatch);
97+
}
98+
99+
let existing_member_identifier = self.existing_member_signature.recover_signer()?;
100+
let recovery_address = existing_state.recovery_address();
101+
102+
if new_member_address.ne(&self.new_member_identifier) {
103+
return Err(AssociationError::Generic(
104+
"new member identifier does not match signature".to_string(),
105+
));
106+
}
107+
108+
// You cannot add yourself
109+
if new_member_address == existing_member_identifier {
110+
return Err(AssociationError::Generic("tried to add self".to_string()));
111+
}
112+
113+
// Only allow LegacyDelegated signatures on XIDs with a nonce of 0
114+
// Otherwise the client should use the regular wallet signature to create
115+
if self.new_member_signature.signature_kind() == SignatureKind::LegacyDelegated {
116+
if existing_state
117+
.xid()
118+
.ne(&generate_xid(&existing_member_identifier.to_string(), &0))
119+
{
120+
return Err(AssociationError::LegacySignatureReuse);
121+
}
122+
}
123+
124+
// Make sure that the signature type lines up with the role
125+
if !allowed_signature_for_kind(
126+
&self.new_member_identifier.kind(),
127+
&self.new_member_signature.signature_kind(),
128+
) {
129+
return Err(AssociationError::SignatureNotAllowed(
130+
self.new_member_identifier.kind(),
131+
self.new_member_signature.signature_kind(),
132+
));
133+
}
134+
135+
let existing_member = existing_state.get(&existing_member_identifier);
136+
137+
let existing_entity_id = match existing_member {
138+
// If there is an existing member of the XID, use that member's ID
139+
Some(member) => member.identifier,
140+
None => {
141+
let recovery_identifier = MemberIdentifier::Address(recovery_address.clone());
142+
// Check if it is a signature from the recovery address, which is allowed to add members
143+
if existing_member_identifier.ne(&recovery_identifier) {
144+
return Err(AssociationError::MissingExistingMember);
145+
}
146+
// BUT, the recovery address has to be used with a real wallet signature, can't be delegated
147+
if self.existing_member_signature.signature_kind() == SignatureKind::LegacyDelegated
148+
{
149+
return Err(AssociationError::LegacySignatureReuse);
150+
}
151+
// If it is a real wallet signature, then it is allowed to add members
152+
recovery_identifier
153+
}
154+
};
155+
156+
let new_member = Member::new(new_member_address, Some(existing_entity_id));
157+
158+
println!("Adding new entity to state {:?}", &new_member);
159+
160+
Ok(existing_state.add(new_member))
161+
}
162+
163+
fn signatures(&self) -> Vec<Vec<u8>> {
164+
vec![
165+
self.existing_member_signature.bytes(),
166+
self.new_member_signature.bytes(),
167+
]
168+
}
169+
}
170+
171+
pub struct RevokeAssociation {
172+
pub client_timestamp_ns: u64,
173+
pub recovery_address_signature: Box<dyn Signature>,
174+
pub revoked_member: MemberIdentifier,
175+
}
176+
177+
impl IdentityAction for RevokeAssociation {
178+
fn update_state(
179+
&self,
180+
maybe_existing_state: Option<AssociationState>,
181+
) -> Result<AssociationState, AssociationError> {
182+
let existing_state = maybe_existing_state.ok_or(AssociationError::NotCreated)?;
183+
self.replay_check(&existing_state)?;
184+
185+
if self.recovery_address_signature.signature_kind() == SignatureKind::LegacyDelegated {
186+
return Err(AssociationError::SignatureNotAllowed(
187+
MemberKind::Address,
188+
SignatureKind::LegacyDelegated,
189+
));
190+
}
191+
// Don't need to check for replay here since revocation is idempotent
192+
let recovery_signer = self.recovery_address_signature.recover_signer()?;
193+
// Make sure there is a recovery address set on the state
194+
let state_recovery_address = existing_state.recovery_address();
195+
196+
// Ensure this message is signed by the recovery address
197+
if recovery_signer.ne(&MemberIdentifier::Address(state_recovery_address.clone())) {
198+
return Err(AssociationError::MissingExistingMember);
199+
}
200+
201+
let installations_to_remove: Vec<Member> = existing_state
202+
.members_by_parent(&self.revoked_member)
203+
.into_iter()
204+
// Only remove children if they are installations
205+
.filter(|child| child.kind() == MemberKind::Installation)
206+
.collect();
207+
208+
// Actually apply the revocation to the parent
209+
let new_state = existing_state.remove(&self.revoked_member);
210+
211+
Ok(installations_to_remove
212+
.iter()
213+
.fold(new_state, |state, installation| {
214+
state.remove(&installation.identifier)
215+
}))
216+
}
217+
218+
fn signatures(&self) -> Vec<Vec<u8>> {
219+
vec![self.recovery_address_signature.bytes()]
220+
}
221+
}
222+
223+
pub enum Action {
224+
CreateInbox(CreateInbox),
225+
AddAssociation(AddAssociation),
226+
RevokeAssociation(RevokeAssociation),
227+
}
228+
229+
impl IdentityAction for Action {
230+
fn update_state(
231+
&self,
232+
existing_state: Option<AssociationState>,
233+
) -> Result<AssociationState, AssociationError> {
234+
match self {
235+
Action::CreateInbox(event) => event.update_state(existing_state),
236+
Action::AddAssociation(event) => event.update_state(existing_state),
237+
Action::RevokeAssociation(event) => event.update_state(existing_state),
238+
}
239+
}
240+
241+
fn signatures(&self) -> Vec<Vec<u8>> {
242+
match self {
243+
Action::CreateInbox(event) => event.signatures(),
244+
Action::AddAssociation(event) => event.signatures(),
245+
Action::RevokeAssociation(event) => event.signatures(),
246+
}
247+
}
248+
}
249+
250+
pub struct IdentityUpdate {
251+
pub actions: Vec<Action>,
252+
}
253+
254+
impl IdentityUpdate {
255+
pub fn new(actions: Vec<Action>) -> Self {
256+
Self { actions }
257+
}
258+
}
259+
260+
impl IdentityAction for IdentityUpdate {
261+
fn update_state(
262+
&self,
263+
existing_state: Option<AssociationState>,
264+
) -> Result<AssociationState, AssociationError> {
265+
let mut state = existing_state.clone();
266+
for action in &self.actions {
267+
state = Some(action.update_state(state)?);
268+
}
269+
270+
let new_state = state.ok_or(AssociationError::NotCreated)?;
271+
272+
// After all the updates in the LogEntry have been processed, add the list of signatures to the state
273+
// so that the signatures can not be re-used in subsequent updates
274+
Ok(new_state.add_seen_signatures(self.signatures()))
275+
}
276+
277+
fn signatures(&self) -> Vec<Vec<u8>> {
278+
self.actions
279+
.iter()
280+
.flat_map(|action| action.signatures())
281+
.collect()
282+
}
283+
}
284+
285+
// Ensure that the type of signature matches the new entity's role.
286+
pub fn allowed_signature_for_kind(role: &MemberKind, signature_kind: &SignatureKind) -> bool {
287+
match role {
288+
MemberKind::Address => match signature_kind {
289+
SignatureKind::Erc191 => true,
290+
SignatureKind::Erc1271 => true,
291+
SignatureKind::InstallationKey => false,
292+
SignatureKind::LegacyDelegated => true,
293+
},
294+
MemberKind::Installation => match signature_kind {
295+
SignatureKind::Erc191 => false,
296+
SignatureKind::Erc1271 => false,
297+
SignatureKind::InstallationKey => true,
298+
SignatureKind::LegacyDelegated => false,
299+
},
300+
}
301+
}

xmtp_id/src/associations/hashes.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use sha2::{Digest, Sha256};
2+
3+
pub fn sha256_string(input: String) -> String {
4+
let mut hasher = Sha256::new();
5+
hasher.update(input.as_bytes());
6+
let result = hasher.finalize();
7+
format!("{:x}", result)
8+
}
9+
10+
pub fn generate_xid(account_address: &String, nonce: &u64) -> String {
11+
sha256_string(format!("{}{}", account_address, nonce))
12+
}

0 commit comments

Comments
 (0)