duniter_documents/blockchain/v10/documents/
identity.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 Identity 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 IDENTITY_REGEX: Regex = Regex::new(
28        "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\nUniqueID: (?P<uid>[[:alnum:]_-]+)\nTimestamp: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n$"
29    ).unwrap();
30}
31
32/// Wrap an Identity document.
33///
34/// Must be created by parsing a text document or using a builder.
35#[derive(Debug, Clone, PartialEq, Hash)]
36pub struct IdentityDocument {
37    /// Document as text.
38    ///
39    /// Is used to check signatures, and other values
40    /// must be extracted from it.
41    text: String,
42
43    /// Currency.
44    currency: String,
45    /// Unique ID
46    username: String,
47    /// Blockstamp
48    blockstamp: Blockstamp,
49    /// Document issuer (there should be only one).
50    issuers: Vec<ed25519::PublicKey>,
51    /// Document signature (there should be only one).
52    signatures: Vec<ed25519::Signature>,
53}
54
55impl IdentityDocument {
56    /// Unique ID
57    pub fn username(&self) -> &str {
58        &self.username
59    }
60}
61
62impl Document for IdentityDocument {
63    type PublicKey = ed25519::PublicKey;
64    type CurrencyType = str;
65
66    fn version(&self) -> u16 {
67        10
68    }
69
70    fn currency(&self) -> &str {
71        &self.currency
72    }
73
74    fn issuers(&self) -> &Vec<ed25519::PublicKey> {
75        &self.issuers
76    }
77
78    fn signatures(&self) -> &Vec<ed25519::Signature> {
79        &self.signatures
80    }
81
82    fn as_bytes(&self) -> &[u8] {
83        self.as_text().as_bytes()
84    }
85}
86
87impl TextDocument for IdentityDocument {
88    fn as_text(&self) -> &str {
89        &self.text
90    }
91
92    fn generate_compact_text(&self) -> String {
93        format!(
94            "{issuer}:{signature}:{blockstamp}:{username}",
95            issuer = self.issuers[0],
96            signature = self.signatures[0],
97            blockstamp = self.blockstamp,
98            username = self.username,
99        )
100    }
101}
102
103impl IntoSpecializedDocument<BlockchainProtocol> for IdentityDocument {
104    fn into_specialized(self) -> BlockchainProtocol {
105        BlockchainProtocol::V10(Box::new(V10Document::Identity(self)))
106    }
107}
108
109/// Identity document builder.
110#[derive(Debug, Copy, Clone)]
111pub struct IdentityDocumentBuilder<'a> {
112    /// Document currency.
113    pub currency: &'a str,
114    /// Identity unique id.
115    pub username: &'a str,
116    /// Reference blockstamp.
117    pub blockstamp: &'a Blockstamp,
118    /// Document/identity issuer.
119    pub issuer: &'a ed25519::PublicKey,
120}
121
122impl<'a> IdentityDocumentBuilder<'a> {
123    fn build_with_text_and_sigs(
124        self,
125        text: String,
126        signatures: Vec<ed25519::Signature>,
127    ) -> IdentityDocument {
128        IdentityDocument {
129            text,
130            currency: self.currency.to_string(),
131            username: self.username.to_string(),
132            blockstamp: *self.blockstamp,
133            issuers: vec![*self.issuer],
134            signatures,
135        }
136    }
137}
138
139impl<'a> DocumentBuilder for IdentityDocumentBuilder<'a> {
140    type Document = IdentityDocument;
141    type PrivateKey = ed25519::PrivateKey;
142
143    fn build_with_signature(&self, signatures: Vec<ed25519::Signature>) -> IdentityDocument {
144        self.build_with_text_and_sigs(self.generate_text(), signatures)
145    }
146
147    fn build_and_sign(&self, private_keys: Vec<ed25519::PrivateKey>) -> IdentityDocument {
148        let (text, signatures) = self.build_signed_text(private_keys);
149        self.build_with_text_and_sigs(text, signatures)
150    }
151}
152
153impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> {
154    fn generate_text(&self) -> String {
155        format!(
156            "Version: 10
157Type: Identity
158Currency: {currency}
159Issuer: {issuer}
160UniqueID: {username}
161Timestamp: {blockstamp}
162",
163            currency = self.currency,
164            issuer = self.issuer,
165            username = self.username,
166            blockstamp = self.blockstamp
167        )
168    }
169}
170
171/// Identity document parser
172#[derive(Debug, Clone, Copy)]
173pub struct IdentityDocumentParser;
174
175impl StandardTextDocumentParser for IdentityDocumentParser {
176    fn parse_standard(
177        doc: &str,
178        body: &str,
179        currency: &str,
180        signatures: Vec<ed25519::Signature>,
181    ) -> Result<V10Document, V10DocumentParsingError> {
182        if let Some(caps) = IDENTITY_REGEX.captures(body) {
183            let issuer = &caps["issuer"];
184            let uid = &caps["uid"];
185            let blockstamp = &caps["blockstamp"];
186
187            // Regex match so should not fail.
188            // TODO : Test it anyway
189            let issuer = ed25519::PublicKey::from_base58(issuer).unwrap();
190            let blockstamp = Blockstamp::from_string(blockstamp).unwrap();
191
192            Ok(V10Document::Identity(IdentityDocument {
193                text: doc.to_owned(),
194                currency: currency.to_owned(),
195                username: uid.to_owned(),
196                blockstamp,
197                issuers: vec![issuer],
198                signatures,
199            }))
200        } else {
201            Err(V10DocumentParsingError::InvalidInnerFormat(
202                "Identity".to_string(),
203            ))
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use duniter_crypto::keys::{PrivateKey, PublicKey, Signature};
212    use blockchain::{Document, VerificationResult};
213
214    #[test]
215    fn generate_real_document() {
216        let pubkey = ed25519::PublicKey::from_base58(
217            "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV",
218        ).unwrap();
219
220        let prikey = ed25519::PrivateKey::from_base58(
221            "468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5G\
222             iERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7",
223        ).unwrap();
224
225        let sig = ed25519::Signature::from_base64(
226            "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGM\
227             MmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==",
228        ).unwrap();
229
230        let block = Blockstamp::from_string(
231            "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
232        ).unwrap();
233
234        let builder = IdentityDocumentBuilder {
235            currency: "duniter_unit_test_currency",
236            username: "tic",
237            blockstamp: &block,
238            issuer: &pubkey,
239        };
240
241        assert_eq!(
242            builder.build_with_signature(vec![sig]).verify_signatures(),
243            VerificationResult::Valid()
244        );
245        assert_eq!(
246            builder.build_and_sign(vec![prikey]).verify_signatures(),
247            VerificationResult::Valid()
248        );
249    }
250
251    #[test]
252    fn identity_standard_regex() {
253        assert!(IDENTITY_REGEX.is_match(
254            "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
255UniqueID: tic
256Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
257"
258        ));
259    }
260
261    #[test]
262    fn parse_identity_document() {
263        let doc = "Version: 10
264Type: Identity
265Currency: duniter_unit_test_currency
266Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
267UniqueID: tic
268Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
269";
270
271        let body = "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV
272UniqueID: tic
273Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
274";
275
276        let currency = "duniter_unit_test_currency";
277
278        let signatures = vec![Signature::from_base64(
279"1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg=="
280        ).unwrap(),];
281
282        let doc = IdentityDocumentParser::parse_standard(doc, body, currency, signatures).unwrap();
283        if let V10Document::Identity(doc) = doc {
284            println!("Doc : {:?}", doc);
285            assert_eq!(doc.verify_signatures(), VerificationResult::Valid())
286        } else {
287            panic!("Wrong document type");
288        }
289    }
290}