duniter_documents/blockchain/v10/documents/
membership.rs1use duniter_crypto::keys::{PublicKey, ed25519};
19use regex::Regex;
20
21use Blockstamp;
22use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument};
23use blockchain::v10::documents::{StandardTextDocumentParser, TextDocument, TextDocumentBuilder,
24 V10Document, V10DocumentParsingError};
25
26lazy_static! {
27 static ref MEMBERSHIP_REGEX: Regex = Regex::new(
28 "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\n\
29 Block: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n\
30 Membership: (?P<membership>(IN|OUT))\n\
31 UserID: (?P<ity_user>[[:alnum:]_-]+)\n\
32 CertTS: (?P<ity_block>[0-9]+-[0-9A-F]{64})\n$"
33 ).unwrap();
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Hash)]
38pub enum MembershipType {
39 In(),
41 Out(),
43}
44
45#[derive(Debug, Clone, PartialEq, Hash)]
49pub struct MembershipDocument {
50 text: String,
54
55 currency: String,
57 issuers: Vec<ed25519::PublicKey>,
59 blockstamp: Blockstamp,
61 membership: MembershipType,
63 identity_username: String,
65 identity_blockstamp: Blockstamp,
67 signatures: Vec<ed25519::Signature>,
69}
70
71impl MembershipDocument {
72 pub fn membership(&self) -> MembershipType {
74 self.membership
75 }
76
77 pub fn identity_username(&self) -> &str {
79 &self.identity_username
80 }
81}
82
83impl Document for MembershipDocument {
84 type PublicKey = ed25519::PublicKey;
85 type CurrencyType = str;
86
87 fn version(&self) -> u16 {
88 10
89 }
90
91 fn currency(&self) -> &str {
92 &self.currency
93 }
94
95 fn issuers(&self) -> &Vec<ed25519::PublicKey> {
96 &self.issuers
97 }
98
99 fn signatures(&self) -> &Vec<ed25519::Signature> {
100 &self.signatures
101 }
102
103 fn as_bytes(&self) -> &[u8] {
104 self.as_text().as_bytes()
105 }
106}
107
108impl TextDocument for MembershipDocument {
109 fn as_text(&self) -> &str {
110 &self.text
111 }
112
113 fn generate_compact_text(&self) -> String {
114 format!(
115 "{issuer}:{signature}:{blockstamp}:{idty_blockstamp}:{username}",
116 issuer = self.issuers[0],
117 signature = self.signatures[0],
118 blockstamp = self.blockstamp,
119 idty_blockstamp = self.identity_blockstamp,
120 username = self.identity_username,
121 )
122 }
123}
124
125impl IntoSpecializedDocument<BlockchainProtocol> for MembershipDocument {
126 fn into_specialized(self) -> BlockchainProtocol {
127 BlockchainProtocol::V10(Box::new(V10Document::Membership(self)))
128 }
129}
130
131#[derive(Debug, Copy, Clone)]
133pub struct MembershipDocumentBuilder<'a> {
134 pub currency: &'a str,
136 pub issuer: &'a ed25519::PublicKey,
138 pub blockstamp: &'a Blockstamp,
140 pub membership: MembershipType,
142 pub identity_username: &'a str,
144 pub identity_blockstamp: &'a Blockstamp,
146}
147
148impl<'a> MembershipDocumentBuilder<'a> {
149 fn build_with_text_and_sigs(
150 self,
151 text: String,
152 signatures: Vec<ed25519::Signature>,
153 ) -> MembershipDocument {
154 MembershipDocument {
155 text,
156 currency: self.currency.to_string(),
157 issuers: vec![*self.issuer],
158 blockstamp: *self.blockstamp,
159 membership: self.membership,
160 identity_username: self.identity_username.to_string(),
161 identity_blockstamp: *self.identity_blockstamp,
162 signatures,
163 }
164 }
165}
166
167impl<'a> DocumentBuilder for MembershipDocumentBuilder<'a> {
168 type Document = MembershipDocument;
169 type PrivateKey = ed25519::PrivateKey;
170
171 fn build_with_signature(&self, signatures: Vec<ed25519::Signature>) -> MembershipDocument {
172 self.build_with_text_and_sigs(self.generate_text(), signatures)
173 }
174
175 fn build_and_sign(&self, private_keys: Vec<ed25519::PrivateKey>) -> MembershipDocument {
176 let (text, signatures) = self.build_signed_text(private_keys);
177 self.build_with_text_and_sigs(text, signatures)
178 }
179}
180
181impl<'a> TextDocumentBuilder for MembershipDocumentBuilder<'a> {
182 fn generate_text(&self) -> String {
183 format!(
184 "Version: 10
185Type: Membership
186Currency: {currency}
187Issuer: {issuer}
188Block: {blockstamp}
189Membership: {membership}
190UserID: {username}
191CertTS: {ity_blockstamp}
192",
193 currency = self.currency,
194 issuer = self.issuer,
195 blockstamp = self.blockstamp,
196 membership = match self.membership {
197 MembershipType::In() => "IN",
198 MembershipType::Out() => "OUT",
199 },
200 username = self.identity_username,
201 ity_blockstamp = self.identity_blockstamp,
202 )
203 }
204}
205
206#[derive(Debug, Clone, Copy)]
208pub struct MembershipDocumentParser;
209
210impl StandardTextDocumentParser for MembershipDocumentParser {
211 fn parse_standard(
212 doc: &str,
213 body: &str,
214 currency: &str,
215 signatures: Vec<ed25519::Signature>,
216 ) -> Result<V10Document, V10DocumentParsingError> {
217 if let Some(caps) = MEMBERSHIP_REGEX.captures(body) {
218 let issuer = &caps["issuer"];
219
220 let blockstamp = &caps["blockstamp"];
221 let membership = &caps["membership"];
222 let username = &caps["ity_user"];
223 let ity_block = &caps["ity_block"];
224
225 let issuer = ed25519::PublicKey::from_base58(issuer).unwrap();
228 let blockstamp = Blockstamp::from_string(blockstamp).unwrap();
229 let membership = match membership {
230 "IN" => MembershipType::In(),
231 "OUT" => MembershipType::Out(),
232 _ => panic!("Invalid membership type {}", membership),
233 };
234
235 let ity_block = Blockstamp::from_string(ity_block).unwrap();
236
237 Ok(V10Document::Membership(MembershipDocument {
238 text: doc.to_owned(),
239 issuers: vec![issuer],
240 currency: currency.to_owned(),
241 blockstamp,
242 membership,
243 identity_username: username.to_owned(),
244 identity_blockstamp: ity_block,
245 signatures,
246 }))
247 } else {
248 Err(V10DocumentParsingError::InvalidInnerFormat(
249 "Membership".to_string(),
250 ))
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use duniter_crypto::keys::{PrivateKey, PublicKey, Signature};
259 use blockchain::VerificationResult;
260
261 #[test]
262 fn generate_real_document() {
263 let pubkey = ed25519::PublicKey::from_base58(
264 "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV",
265 ).unwrap();
266
267 let prikey = ed25519::PrivateKey::from_base58(
268 "468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5G\
269 iERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7",
270 ).unwrap();
271
272 let sig = ed25519::Signature::from_base64(
273 "s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkEl\
274 AyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==",
275 ).unwrap();
276
277 let block = Blockstamp::from_string(
278 "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
279 ).unwrap();
280
281 let builder = MembershipDocumentBuilder {
282 currency: "duniter_unit_test_currency",
283 issuer: &pubkey,
284 blockstamp: &block,
285 membership: MembershipType::In(),
286 identity_username: "tic",
287 identity_blockstamp: &block,
288 };
289
290 assert_eq!(
291 builder.build_with_signature(vec![sig]).verify_signatures(),
292 VerificationResult::Valid()
293 );
294 assert_eq!(
295 builder.build_and_sign(vec![prikey]).verify_signatures(),
296 VerificationResult::Valid()
297 );
298 }
299
300 #[test]
301 fn membership_standard_regex() {
302 assert!(MEMBERSHIP_REGEX.is_match(
303 "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
304Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
305Membership: IN
306UserID: tic
307CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
308"
309 ));
310 }
311
312 #[test]
313 fn membership_identity_document() {
314 let doc = "Version: 10
315Type: Membership
316Currency: duniter_unit_test_currency
317Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
318Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
319Membership: IN
320UserID: tic
321CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
322";
323
324 let body = "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
325Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
326Membership: IN
327UserID: tic
328CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
329";
330
331 let currency = "duniter_unit_test_currency";
332
333 let signatures = vec![Signature::from_base64(
334"s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw=="
335 ).unwrap(),];
336
337 let doc =
338 MembershipDocumentParser::parse_standard(doc, body, currency, signatures).unwrap();
339 if let V10Document::Membership(doc) = doc {
340 println!("Doc : {:?}", doc);
341 assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
342 assert_eq!(
343 doc.generate_compact_text(),
344 "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:\
345 s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==:\
346 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
347 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
348 tic"
349 );
350 } else {
351 panic!("Wrong document type");
352 }
353 }
354}