duniter_documents/blockchain/v10/documents/
revocation.rs1use 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#[derive(Debug, Clone)]
39pub struct RevocationDocument {
40 text: String,
44
45 currency: String,
47 issuers: Vec<ed25519::PublicKey>,
49 identity_username: String,
51 identity_blockstamp: Blockstamp,
53 identity_sig: ed25519::Signature,
55 signatures: Vec<ed25519::Signature>,
57}
58
59impl RevocationDocument {
60 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#[derive(Debug, Copy, Clone)]
113pub struct RevocationDocumentBuilder<'a> {
114 pub currency: &'a str,
116 pub issuer: &'a ed25519::PublicKey,
118 pub identity_username: &'a str,
120 pub identity_blockstamp: &'a Blockstamp,
122 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#[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 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}