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