diff --git a/Cargo.lock b/Cargo.lock index c87308c3e0cf5a9e4e336c6fd6a81f77b40ea35b..6f1b41c6a7f77b86a07fe3c23c3a9086cf6e3422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,11 @@ +[[package]] +name = "aho-corasick" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "base58" version = "0.1.0" @@ -40,6 +48,19 @@ dependencies = [ "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "duniter-protocol" +version = "0.1.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)", + "duniter-keys 0.3.0", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "duniter-wotb" version = "0.4.1" @@ -68,11 +89,29 @@ name = "gcc" version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazy_static" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "linked-hash-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.35 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "0.3.15" @@ -92,6 +131,23 @@ name = "redox_syscall" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "regex" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "rust-crypto" version = "0.2.36" @@ -156,6 +212,15 @@ dependencies = [ "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "thread_local" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.39" @@ -171,6 +236,24 @@ name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.3.3" @@ -191,6 +274,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] +"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" "checksum base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5024ee8015f02155eee35c711107ddd9a9bf3cb689cf2a9089c97e79b6e1ae83" "checksum base64 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c4a342b450b268e1be8036311e2c613d7f8a7ed31214dff1cc3b60852a3168d" "checksum bincode 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9a6301db0b49fb63551bc15b5ae348147101cdf323242b93ec7546d5002ff1af" @@ -199,10 +283,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb" +"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" "checksum libc 0.2.35 (registry+https://github.com/rust-lang/crates.io-index)" = "96264e9b293e95d25bfcbbf8a88ffd1aedc85b754eba8b7d78012f638ba220eb" +"checksum linked-hash-map 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2aab0478615bb586559b0114d94dd8eca4fdbb73b443adcb0d00b61692b4bf" +"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)" = "512870020642bb8c221bf68baa1b2573da814f6ccfe5c9699b1c303047abe9b1" "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" +"checksum regex 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "744554e01ccbd98fff8c457c3b092cd67af62a555a43bfe97ae8a0451f7799fa" +"checksum regex-syntax 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" @@ -211,8 +300,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum winapi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b09fb3b6f248ea4cd42c9a65113a847d612e17505d6ebd1f7357ad68a8bf8693" "checksum winapi-i686-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ec6667f60c23eca65c561e63a13d81b44234c2e38a6b6c959025ee907ec614cc" "checksum winapi-x86_64-pc-windows-gnu 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98f12c52b2630cd05d2c3ffd8e008f7f48252c042b4871c72aed9dc733b96668" diff --git a/Cargo.toml b/Cargo.toml index f6e34e4f0c5e7962fac73be4770deaf948689044..c2e9aead4099b2c8caad969b0b019ba38df5df0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "wotb", "keys", + "protocol", ] diff --git a/keys/README.md b/keys/README.md index f388b3dd079be7373ebe7881f484c6ab5b23dd8e..288814b3ab032d5e36b5201d0eda571fd0f020b5 100644 --- a/keys/README.md +++ b/keys/README.md @@ -1,9 +1,9 @@ -# Introduction +# keys `keys` is a crate managing cryptographic keys for the Duniter project. [Duniter]: https://duniter.org/en/ -# How to use it ? +## How to use it You can add `duniter-keys` as a `cargo` dependency in your Rust project. diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8faccd1cde85fd5174e5f82610637db99d9e8cb2 --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "duniter-protocol" +version = "0.1.0" +authors = ["nanocryk <nanocryk@duniter.org>"] +description = "Implements the Duniter Protocol" +repository = "https://git.duniter.org/nodes/rust/duniter-rs" +readme = "README.md" +keywords = ["duniter", "blockchain", "cryptocurrency", "document"] +license = "AGPL-3.0" + +[lib] +path = "lib.rs" + +[dependencies] +rust-crypto = "0.2.36" +linked-hash-map = "0.5.0" +base58 = "0.1.0" +base64 = "0.8.0" +lazy_static = "1.0.0" +regex = "0.2" +duniter-keys = { path = "../keys" } diff --git a/protocol/README.md b/protocol/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d9a912991098f9ad2a437a6c3b666bf8b4fe9eea --- /dev/null +++ b/protocol/README.md @@ -0,0 +1,10 @@ +# protocol + +`protocol` is a crate implementing the [Duniter] Protocol in [version 10][version10]. + +[Duniter]: https://duniter.org/en/ +[version10]: https://git.duniter.org/nodes/typescript/duniter/blob/master/doc/Protocol.md + +## How to use it + +You can add `duniter-keys` as a `cargo` dependency in your Rust project. diff --git a/protocol/blockchain/mod.rs b/protocol/blockchain/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba814282a2c29256f431b2c0104543ca674c2d3e --- /dev/null +++ b/protocol/blockchain/mod.rs @@ -0,0 +1,255 @@ +// 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/>. + +//! Provide wrappers around Duniter documents and events. + +use std::fmt::Debug; + +use duniter_keys::{PrivateKey, PublicKey}; + +pub mod v10; + +/// List of blockchain protocol versions. +#[derive(Debug)] +pub enum BlockchainProtocol { + /// Version 10. + V10(v10::documents::V10Document), + /// Version 11. (not done yet, but defined for tests) + V11(), +} + +/// trait providing commun methods for any documents of any protocol version. +/// +/// # Design choice +/// +/// Allow only ed25519 for protocol 10 and many differents +/// schemes for protocol 11 through a proxy type. +pub trait Document: Debug { + /// Type of the `PublicKey` used by the document. + type PublicKey: PublicKey; + /// Data type of the currency code used by the document. + type CurrencyType: ?Sized; + + /// Get document version. + fn version(&self) -> u16; + + /// Get document currency. + fn currency(&self) -> &Self::CurrencyType; + + /// Iterate over document issuers. + fn issuers(&self) -> &Vec<Self::PublicKey>; + + /// Iterate over document signatures. + fn signatures(&self) -> &Vec<<Self::PublicKey as PublicKey>::Signature>; + + /// Get document as bytes for signature verification. + fn as_bytes(&self) -> &[u8]; + + /// Verify signatures of document content (as text format) + fn verify_signatures(&self) -> VerificationResult { + let issuers_count = self.issuers().len(); + let signatures_count = self.signatures().len(); + + if issuers_count != signatures_count { + VerificationResult::IncompletePairs(issuers_count, signatures_count) + } else { + let issuers = self.issuers(); + let signatures = self.signatures(); + let mismatches: Vec<_> = issuers + .iter() + .zip(signatures.iter()) + .enumerate() + .filter(|&(_, (key, signature))| !key.verify(self.as_bytes(), signature)) + .map(|(i, _)| i) + .collect(); + + if mismatches.is_empty() { + VerificationResult::Valid() + } else { + VerificationResult::Invalid(mismatches) + } + } + } +} + +/// List of possible results for signature verification. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VerificationResult { + /// All signatures are valid. + Valid(), + /// Not same amount of issuers and signatures. + /// (issuers count, signatures count) + IncompletePairs(usize, usize), + /// Signatures don't match. + /// List of mismatching pairs indexes. + Invalid(Vec<usize>), +} + +/// Trait allowing access to the document through it's proper protocol version. +/// +/// This trait is generic over `P` providing all supported protocol version variants. +/// +/// A lifetime is specified to allow enum variants to hold references to the document. +pub trait IntoSpecializedDocument<P> { + /// Get a protocol-specific document wrapped in an enum variant. + fn into_specialized(self) -> P; +} + +/// Trait helper for building new documents. +pub trait DocumentBuilder { + /// Type of the builded document. + type Document: Document; + + /// Type of the private keys signing the documents. + type PrivateKey: PrivateKey< + Signature = <<Self::Document as Document>::PublicKey as PublicKey>::Signature, + >; + + /// Build a document with provided signatures. + fn build_with_signature( + self, + signatures: Vec<<<Self::Document as Document>::PublicKey as PublicKey>::Signature>, + ) -> Self::Document; + + /// Build a document and sign it with the private key. + fn build_and_sign(self, private_keys: Vec<Self::PrivateKey>) -> Self::Document; +} + +/// Trait for a document parser from a `S` source +/// format to a `D` document. Will return the +/// parsed document or an `E` error. +pub trait DocumentParser<S, D, E> { + /// Parse a source and return a document or an error. + fn parse(source: S) -> Result<D, E>; +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_keys::{Signature, ed25519}; + + // simple text document for signature testing + #[derive(Debug, Clone)] + struct PlainTextDocument { + pub text: &'static str, + pub issuers: Vec<ed25519::PublicKey>, + pub signatures: Vec<ed25519::Signature>, + } + + impl Document for PlainTextDocument { + type PublicKey = ed25519::PublicKey; + type CurrencyType = str; + + fn version(&self) -> u16 { + unimplemented!() + } + + fn currency(&self) -> &str { + unimplemented!() + } + + fn issuers(&self) -> &Vec<ed25519::PublicKey> { + &self.issuers + } + + fn signatures(&self) -> &Vec<ed25519::Signature> { + &self.signatures + } + + fn as_bytes(&self) -> &[u8] { + self.text.as_bytes() + } + } + + #[test] + fn verify_signatures() { + let text = "Version: 10 +Type: Identity +Currency: duniter_unit_test_currency +Issuer: DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV +UniqueID: tic +Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +"; + + // good pair + let issuer1 = ed25519::PublicKey::from_base58( + "DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV", + ).unwrap(); + + let sig1 = ed25519::Signature::from_base64( + "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGMM\ + mQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==", + ).unwrap(); + + // incorrect pair + let issuer2 = ed25519::PublicKey::from_base58( + "DNann1Lh55eZMEDXeYt32bzHbA3NJR46DeQYCS2qQdLV", + ).unwrap(); + + let sig2 = ed25519::Signature::from_base64( + "1eubHHbuNfilHHH0G2bI30iZzebQ2cQ1PC7uPAw08FGMM\ + mQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==", + ).unwrap(); + + { + let doc = PlainTextDocument { + text, + issuers: vec![issuer1], + signatures: vec![sig1], + }; + + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()); + } + + { + let doc = PlainTextDocument { + text, + issuers: vec![issuer1], + signatures: vec![sig2], + }; + + assert_eq!( + doc.verify_signatures(), + VerificationResult::Invalid(vec![0]) + ); + } + + { + let doc = PlainTextDocument { + text, + issuers: vec![issuer1, issuer2], + signatures: vec![sig1], + }; + + assert_eq!( + doc.verify_signatures(), + VerificationResult::IncompletePairs(2, 1) + ); + } + + { + let doc = PlainTextDocument { + text, + issuers: vec![issuer1], + signatures: vec![sig1, sig2], + }; + + assert_eq!( + doc.verify_signatures(), + VerificationResult::IncompletePairs(1, 2) + ); + } + } +} diff --git a/protocol/blockchain/v10/documents/identity.rs b/protocol/blockchain/v10/documents/identity.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b733759c4fd902a90f75b21b238faa000c0b236 --- /dev/null +++ b/protocol/blockchain/v10/documents/identity.rs @@ -0,0 +1,273 @@ +// 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 Identity documents. + +use duniter_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 IDENTITY_REGEX: Regex = Regex::new( + "^Issuer: (?P<issuer>[1-9A-Za-z][^OIl]{43,44})\nUniqueID: (?P<uid>[[:alnum:]_-]+)\nTimestamp: (?P<blockstamp>[0-9]+-[0-9A-F]{64})\n$" + ).unwrap(); +} + +/// Wrap an Identity document. +/// +/// Must be created by parsing a text document or using a builder. +#[derive(Debug, Clone)] +pub struct IdentityDocument { + /// Document as text. + /// + /// Is used to check signatures, and other values + /// must be extracted from it. + text: String, + + /// Currency. + currency: String, + /// Unique ID + unique_id: String, + /// Blockstamp + blockstamp: Blockstamp, + /// Document issuer (there should be only one). + issuers: Vec<ed25519::PublicKey>, + /// Document signature (there should be only one). + signatures: Vec<ed25519::Signature>, +} + +impl Document for IdentityDocument { + 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 IdentityDocument { + fn as_text(&self) -> &str { + &self.text + } +} + +impl IntoSpecializedDocument<BlockchainProtocol> for IdentityDocument { + fn into_specialized(self) -> BlockchainProtocol { + BlockchainProtocol::V10(V10Document::Identity(self)) + } +} + +/// Identity document builder. +#[derive(Debug, Copy, Clone)] +pub struct IdentityDocumentBuilder<'a> { + /// Document currency. + pub currency: &'a str, + /// Identity unique id. + pub unique_id: &'a str, + /// Reference blockstamp. + pub blockstamp: &'a Blockstamp, + /// Document/identity issuer. + pub issuer: &'a ed25519::PublicKey, +} + +impl<'a> IdentityDocumentBuilder<'a> { + fn build_with_text_and_sigs( + self, + text: String, + signatures: Vec<ed25519::Signature>, + ) -> IdentityDocument { + IdentityDocument { + text: text, + currency: self.currency.to_string(), + unique_id: self.unique_id.to_string(), + blockstamp: *self.blockstamp, + issuers: vec![*self.issuer], + signatures, + } + } +} + +impl<'a> DocumentBuilder for IdentityDocumentBuilder<'a> { + type Document = IdentityDocument; + type PrivateKey = ed25519::PrivateKey; + + fn build_with_signature(self, signatures: Vec<ed25519::Signature>) -> IdentityDocument { + self.build_with_text_and_sigs(self.generate_text(), signatures) + } + + fn build_and_sign(self, private_keys: Vec<ed25519::PrivateKey>) -> IdentityDocument { + let (text, signatures) = self.build_signed_text(private_keys); + self.build_with_text_and_sigs(text, signatures) + } +} + +impl<'a> TextDocumentBuilder for IdentityDocumentBuilder<'a> { + fn generate_text(&self) -> String { + format!( + "Version: 10 +Type: Identity +Currency: {currency} +Issuer: {issuer} +UniqueID: {unique_id} +Timestamp: {blockstamp} +", + currency = self.currency, + issuer = self.issuer, + unique_id = self.unique_id, + blockstamp = self.blockstamp + ) + } +} + +/// Identity document parser +#[derive(Debug, Clone, Copy)] +pub struct IdentityDocumentParser; + +impl StandardTextDocumentParser for IdentityDocumentParser { + fn parse_standard( + doc: &str, + body: &str, + currency: &str, + signatures: Vec<ed25519::Signature>, + ) -> Result<V10Document, V10DocumentParsingError> { + if let Some(caps) = IDENTITY_REGEX.captures(body) { + let issuer = &caps["issuer"]; + let uid = &caps["uid"]; + let blockstamp = &caps["blockstamp"]; + + // 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(); + + Ok(V10Document::Identity(IdentityDocument { + text: doc.to_owned(), + currency: currency.to_owned(), + unique_id: uid.to_owned(), + blockstamp: blockstamp, + issuers: vec![issuer], + signatures, + })) + } else { + Err(V10DocumentParsingError::InvalidInnerFormat( + "Identity".to_string(), + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_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( + "1eubHHbuNfilHMM0G2bI30iZzebQ2cQ1PC7uPAw08FGM\ + MmQCRerlF/3pc4sAcsnexsxBseA/3lY03KlONqJBAg==", + ).unwrap(); + + let block = Blockstamp::from_string( + "0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + ).unwrap(); + + { + let doc = IdentityDocumentBuilder { + currency: "duniter_unit_test_currency", + unique_id: "tic", + blockstamp: &block, + issuer: &pubkey, + }.build_with_signature(vec![sig]); + + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()); + } + + { + let doc = IdentityDocumentBuilder { + currency: "duniter_unit_test_currency", + unique_id: "tic", + blockstamp: &block, + issuer: &pubkey, + }.build_and_sign(vec![prikey]); + + assert_eq!(doc.verify_signatures(), VerificationResult::Valid()); + } + } + + #[test] + fn identity_standard_regex() { + assert!(IDENTITY_REGEX.is_match( + "Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo +UniqueID: toc +Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +" + )); + } + + #[test] + fn parse_identity_document() { + let doc = "Version: 10 +Type: Identity +Currency: duniter_unit_test_currency +Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo +UniqueID: toc +Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +lcekuS0eP2dpFL99imJcwvDAwx49diiDMkG8Lj7FLkC/6IJ0tgNjUzCIZgMGi7bL5tODRiWi9B49UMXb8b3MAw=="; + + let body = "Issuer: DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo +UniqueID: toc +Timestamp: 0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +"; + + let currency = "duniter_unit_test_currency"; + + let signatures = vec![Signature::from_base64( +"lcekuS0eP2dpFL99imJcwvDAwx49diiDMkG8Lj7FLkC/6IJ0tgNjUzCIZgMGi7bL5tODRiWi9B49UMXb8b3MAw==" + ).unwrap(),]; + + let _ = IdentityDocumentParser::parse_standard(doc, body, currency, signatures).unwrap(); + } +} diff --git a/protocol/blockchain/v10/documents/mod.rs b/protocol/blockchain/v10/documents/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..681d2be6604efb3f050e8873bf7f1a2f9dfeb60a --- /dev/null +++ b/protocol/blockchain/v10/documents/mod.rs @@ -0,0 +1,262 @@ +// 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/>. + +//! Provide wrappers around Duniter blockchain documents for protocol version 10. + +use duniter_keys::{Signature, ed25519}; +use regex::Regex; +use blockchain::{Document, DocumentBuilder, DocumentParser}; +use blockchain::v10::documents::identity::IdentityDocumentParser; + +pub mod identity; + +pub use blockchain::v10::documents::identity::{IdentityDocument, IdentityDocumentBuilder}; + +// Use of lazy_static so the regex is only compiled at first use. +lazy_static! { + static ref DOCUMENT_REGEX: Regex = Regex::new( + "^(?P<doc>Version: 10\n\ + Type: (?P<type>[[:alpha:]]+)\n\ + Currency: (?P<currency>[[:alnum:] _-]+)\n\ + (?P<body>(?:.*\n)+?))\ + (?P<sigs>([[:alnum:]+/=]+\n)+)$" + ).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(), + + /// Transaction document. + Transaction(), + + /// Identity document. + Identity(IdentityDocument), + + /// Membership document. + Membership(), + + /// Certification document. + Certification(), + + /// Revocation document. + Revocation(), +} + +/// Trait for a V10 document. +pub trait TextDocument + : Document<PublicKey = ed25519::PublicKey, CurrencyType = str> { + /// Return document as text. + fn as_text(&self) -> &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 + } +} + +/// 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<ed25519::PrivateKey>, + ) -> (String, Vec<ed25519::Signature>) { + use duniter_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), +} + +trait StandardTextDocumentParser { + fn parse_standard( + doc: &str, + body: &str, + currency: &str, + signatures: Vec<ed25519::Signature>, + ) -> Result<V10Document, V10DocumentParsingError>; +} + +trait CompactTextDocumentParser<D: TextDocument> { + fn parse_compact( + doc: &str, + body: &str, + currency: &str, + signatures: Vec<ed25519::Signature>, + ) -> Result<D, V10DocumentParsingError>; +} + +/// 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<V10Document, V10DocumentParsingError> { + 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::<Vec<_>>(); + + // TODO : Improve error handling of Signature::from_base64 failure + + match doctype { + "Identity" => IdentityDocumentParser::parse_standard(doc, body, currency, sigs), + _ => Err(V10DocumentParsingError::UnknownDocumentType( + doctype.to_string(), + )), + } + } else { + Err(V10DocumentParsingError::InvalidWrapperFormat()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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 + ); + } +} diff --git a/protocol/blockchain/v10/mod.rs b/protocol/blockchain/v10/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..9b3d159d6d576877e01a5822ed3e720793bb3775 --- /dev/null +++ b/protocol/blockchain/v10/mod.rs @@ -0,0 +1,18 @@ +// 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/>. + +//! Provide wrappers around Duniter V10 documents and events. + +pub mod documents; diff --git a/protocol/lib.rs b/protocol/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..84908914a5384bc2eed4cb7a35b5510681199504 --- /dev/null +++ b/protocol/lib.rs @@ -0,0 +1,198 @@ +// 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/>. + +//! Implements the Duniter Protocol. + +#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, + trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, + unused_qualifications)] + +extern crate base58; +extern crate base64; +extern crate crypto; +extern crate duniter_keys; +#[macro_use] +extern crate lazy_static; +extern crate linked_hash_map; +extern crate regex; + +use std::fmt::{Debug, Display, Error, Formatter}; + +use duniter_keys::BaseConvertionError; + +pub mod blockchain; + +/// A block Id. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct BlockId(pub u32); + +impl Display for BlockId { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}", self.0) + } +} + +/// A hash wrapper. +/// +/// A hash is often provided as string composed of 64 hexadecimal character (0 to 9 then A to F). +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Hash(pub [u8; 32]); + +impl Display for Hash { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}", self.to_hex()) + } +} + +impl Debug for Hash { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "Hash({})", self) + } +} + +impl Hash { + /// Convert a `Hash` to an hex string. + pub fn to_hex(&self) -> String { + let strings: Vec<String> = self.0.iter().map(|b| format!("{:02X}", b)).collect(); + + strings.join("") + } + + /// Convert a hex string in a `Hash`. + /// + /// The hex string must only contains hex characters + /// and produce a 32 bytes value. + pub fn from_hex(text: &str) -> Result<Hash, BaseConvertionError> { + if text.len() != 64 { + Err(BaseConvertionError::InvalidKeyLendth(text.len(), 64)) + } else { + let mut hash = Hash([0u8; 32]); + + let chars: Vec<char> = text.chars().collect(); + + for i in 0..64 { + if i % 2 != 0 { + continue; + } + + let byte1 = chars[i].to_digit(16); + let byte2 = chars[i + 1].to_digit(16); + + if byte1.is_none() { + return Err(BaseConvertionError::InvalidCharacter(chars[i], i)); + } else if byte2.is_none() { + return Err(BaseConvertionError::InvalidCharacter(chars[i + 1], i + 1)); + } + + let byte1 = byte1.unwrap() as u8; + let byte2 = byte2.unwrap() as u8; + + let byte = (byte1 << 4) | byte2; + hash.0[i / 2] = byte; + } + + Ok(hash) + } + } +} + +/// Wrapper of a block hash. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct BlockHash(Hash); + +impl Display for BlockHash { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}", self.0.to_hex()) + } +} + +impl Debug for BlockHash { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "BlockHash({})", self) + } +} + +/// Type of errors for [`BlockUId`] parsing. +/// +/// [`BlockUId`]: struct.BlockUId.html +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum BlockUIdParseError { + /// Given string have invalid format + InvalidFormat(), + /// [`BlockId`](struct.BlockHash.html) part is not a valid number. + InvalidBlockId(), + /// [`BlockHash`](struct.BlockHash.html) part is not a valid hex number. + InvalidBlockHash(), +} + +/// A blockstamp (Unique ID). +/// +/// It's composed of the [`BlockId`] and +/// the [`BlockHash`] of the block. +/// +/// Thanks to blockchain immutability and frequent block production, it can +/// be used to date information. +/// +/// [`BlockId`]: struct.BlockId.html +/// [`BlockHash`]: struct.BlockHash.html +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Blockstamp { + /// Block Id. + pub id: BlockId, + /// Block hash. + pub hash: BlockHash, +} + +impl Display for Blockstamp { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{}-{}", self.id, self.hash) + } +} + +impl Debug for Blockstamp { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "BlockUId({})", self) + } +} + +impl Blockstamp { + /// Create a `BlockUId` from a text. + pub fn from_string(src: &str) -> Result<Blockstamp, BlockUIdParseError> { + let mut split = src.split('-'); + + if split.clone().count() != 2 { + Err(BlockUIdParseError::InvalidFormat()) + } else { + let id = split.next().unwrap().parse::<u32>(); + let hash = Hash::from_hex(split.next().unwrap()); + + if id.is_err() { + Err(BlockUIdParseError::InvalidBlockId()) + } else if hash.is_err() { + Err(BlockUIdParseError::InvalidBlockHash()) + } else { + Ok(Blockstamp { + id: BlockId(id.unwrap()), + hash: BlockHash(hash.unwrap()), + }) + } + } + } + + /// Convert a `BlockUId` to its text format. + pub fn to_string(&self) -> String { + format!("{}", self) + } +} diff --git a/wotb/README.md b/wotb/README.md index fc6bf882c7373eb98ed7a1a27c06a6b3c5215b49..32266674b05e3aa88c51a4c7a199646e2c7428db 100644 --- a/wotb/README.md +++ b/wotb/README.md @@ -1,11 +1,10 @@ -# Introduction +# wotb `wotb` is a crate making "Web of Trust" computations for the [Duniter] project. [Duniter]: https://duniter.org/en/ -# How to use it ? +## How to use it -You can add `duniter-wotb` as a `cargo` dependency in your Rust project. -To use it in JavaScript, see `duniter-wotb-js` package. (not done yet) \ No newline at end of file +You can add `duniter-wotb` as a `cargo` dependency in your Rust project. \ No newline at end of file