// Copyright (C) 2018 The Duniter Project Developers. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //! Provide wrappers around Duniter blockchain documents for protocol version 10. extern crate crypto; use self::crypto::digest::Digest; use duniter_crypto::keys::{Signature, ed25519}; use regex::Regex; use blockchain::{Document, DocumentBuilder, DocumentParser}; use blockchain::v10::documents::identity::IdentityDocumentParser; pub mod identity; pub mod membership; pub mod certification; pub mod revocation; pub mod transaction; pub mod block; pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder}; pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser}; pub use blockchain::v10::documents::certification::{CertificationDocument, CertificationDocumentParser}; pub use blockchain::v10::documents::revocation::{RevocationDocument, RevocationDocumentParser}; pub use blockchain::v10::documents::transaction::{TransactionDocument, TransactionDocumentBuilder, TransactionDocumentParser}; pub use blockchain::v10::documents::block::BlockDocument; // Use of lazy_static so the regex is only compiled at first use. lazy_static! { static ref DOCUMENT_REGEX: Regex = Regex::new( "^(?PVersion: (?P[0-9]+)\n\ Type: (?P[[:alpha:]]+)\n\ Currency: (?P[[:alnum:] _-]+)\n\ (?P(?:.*\n)+?))\ (?P([[:alnum:]+/=]+\n)*([[:alnum:]+/=]+))$" ).unwrap(); static ref SIGNATURES_REGEX: Regex = Regex::new("[[:alnum:]+/=]+\n?").unwrap(); } /// List of wrapped document types. /// /// > TODO Add wrapped types in enum variants. #[derive(Debug, Clone)] pub enum V10Document { /// Block document. Block(Box), /// Transaction document. Transaction(Box), /// Identity document. Identity(IdentityDocument), /// Membership document. Membership(MembershipDocument), /// Certification document. Certification(Box), /// Revocation document. Revocation(Box), } /// Trait for a V10 document. pub trait TextDocument: Document { /// Return document as text. fn as_text(&self) -> &str; /// Return sha256 hash of text document fn hash(&self, digest: &mut H) -> String { digest.input_str(self.as_text()); digest.result_str() } /// Return document as text with leading signatures. fn as_text_with_signatures(&self) -> String { let mut text = self.as_text().to_string(); for sig in self.signatures() { text = format!("{}{}\n", text, sig.to_base64()); } text } /// Generate document compact text. /// the compact format is the one used in the blocks. /// /// - Don't contains leading signatures /// - Contains line breaks on all line. fn generate_compact_text(&self) -> String; } /// Trait for a V10 document builder. pub trait TextDocumentBuilder: DocumentBuilder { /// Generate document text. /// /// - Don't contains leading signatures /// - Contains line breaks on all line. fn generate_text(&self) -> String; /// Generate final document with signatures, and also return them in an array. /// /// Returns : /// /// - Text without signatures /// - Signatures fn build_signed_text( &self, private_keys: Vec, ) -> (String, Vec) { use duniter_crypto::keys::PrivateKey; let text = self.generate_text(); let signatures: Vec<_> = { let text_bytes = text.as_bytes(); private_keys .iter() .map(|key| key.sign(text_bytes)) .collect() }; (text, signatures) } } /// List of possible errors while parsing. #[derive(Debug, Clone)] pub enum V10DocumentParsingError { /// The given source don't have a valid document format. InvalidWrapperFormat(), /// The given source don't have a valid specific document format (document type). InvalidInnerFormat(String), /// Type fields contains an unknown document type. UnknownDocumentType(String), } /// V10 Documents in separated parts #[derive(Debug, Clone)] pub struct V10DocumentParts { /// Whole document in text pub doc: String, /// Payload pub body: String, /// Currency pub currency: String, /// Signatures pub signatures: Vec, } trait StandardTextDocumentParser { fn parse_standard( doc: &str, body: &str, currency: &str, signatures: Vec, ) -> Result; } /// A V10 document parser. #[derive(Debug, Clone, Copy)] pub struct V10DocumentParser; impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10DocumentParser { fn parse(source: &'a str) -> Result { if let Some(caps) = DOCUMENT_REGEX.captures(source) { let doctype = &caps["type"]; let doc = &caps["doc"]; let currency = &caps["currency"]; let body = &caps["body"]; let sigs = SIGNATURES_REGEX .captures_iter(&caps["sigs"]) .map(|capture| ed25519::Signature::from_base64(&capture[0]).unwrap()) .collect::>(); // TODO : Improve error handling of Signature::from_base64 failure match doctype { "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs), "Membership" => MembershipDocumentParser::parse_standard(doc, body, currency, sigs), "Certification" => { CertificationDocumentParser::parse_standard(doc, body, currency, sigs) } "Revocation" => RevocationDocumentParser::parse_standard(doc, body, currency, sigs), "Transaction" => { TransactionDocumentParser::parse_standard(doc, body, currency, sigs) } _ => Err(V10DocumentParsingError::UnknownDocumentType( doctype.to_string(), )), } } else { Err(V10DocumentParsingError::InvalidWrapperFormat()) } } } #[cfg(test)] mod tests { use super::*; use blockchain::{Document, VerificationResult}; #[test] fn document_regex() { assert!(DOCUMENT_REGEX.is_match( "Version: 10 Type: Transaction Currency: beta_brousouf Blockstamp: 204-00003E2B8A35370BA5A7064598F628A62D4E9EC1936BE8651CE9A85F2E06981B Locktime: 0 Issuers: HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp 9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB Inputs: 40:2:T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:2 70:2:T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:8 20:2:D:HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY:46 70:2:T:A0D9B4CDC113ECE1145C5525873821398890AE842F4B318BD076095A23E70956:3 20:2:T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:5 15:2:D:9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:46 Unlocks: 0:SIG(0) 1:XHX(7665798292) 2:SIG(0) 3:SIG(0) SIG(2) 4:SIG(0) SIG(1) SIG(2) 5:SIG(2) Outputs: 120:2:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g) 146:2:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx) 49:2:(SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i)\ || XHX(3EB4702F2AC2FD3FA4FDC46A4FC05AE8CDEE1A85)) Comment: -----@@@----- (why not this comment?) 42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r 2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX 2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" )); assert!(DOCUMENT_REGEX.is_match( "Version: 10 Type: Certification Currency: beta_brousouf Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV IdtyIssuer: HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd IdtyUniqueID: lolcat IdtyTimestamp: 32-DB30D958EE5CB75186972286ED3F4686B8A1C2CD IdtySignature: J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUb\ GpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci CertTimestamp: 36-1076F10A7397715D2BEE82579861999EA1F274AC SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneNYSMV3rk" )); } #[test] fn signatures_regex() { assert_eq!( SIGNATURES_REGEX .captures_iter( " 42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r 2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX 2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" ) .count(), 3 ); assert_eq!( SIGNATURES_REGEX .captures_iter( " 42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r 2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" ) .count(), 2 ); } #[test] fn parse_identity_document() { let text = "Version: 10 Type: Identity Currency: g1 Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx UniqueID: elois Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 Ydnclvw76/JHcKSmU9kl9Ie0ne5/X8NYOqPqbGnufIK3eEPRYYdEYaQh+zffuFhbtIRjv6m/DkVLH5cLy/IyAg=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Identity(doc) = doc { println!("Doc : {:?}", doc); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) } else { panic!("Wrong document type"); } } #[test] fn parse_membership_document() { let text = "Version: 10 Type: Membership Currency: g1 Issuer: D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 Membership: IN UserID: elois CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Membership(doc) = doc { println!("Doc : {:?}", doc); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) } else { panic!("Wrong document type"); } } #[test] fn parse_certification_document() { let text = "Version: 10 Type: Certification Currency: g1 Issuer: 2sZF6j2PkxBDNAqUde7Dgo5x3crkerZpQ4rBqqJGn8QT IdtyIssuer: 7jzkd8GiFnpys4X7mP78w2Y3y3kwdK6fVSLEaojd3aH9 IdtyUniqueID: fbarbut IdtyTimestamp: 98221-000000575AC04F5164F7A307CDB766139EA47DD249E4A2444F292BC8AAB408B3 IdtySignature: DjeipIeb/RF0tpVCnVnuw6mH1iLJHIsDfPGLR90Twy3PeoaDz6Yzhc/UjLWqHCi5Y6wYajV0dNg4jQRUneVBCQ== CertTimestamp: 99956-00000472758331FDA8388E30E50CA04736CBFD3B7C21F34E74707107794B56DD Hkps1QU4HxIcNXKT8YmprYTVByBhPP1U2tIM7Z8wENzLKIWAvQClkAvBE7pW9dnVa18sJIJhVZUcRrPAZfmjBA=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Certification(doc) = doc { println!("Doc : {:?}", doc); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) } else { panic!("Wrong document type"); } } #[test] fn parse_revocation_document() { let text = "Version: 10 Type: Revocation Currency: g1 Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV IdtyUniqueID: tic IdtyTimestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 IdtySignature: 1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMMmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg== XXOgI++6qpY9O31ml/FcfbXCE6aixIrgkT5jL7kBle3YOMr+8wrp7Rt+z9hDVjrNfYX2gpeJsuMNfG4T/fzVDQ=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Revocation(doc) = doc { println!("Doc : {:?}", doc); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) } else { panic!("Wrong document type"); } } #[test] fn parse_transaction_document() { let text = "Version: 10 Type: Transaction Currency: g1 Blockstamp: 107702-0000017CDBE974DC9A46B89EE7DC2BEE4017C43A005359E0853026C21FB6A084 Locktime: 0 Issuers: Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC Inputs: 1002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:104937 1002:0:D:Do6Y6nQ2KTo5fB4MXbSwabXVmXHxYRB9UUAaTPKn1XqC:105214 Unlocks: 0:SIG(0) 1:SIG(0) Outputs: 2004:0:SIG(DTgQ97AuJ8UgVXcxmNtULAs8Fg1kKC1Wr9SAS96Br9NG) Comment: c est pour 2 mois d adhesion ressourcerie lnpuFsIymgz7qhKF/GsZ3n3W8ZauAAfWmT4W0iJQBLKJK2GFkesLWeMj/+GBfjD6kdkjreg9M6VfkwIZH+hCCQ=="; let doc = V10DocumentParser::parse(text).unwrap(); if let V10Document::Transaction(doc) = doc { println!("Doc : {:?}", doc); assert_eq!(doc.verify_signatures(), VerificationResult::Valid()) } else { panic!("Wrong document type"); } } }