duniter_documents/blockchain/v10/documents/
certification.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 Certification documents.
17
18use duniter_crypto::keys::{PublicKey, Signature, 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 CERTIFICATION_REGEX: Regex = Regex::new(
28        "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\n\
29         IdtyIssuer: (?P<target>[1-9A-Za-z][^OIl]{43,44})\n\
30         IdtyUniqueID: (?P<idty_uid>[[:alnum:]_-]+)\n\
31         IdtyTimestamp: (?P<idty_blockstamp>[0-9]+-[0-9A-F]{64})\n\
32         IdtySignature: (?P<idty_sig>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)\n\
33         CertTimestamp: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n$"
34    ).unwrap();
35}
36
37/// Wrap an Certification document.
38///
39/// Must be created by parsing a text document or using a builder.
40#[derive(Debug, Clone)]
41pub struct CertificationDocument {
42    /// Document as text.
43    ///
44    /// Is used to check signatures, and other values mut be extracted from it.
45    text: String,
46
47    /// Name of the currency.
48    currency: String,
49    /// Document issuer (there should be only one).
50    issuers: Vec<ed25519::PublicKey>,
51    /// issuer of target identity.
52    target: ed25519::PublicKey,
53    /// Username of target identity
54    identity_username: String,
55    /// Target Identity document blockstamp.
56    identity_blockstamp: Blockstamp,
57    /// Target Identity document signature.
58    identity_sig: ed25519::Signature,
59    /// Blockstamp
60    blockstamp: Blockstamp,
61    /// Document signature (there should be only one).
62    signatures: Vec<ed25519::Signature>,
63}
64
65impl CertificationDocument {
66    /// Username of target identity
67    pub fn identity_username(&self) -> &str {
68        &self.identity_username
69    }
70
71    /// Pubkey of source identity
72    pub fn source(&self) -> &ed25519::PublicKey {
73        &self.issuers[0]
74    }
75
76    /// Pubkey of target identity
77    pub fn target(&self) -> &ed25519::PublicKey {
78        &self.target
79    }
80}
81
82impl Document for CertificationDocument {
83    type PublicKey = ed25519::PublicKey;
84    type CurrencyType = str;
85
86    fn version(&self) -> u16 {
87        10
88    }
89
90    fn currency(&self) -> &str {
91        &self.currency
92    }
93
94    fn issuers(&self) -> &Vec<ed25519::PublicKey> {
95        &self.issuers
96    }
97
98    fn signatures(&self) -> &Vec<ed25519::Signature> {
99        &self.signatures
100    }
101
102    fn as_bytes(&self) -> &[u8] {
103        self.as_text().as_bytes()
104    }
105}
106
107impl TextDocument for CertificationDocument {
108    fn as_text(&self) -> &str {
109        &self.text
110    }
111
112    fn generate_compact_text(&self) -> String {
113        format!(
114            "{issuer}:{target}:{block_number}:{signature}",
115            issuer = self.issuers[0],
116            target = self.target,
117            block_number = self.blockstamp.id.0,
118            signature = self.signatures[0],
119        )
120    }
121}
122
123impl IntoSpecializedDocument<BlockchainProtocol> for CertificationDocument {
124    fn into_specialized(self) -> BlockchainProtocol {
125        BlockchainProtocol::V10(Box::new(V10Document::Certification(Box::new(self))))
126    }
127}
128
129/// Certification document builder.
130#[derive(Debug, Copy, Clone)]
131pub struct CertificationDocumentBuilder<'a> {
132    /// Document currency.
133    pub currency: &'a str,
134    /// Certification issuer (=source).
135    pub issuer: &'a ed25519::PublicKey,
136    /// Reference blockstamp.
137    pub blockstamp: &'a Blockstamp,
138    /// Pubkey of target identity.
139    pub target: &'a ed25519::PublicKey,
140    /// Username of target Identity.
141    pub identity_username: &'a str,
142    /// Blockstamp of target Identity.
143    pub identity_blockstamp: &'a Blockstamp,
144    /// Signature of target Identity.
145    pub identity_sig: &'a ed25519::Signature,
146}
147
148impl<'a> CertificationDocumentBuilder<'a> {
149    fn build_with_text_and_sigs(
150        self,
151        text: String,
152        signatures: Vec<ed25519::Signature>,
153    ) -> CertificationDocument {
154        CertificationDocument {
155            text,
156            currency: self.currency.to_string(),
157            issuers: vec![*self.issuer],
158            blockstamp: *self.blockstamp,
159            target: *self.target,
160            identity_username: self.identity_username.to_string(),
161            identity_blockstamp: *self.identity_blockstamp,
162            identity_sig: *self.identity_sig,
163            signatures,
164        }
165    }
166}
167
168impl<'a> DocumentBuilder for CertificationDocumentBuilder<'a> {
169    type Document = CertificationDocument;
170    type PrivateKey = ed25519::PrivateKey;
171
172    fn build_with_signature(&self, signatures: Vec<ed25519::Signature>) -> CertificationDocument {
173        self.build_with_text_and_sigs(self.generate_text(), signatures)
174    }
175
176    fn build_and_sign(&self, private_keys: Vec<ed25519::PrivateKey>) -> CertificationDocument {
177        let (text, signatures) = self.build_signed_text(private_keys);
178        self.build_with_text_and_sigs(text, signatures)
179    }
180}
181
182impl<'a> TextDocumentBuilder for CertificationDocumentBuilder<'a> {
183    fn generate_text(&self) -> String {
184        format!(
185            "Version: 10
186Type: Certification
187Currency: {currency}
188Issuer: {issuer}
189IdtyIssuer: {target}
190IdtyUniqueID: {idty_uid}
191IdtyTimestamp: {idty_blockstamp}
192IdtySignature: {idty_sig}
193CertTimestamp: {blockstamp}
194",
195            currency = self.currency,
196            issuer = self.issuer,
197            target = self.target,
198            idty_uid = self.identity_username,
199            idty_blockstamp = self.identity_blockstamp,
200            idty_sig = self.identity_sig,
201            blockstamp = self.blockstamp,
202        )
203    }
204}
205
206/// Certification document parser
207#[derive(Debug, Clone, Copy)]
208pub struct CertificationDocumentParser;
209
210impl StandardTextDocumentParser for CertificationDocumentParser {
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) = CERTIFICATION_REGEX.captures(body) {
218            let issuer = &caps["issuer"];
219            let target = &caps["target"];
220            let identity_username = &caps["idty_uid"];
221            let identity_blockstamp = &caps["idty_blockstamp"];
222            let identity_sig = &caps["idty_sig"];
223            let blockstamp = &caps["blockstamp"];
224
225            // Regex match so should not fail.
226            // TODO : Test it anyway
227            let issuer = ed25519::PublicKey::from_base58(issuer).unwrap();
228            let target = ed25519::PublicKey::from_base58(target).unwrap();
229            let identity_username = String::from(identity_username);
230            let identity_blockstamp = Blockstamp::from_string(identity_blockstamp).unwrap();
231            let identity_sig = ed25519::Signature::from_base64(identity_sig).unwrap();
232            let blockstamp = Blockstamp::from_string(blockstamp).unwrap();
233
234            Ok(V10Document::Certification(Box::new(
235                CertificationDocument {
236                    text: doc.to_owned(),
237                    issuers: vec![issuer],
238                    currency: currency.to_owned(),
239                    target,
240                    identity_username,
241                    identity_blockstamp,
242                    identity_sig,
243                    blockstamp,
244                    signatures,
245                },
246            )))
247        } else {
248            Err(V10DocumentParsingError::InvalidInnerFormat(
249                "Certification".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            "4tNQ7d9pj2Da5wUVoW9mFn7JjuPoowF977au8DdhEjVR",
265        ).unwrap();
266
267        let prikey = ed25519::PrivateKey::from_base58(
268            "3XGWuuU1dQ7zaYPzE76ATfY71STzRkbT3t4DE1bSjMhYje81XdJFeXVG9uMPi3oDeRTosT2dmBAFH8VydrAUWXRZ",
269        ).unwrap();
270
271        let sig = ed25519::Signature::from_base64(
272            "qfR6zqT1oJbqIsppOi64gC9yTtxb6g6XA9RYpulkq9ehMvqg2VYVigCbR0yVpqKFsnYiQTrnjgFuFRSJCJDfCw==",
273        ).unwrap();
274
275        let target = ed25519::PublicKey::from_base58(
276            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV",
277        ).unwrap();
278
279        let identity_blockstamp = Blockstamp::from_string(
280            "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
281        ).unwrap();
282
283        let identity_sig = ed25519::Signature::from_base64(
284            "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==",
285        ).unwrap();
286
287        let blockstamp = Blockstamp::from_string(
288            "36-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B865",
289        ).unwrap();
290
291        let builder = CertificationDocumentBuilder {
292            currency: "duniter_unit_test_currency",
293            issuer: &pubkey,
294            target: &target,
295            identity_username: "tic",
296            identity_blockstamp: &identity_blockstamp,
297            identity_sig: &identity_sig,
298            blockstamp: &blockstamp,
299        };
300
301        assert_eq!(
302            builder.build_with_signature(vec![sig]).verify_signatures(),
303            VerificationResult::Valid()
304        );
305
306        assert_eq!(
307            builder.build_and_sign(vec![prikey]).verify_signatures(),
308            VerificationResult::Valid()
309        );
310    }
311
312    #[test]
313    fn certification_standard_regex() {
314        assert!(CERTIFICATION_REGEX.is_match(
315            "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
316IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9
317IdtyUniqueID: fbarbut
318IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3
319IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ==
320CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD
321"
322        ));
323    }
324
325    #[test]
326    fn certification_document() {
327        let doc = "Version: 10
328Type: Certification
329Currency: g1
330Issuer: 2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT
331IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9
332IdtyUniqueID: fbarbut
333IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3
334IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ==
335CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD
336";
337
338        let body = "Issuer: 2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT
339IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9
340IdtyUniqueID: fbarbut
341IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3
342IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ==
343CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD
344";
345
346        let currency = "g1";
347
348        let signatures = vec![Signature::from_base64(
349"Hkps1QU4HxIcNXKT8YmprYTVByBhPP1U2tIM7Z8wENzLKIWAvQClkAvBE7pW9dnVa18sJIJhVZUcRrPAZfmjBA=="
350        ).unwrap(),];
351
352        let doc =
353            CertificationDocumentParser::parse_standard(doc, body, currency, signatures).unwrap();
354        if let V10Document::Certification(doc) = doc {
355            println!("Doc : {:?}", doc);
356            assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
357            assert_eq!(
358                doc.generate_compact_text(),
359                "2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT:\
360                7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9:99956:\
361                Hkps1QU4HxIcNXKT8YmprYTVByBhPP1U2tIM7Z8wENzLKIWAvQClkAvBE7pW9dnVa18sJIJhVZUcRrPAZfmjBA=="
362            );
363        } else {
364            panic!("Wrong document type");
365        }
366    }
367}