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(),
                 )),