// 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 .
//! 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[1-9A-Za-z][^OIl]{43,44})\n\
Block: (?P[0-9]+-[0-9A-F]{64})\n\
Membership: (?P(IN|OUT))\n\
UserID: (?P[[:alnum:]_-]+)\n\
CertTS: (?P[0-9]+-[0-9A-F]{64})\n$"
).unwrap();
}
/// Type of a Membership.
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
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, PartialEq, Hash)]
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,
/// 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,
}
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 {
&self.issuers
}
fn signatures(&self) -> &Vec {
&self.signatures
}
fn as_bytes(&self) -> &[u8] {
self.as_text().as_bytes()
}
}
impl TextDocument for MembershipDocument {
fn as_text(&self) -> &str {
&self.text
}
fn generate_compact_text(&self) -> String {
format!(
"{issuer}:{signature}:{blockstamp}:{idty_blockstamp}:{username}",
issuer = self.issuers[0],
signature = self.signatures[0],
blockstamp = self.blockstamp,
idty_blockstamp = self.identity_blockstamp,
username = self.identity_username,
)
}
}
impl IntoSpecializedDocument for MembershipDocument {
fn into_specialized(self) -> BlockchainProtocol {
BlockchainProtocol::V10(Box::new(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,
) -> MembershipDocument {
MembershipDocument {
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) -> MembershipDocument {
self.build_with_text_and_sigs(self.generate_text(), signatures)
}
fn build_and_sign(&self, private_keys: Vec) -> 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,
) -> Result {
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,
membership,
identity_username: username.to_owned(),
identity_blockstamp: ity_block,
signatures,
}))
} else {
Err(V10DocumentParsingError::InvalidInnerFormat(
"Membership".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
";
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 doc =
MembershipDocumentParser::parse_standard(doc, body, currency, signatures).unwrap();
if let V10Document::Membership(doc) = doc {
println!("Doc : {:?}", doc);
assert_eq!(doc.verify_signatures(), VerificationResult::Valid());
assert_eq!(
doc.generate_compact_text(),
"DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV:\
s2hUbokkibTAWGEwErw6hyXSWlWFQ2UWs2PWx8d/kkElAyuuWaQq4Tsonuweh1xn4AC1TVWt4yMR3WrDdkhnAw==:\
0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:\
tic"
);
} else {
panic!("Wrong document type");
}
}
}