duniter_documents/blockchain/v10/documents/
membership.rs

1//  Copyright (C) 2018  The Duniter Project Developers.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as
5// published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Wrappers around Membership documents.
17
18use 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/// Type of a Membership.
37#[derive(Debug, Clone, Copy, PartialEq, Hash)]
38pub enum MembershipType {
39    /// The member wishes to opt-in.
40    In(),
41    /// The member wishes to opt-out.
42    Out(),
43}
44
45/// Wrap an Membership document.
46///
47/// Must be created by parsing a text document or using a builder.
48#[derive(Debug, Clone, PartialEq, Hash)]
49pub struct MembershipDocument {
50    /// Document as text.
51    ///
52    /// Is used to check signatures, and other values mut be extracted from it.
53    text: String,
54
55    /// Name of the currency.
56    currency: String,
57    /// Document issuer (there should be only one).
58    issuers: Vec<ed25519::PublicKey>,
59    /// Blockstamp
60    blockstamp: Blockstamp,
61    /// Membership message.
62    membership: MembershipType,
63    /// Identity to use for this public key.
64    identity_username: String,
65    /// Identity document blockstamp.
66    identity_blockstamp: Blockstamp,
67    /// Document signature (there should be only one).
68    signatures: Vec<ed25519::Signature>,
69}
70
71impl MembershipDocument {
72    /// Membership message.
73    pub fn membership(&self) -> MembershipType {
74        self.membership
75    }
76
77    /// Identity to use for this public key.
78    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/// Membership document builder.
132#[derive(Debug, Copy, Clone)]
133pub struct MembershipDocumentBuilder<'a> {
134    /// Document currency.
135    pub currency: &'a str,
136    /// Document/identity issuer.
137    pub issuer: &'a ed25519::PublicKey,
138    /// Reference blockstamp.
139    pub blockstamp: &'a Blockstamp,
140    /// Membership message.
141    pub membership: MembershipType,
142    /// Identity username.
143    pub identity_username: &'a str,
144    /// Identity document blockstamp.
145    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/// Membership document parser
207#[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            // Regex match so should not fail.
226            // TODO : Test it anyway
227            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}