diff --git a/Cargo.lock b/Cargo.lock index 8d8d33169531d4dfdecf39436c4c6919ca379a55..5ecd84f1686ded6b94b0898a4880f7880ee0bec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "duniter-protocol" -version = "0.3.0" +version = "0.4.0" dependencies = [ "base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index b92ef593aecb01eaeab107c6f9958d537f008c94..0420f09aa36f2a91b2387f02e22bf54dd9bfc409 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "duniter-protocol" -version = "0.3.0" +version = "0.4.0" authors = ["nanocryk <nanocryk@duniter.org>"] description = "Implements the Duniter Protocol" repository = "https://git.duniter.org/nodes/rust/duniter-rs" diff --git a/protocol/blockchain/v10/documents/membership.rs b/protocol/blockchain/v10/documents/membership.rs new file mode 100644 index 0000000000000000000000000000000000000000..07153b79a886e6ef368ae14953be5e3bb03ba444 --- /dev/null +++ b/protocol/blockchain/v10/documents/membership.rs @@ -0,0 +1,328 @@ +// 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 <https://www.gnu.org/licenses/>. + +//! Wrappers around Membership documents. + +use duniter_crypto::keys::{PublicKey, ed25519}; +use regex::Regex; + +use Blockstamp; +use blockchain::{BlockchainProtocol, Document, DocumentBuilder, IntoSpecializedDocument}; +use blockchain::v10::documents::{StandardTextDocumentParser, TextDocument, TextDocumentBuilder, + V10Document, V10DocumentParsingError}; + +lazy_static! { + static ref MEMBERSHIP_REGEX: Regex = Regex::new( + "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\n\ +Block: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n\ +Membership: (?P<membership>(IN|OUT))\n\ +UserID: (?P<ity_user>[[:alnum:]_-]+)\n\ +CertTS: (?P<ity_block>[0-9]+-[0-9A-F]{64})\n$" + ).unwrap(); +} + +/// Type of a Membership. +#[derive(Debug, Clone, Copy)] +pub enum MembershipType { + /// The member wishes to opt-in. + In(), + /// The member wishes to opt-out. + Out(), +} + +/// Wrap an Membership document. +/// +/// Must be created by parsing a text document or using a builder. +#[derive(Debug, Clone)] +pub struct MembershipDocument { + /// Document as text. + /// + /// Is used to check signatures, and other values mut be extracted from it. + text: String, + + /// Name of the currency. + currency: String, + /// Document issuer (there should be only one). + issuers: Vec<ed25519::PublicKey>, + /// Blockstamp + blockstamp: Blockstamp, + /// Membership message. + membership: MembershipType, + /// Identity to use for this public key. + identity_username: String, + /// Identity document blockstamp. + identity_blockstamp: Blockstamp, + /// Document signature (there should be only one). + signatures: Vec<ed25519::Signature>, +} + +impl MembershipDocument { + /// Membership message. + pub fn membership(&self) -> MembershipType { + self.membership + } + + /// Identity to use for this public key. + pub fn identity_username(&self) -> &str { + &self.identity_username + } +} + +impl Document for MembershipDocument { + type PublicKey = ed25519::PublicKey; + type CurrencyType = str; + + fn version(&self) -> u16 { + 10 + } + + fn currency(&self) -> &str { + &self.currency + } + + fn issuers(&self) -> &Vec<ed25519::PublicKey> { + &self.issuers + } + + fn signatures(&self) -> &Vec<ed25519::Signature> { + &self.signatures + } + + fn as_bytes(&self) -> &[u8] { + self.as_text().as_bytes() + } +} + +impl TextDocument for MembershipDocument { + fn as_text(&self) -> &str { + &self.text + } +} + +impl IntoSpecializedDocument<BlockchainProtocol> for MembershipDocument { + fn into_specialized(self) -> BlockchainProtocol { + BlockchainProtocol::V10(V10Document::Membership(self)) + } +} + +/// Membership document builder. +#[derive(Debug, Copy, Clone)] +pub struct MembershipDocumentBuilder<'a> { + /// Document currency. + pub currency: &'a str, + /// Document/identity issuer. + pub issuer: &'a ed25519::PublicKey, + /// Reference blockstamp. + pub blockstamp: &'a Blockstamp, + /// Membership message. + pub membership: MembershipType, + /// Identity username. + pub identity_username: &'a str, + /// Identity document blockstamp. + pub identity_blockstamp: &'a Blockstamp, +} + +impl<'a> MembershipDocumentBuilder<'a> { + fn build_with_text_and_sigs( + self, + text: String, + signatures: Vec<ed25519::Signature>, + ) -> MembershipDocument { + MembershipDocument { + text: text, + currency: self.currency.to_string(), + issuers: vec![*self.issuer], + blockstamp: *self.blockstamp, + membership: self.membership, + identity_username: self.identity_username.to_string(), + identity_blockstamp: *self.identity_blockstamp, + signatures, + } + } +} + +impl<'a> DocumentBuilder for MembershipDocumentBuilder<'a> { + type Document = MembershipDocument; + type PrivateKey = ed25519::PrivateKey; + + fn build_with_signature(&self, signatures: Vec<ed25519::Signature>) -> MembershipDocument { + self.build_with_text_and_sigs(self.generate_text(), signatures) + } + + fn build_and_sign(&self, private_keys: Vec<ed25519::PrivateKey>) -> MembershipDocument { + let (text, signatures) = self.build_signed_text(private_keys); + self.build_with_text_and_sigs(text, signatures) + } +} + +impl<'a> TextDocumentBuilder for MembershipDocumentBuilder<'a> { + fn generate_text(&self) -> String { + format!( + "Version: 10 +Type: Membership +Currency: {currency} +Issuer: {issuer} +Block: {blockstamp} +Membership: {membership} +UserID: {username} +CertTS: {ity_blockstamp} +", + currency = self.currency, + issuer = self.issuer, + blockstamp = self.blockstamp, + membership = match self.membership { + MembershipType::In() => "IN", + MembershipType::Out() => "OUT", + }, + username = self.identity_username, + ity_blockstamp = self.identity_blockstamp, + ) + } +} + +/// Membership document parser +#[derive(Debug, Clone, Copy)] +pub struct MembershipDocumentParser; + +impl StandardTextDocumentParser for MembershipDocumentParser { + fn parse_standard( + doc: &str, + body: &str, + currency: &str, + signatures: Vec<ed25519::Signature>, + ) -> Result<V10Document, V10DocumentParsingError> { + if let Some(caps) = MEMBERSHIP_REGEX.captures(body) { + let issuer = &caps["issuer"]; + + let blockstamp = &caps["blockstamp"]; + let membership = &caps["membership"]; + let username = &caps["ity_user"]; + let ity_block = &caps["ity_block"]; + + // Regex match so should not fail. + // TODO : Test it anyway + let issuer = ed25519::PublicKey::from_base58(issuer).unwrap(); + let blockstamp = Blockstamp::from_string(blockstamp).unwrap(); + let membership = match membership { + "IN" => MembershipType::In(), + "OUT" => MembershipType::Out(), + _ => panic!("Invalid membership type {}", membership), + }; + + let ity_block = Blockstamp::from_string(ity_block).unwrap(); + + Ok(V10Document::Membership(MembershipDocument { + text: doc.to_owned(), + issuers: vec![issuer], + currency: currency.to_owned(), + blockstamp: blockstamp, + membership, + identity_username: username.to_owned(), + identity_blockstamp: ity_block, + signatures, + })) + } else { + Err(V10DocumentParsingError::InvalidInnerFormat( + "Identity".to_string(), + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_crypto::keys::{PrivateKey, PublicKey, Signature}; + use blockchain::VerificationResult; + + #[test] + fn generate_real_document() { + let pubkey = ed25519::PublicKey::from_base58( + "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV", + ).unwrap(); + + let prikey = ed25519::PrivateKey::from_base58( + "468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5G\ + iERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7", + ).unwrap(); + + let sig = ed25519::Signature::from_base64( + "s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkEl\ + AyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==", + ).unwrap(); + + let block = Blockstamp::from_string( + "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + ).unwrap(); + + let builder = MembershipDocumentBuilder { + currency: "duniter_unit_test_currency", + issuer: &pubkey, + blockstamp: &block, + membership: MembershipType::In(), + identity_username: "tic", + identity_blockstamp: &block, + }; + + assert_eq!( + builder.build_with_signature(vec![sig]).verify_signatures(), + VerificationResult::Valid() + ); + assert_eq!( + builder.build_and_sign(vec![prikey]).verify_signatures(), + VerificationResult::Valid() + ); + } + + #[test] + fn membership_standard_regex() { + assert!(MEMBERSHIP_REGEX.is_match( + "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +Membership: IN +UserID: tic +CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +" + )); + } + + #[test] + fn membership_identity_document() { + let doc = "Version: 10 +Type: Membership +Currency: duniter_unit_test_currency +Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +Membership: IN +UserID: tic +CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw=="; + + let body = "Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +Block: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +Membership: IN +UserID: tic +CertTS: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +"; + + let currency = "duniter_unit_test_currency"; + + let signatures = vec![Signature::from_base64( +"s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==" + ).unwrap(),]; + + let _ = MembershipDocumentParser::parse_standard(doc, body, currency, signatures).unwrap(); + } +} diff --git a/protocol/blockchain/v10/documents/mod.rs b/protocol/blockchain/v10/documents/mod.rs index b3c9d92822ec94cdc4b8f0359f3d518925298ffa..31ed9af9e8a2bd5b6661ad18e50a4616fe55b9e7 100644 --- a/protocol/blockchain/v10/documents/mod.rs +++ b/protocol/blockchain/v10/documents/mod.rs @@ -21,8 +21,10 @@ use blockchain::{Document, DocumentBuilder, DocumentParser}; use blockchain::v10::documents::identity::IdentityDocumentParser; pub mod identity; +pub mod membership; pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder}; +pub use blockchain::v10::documents::membership::{MembershipDocument, MembershipDocumentParser}; // Use of lazy_static so the regex is only compiled at first use. lazy_static! { @@ -54,7 +56,7 @@ pub enum V10Document { Identity(IdentityDocument), /// Membership document. - Membership(), + Membership(MembershipDocument), /// Certification document. Certification(), @@ -164,6 +166,7 @@ impl<'a> DocumentParser<&'a str, V10Document, V10DocumentParsingError> for V10Do match doctype { "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs), + "Membership" => MembershipDocumentParser::parse_standard(doc, body, currency, sigs), _ => Err(V10DocumentParsingError::UnknownDocumentType( doctype.to_string(), )),