crypto/
thread_ownership_resolution.rs1use heddle_object_model::object::thread_replication::{
5 ownership_claim::ThreadOwnershipClaim,
6 ownership_resolution::{FORMAT, ThreadOwnershipResolution},
7};
8use serde::{Deserialize, Serialize};
9
10use crate::{Ed25519Signer, Signer, thread_operation::Error};
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SignedOwnershipResolution {
14 pub canonical: Vec<u8>,
15 pub local_signature: Vec<u8>,
16 pub acceptance_signature: Vec<u8>,
17}
18impl SignedOwnershipResolution {
19 pub fn sign(
20 value: &ThreadOwnershipResolution,
21 local_owner: &impl Signer,
22 acceptor: &impl Signer,
23 ) -> Result<Self, Error> {
24 if local_owner.public_key() != value.local_owner
25 || acceptor.public_key() != value.accepting_publisher
26 {
27 return Err(Error::Publisher);
28 }
29 let canonical = value.encode()?;
30 let local_signature = local_owner.sign(&signing_bytes(&canonical))?;
31 let acceptance_signature = acceptor.sign(&signing_bytes(&canonical))?;
32 Ok(Self {
33 canonical,
34 local_signature,
35 acceptance_signature,
36 })
37 }
38 pub fn verify(
42 &self,
43 winning_claim: &ThreadOwnershipClaim,
44 ) -> Result<ThreadOwnershipResolution, Error> {
45 let value = ThreadOwnershipResolution::decode(&self.canonical)?;
46 if winning_claim.id()? != value.winning_claim
47 || winning_claim.prior_local_key != value.local_owner
48 || winning_claim.thread != value.thread
49 || winning_claim.account()? != value.account()?
50 {
51 return Err(Error::Publisher);
52 }
53 Ed25519Signer::verify_with_public_key(
54 &signing_bytes(&self.canonical),
55 &value.local_owner,
56 &self.local_signature,
57 )?;
58 Ed25519Signer::verify_with_public_key(
59 &signing_bytes(&self.canonical),
60 &value.accepting_publisher,
61 &self.acceptance_signature,
62 )?;
63 Ok(value)
64 }
65}
66pub fn signing_bytes(canonical: &[u8]) -> Vec<u8> {
67 let mut bytes = FORMAT.as_bytes().to_vec();
68 bytes.push(0);
69 bytes.extend_from_slice(canonical);
70 bytes
71}
72
73#[cfg(test)]
74mod tests {
75 use heddle_object_model::object::{
76 CollaborationActor, ContentHash,
77 thread_replication::{SourceAuthor, ownership_claim::ThreadOwnershipClaim},
78 };
79
80 use super::*;
81
82 fn fixture() -> (
83 ThreadOwnershipClaim,
84 ThreadOwnershipResolution,
85 Ed25519Signer,
86 Ed25519Signer,
87 ) {
88 let local = Ed25519Signer::from_seed(&[41; 32]).expect("local key");
89 let acceptor = Ed25519Signer::from_seed(&[42; 32]).expect("recipient key");
90 let spool = "00000000-0000-0000-0000-000000000022"
91 .parse()
92 .expect("UUID");
93 let acceptance = SourceAuthor::account(
94 spool,
95 CollaborationActor {
96 principal_id: "00000000-0000-0000-0000-000000000023"
97 .parse()
98 .expect("UUID"),
99 agent_id: None,
100 },
101 vec![46; 32],
102 )
103 .expect("account acceptance");
104 let claim = ThreadOwnershipClaim {
105 version: 1,
106 thread: ContentHash::from_bytes([43; 32]),
107 prior_local_key: local.public_key().try_into().expect("key"),
108 accepting_publisher: acceptor.public_key().try_into().expect("key"),
109 acceptance: acceptance.clone(),
110 source_frontier: [ContentHash::from_bytes([47; 32])].into(),
111 };
112 let winning_claim = claim.id().expect("claim id");
113 let resolution = ThreadOwnershipResolution {
114 version: 1,
115 spool,
116 thread: claim.thread,
117 winning_claim,
118 conflicting_claims: [winning_claim, ContentHash::from_bytes([48; 32])].into(),
119 frontier: [ContentHash::from_bytes([49; 32])].into(),
120 local_owner: local.public_key().try_into().expect("key"),
121 accepting_publisher: acceptor.public_key().try_into().expect("key"),
122 acceptance,
123 occurred_at_ms: 1,
124 };
125 (claim, resolution, local, acceptor)
126 }
127
128 #[test]
129 fn original_owner_and_fresh_recipient_must_both_sign_exact_resolution() {
130 let (claim, resolution, local, acceptor) = fixture();
131 let signed = SignedOwnershipResolution::sign(&resolution, &local, &acceptor)
132 .expect("both signatures");
133 assert_eq!(signed.verify(&claim).expect("verified"), resolution);
134 let mut missing_local = signed.clone();
135 missing_local.local_signature = vec![0; 64];
136 assert!(missing_local.verify(&claim).is_err());
137 let mut missing_acceptance = signed.clone();
138 missing_acceptance.acceptance_signature = vec![0; 64];
139 assert!(missing_acceptance.verify(&claim).is_err());
140 let mut changed_choice = signed.clone();
141 changed_choice.canonical = ThreadOwnershipResolution {
142 winning_claim: ContentHash::from_bytes([48; 32]),
143 ..resolution.clone()
144 }
145 .encode()
146 .expect("changed choice");
147 assert!(changed_choice.verify(&claim).is_err());
148 assert!(
149 Ed25519Signer::verify_with_public_key(
150 &signed.canonical,
151 &resolution.local_owner,
152 &signed.local_signature,
153 )
154 .is_err(),
155 "domain separation"
156 );
157 }
158
159 #[test]
160 fn recipient_key_is_independent_of_historical_claim_publisher() {
161 let (mut claim, mut resolution, local, _) = fixture();
162 let current_recipient = Ed25519Signer::from_seed(&[99; 32]).expect("current recipient");
163 claim.accepting_publisher = [42; 32];
164 resolution.winning_claim = claim.id().expect("changed claim");
165 resolution.conflicting_claims =
166 [resolution.winning_claim, ContentHash::from_bytes([48; 32])].into();
167 resolution.accepting_publisher = current_recipient.public_key().try_into().expect("key");
168 let signed = SignedOwnershipResolution::sign(&resolution, &local, ¤t_recipient)
169 .expect("current acceptance");
170 assert_eq!(signed.verify(&claim).expect("independent keys"), resolution);
171 }
172}