From 44049e44f513312dcee86c2149a4ced710dd37b5 Mon Sep 17 00:00:00 2001
From: Vincent Texier <vit@free.fr>
Date: Tue, 15 Jun 2021 20:42:45 +0200
Subject: [PATCH] [enh] #95 refactor Document.signatures (List) as
 Document.signature (str) (break backward compatibility)

Transaction can have multiple signatures, with Transaction.signatures attribute and Transaction.multi_sign() method.

verifying_key.py is refactored heavily to avoid circular reference:
* method VerifyingKey.verify_document() removed, use Document.check_signature() or Transaction.check_signatures() instead
* method VerifyingKey.verify_ws2p_head() is removed, use HeadV0.check_signature() instead.
---
 Makefile                              |  2 +-
 duniterpy/documents/block.py          | 87 +++++++++++++++++++++++----
 duniterpy/documents/certification.py  | 36 +++++------
 duniterpy/documents/document.py       | 54 +++++++++++------
 duniterpy/documents/identity.py       | 12 ++--
 duniterpy/documents/membership.py     |  6 +-
 duniterpy/documents/peer.py           |  6 +-
 duniterpy/documents/revocation.py     | 34 +++++------
 duniterpy/documents/transaction.py    | 85 ++++++++++++++++++++++++--
 duniterpy/documents/ws2p/heads.py     | 20 +++++-
 duniterpy/documents/ws2p/messages.py  | 14 ++---
 duniterpy/helpers/network.py          |  2 +-
 duniterpy/key/verifying_key.py        | 46 +-------------
 examples/save_revoke_document.py      |  2 +-
 examples/send_identity.py             |  2 +-
 examples/send_membership.py           |  2 +-
 tests/documents/test_block.py         |  6 +-
 tests/documents/test_certification.py | 24 +++-----
 tests/documents/test_membership.py    |  6 +-
 tests/documents/test_peer.py          |  4 +-
 tests/key/test_verifying_key.py       | 18 ++----
 21 files changed, 289 insertions(+), 179 deletions(-)

diff --git a/Makefile b/Makefile
index bac812af..963563fd 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ docs:
 
 # run tests
 tests:
-	poetry run python3 -m unittest ${TESTS_FILTER}
+	poetry run python3 -m unittest -f ${TESTS_FILTER}
 
 # check
 check: mypy pylint check-format
diff --git a/duniterpy/documents/block.py b/duniterpy/documents/block.py
index a465c4ff..c6252fcb 100644
--- a/duniterpy/documents/block.py
+++ b/duniterpy/documents/block.py
@@ -19,6 +19,9 @@ import re
 from typing import List, Optional, Sequence, Type, TypeVar
 
 from ..constants import BLOCK_HASH_REGEX, PUBKEY_REGEX
+
+# required to type hint cls in classmethod
+from ..key import SigningKey, VerifyingKey
 from .block_uid import BlockUID
 from .certification import Certification
 from .document import Document, MalformedDocumentError
@@ -27,7 +30,6 @@ from .membership import Membership
 from .revocation import Revocation
 from .transaction import Transaction
 
-# required to type hint cls in classmethod
 BlockType = TypeVar("BlockType", bound="Block")
 
 
@@ -206,6 +208,7 @@ class Block(Document):
         :param nonce: the nonce value of the block
         """
         super().__init__(version, currency)
+
         documents_versions = max(
             max([1] + [i.version for i in identities]),
             max([1] + [m.version for m in actives + leavers + joiners]),
@@ -243,15 +246,25 @@ class Block(Document):
         self.transactions = transactions
         self.inner_hash = inner_hash
         self.nonce = nonce
-        self.signatures = list()
 
     @property
     def blockUID(self) -> BlockUID:
+        """
+        Return Block UID
+
+        :return:
+        """
         return BlockUID(self.number, self.proof_of_work())
 
     @classmethod
     def from_parsed_json(cls: Type[BlockType], parsed_json_block: dict) -> BlockType:
-        """return a block from the python structure produced when parsing json"""
+        """
+        Return a Block instance from the python dict produced when parsing json
+
+        :param parsed_json_block: Block as a Python dict
+
+        :return:
+        """
         b = parsed_json_block  # alias for readability
         version = b["version"]
         currency = b["currency"]
@@ -326,11 +339,18 @@ class Block(Document):
         block = cls(**arguments)
 
         # return the block with signature
-        block.signatures = [b["signature"]]
+        block.signature = b["signature"]
         return block
 
     @classmethod
     def from_signed_raw(cls: Type[BlockType], signed_raw: str) -> BlockType:
+        """
+        Create Block instance from signed raw format string
+
+        :param signed_raw: Signed raw format string
+
+        :return:
+        """
         lines = signed_raw.splitlines(True)
         n = 0
 
@@ -528,10 +548,15 @@ class Block(Document):
         )
 
         # return block with signature
-        block.signatures = [signature]
+        block.signature = signature
         return block
 
     def raw(self) -> str:
+        """
+        Return Block in raw format
+
+        :return:
+        """
         doc = """Version: {version}
 Type: Block
 Currency: {currency}
@@ -611,31 +636,43 @@ PreviousIssuer: {1}\n".format(
         return doc
 
     def proof_of_work(self) -> str:
+        """
+        Return Proof of Work hash for the Block
+
+        :return:
+        """
         doc_str = """InnerHash: {inner_hash}
 Nonce: {nonce}
 {signature}
 """.format(
-            inner_hash=self.inner_hash, nonce=self.nonce, signature=self.signatures[0]
+            inner_hash=self.inner_hash, nonce=self.nonce, signature=self.signature
         )
         return hashlib.sha256(doc_str.encode("ascii")).hexdigest().upper()
 
     def computed_inner_hash(self) -> str:
+        """
+        Return inner hash of the Block
+
+        :return:
+        """
         doc = self.raw()
         inner_doc = "\n".join(doc.split("\n")[:-3]) + "\n"
         return hashlib.sha256(inner_doc.encode("ascii")).hexdigest().upper()
 
-    def sign(self, keys):
+    def sign(self, key: SigningKey) -> None:
         """
-        Sign the current document.
-        Warning : current signatures will be replaced with the new ones.
+        Sign the current document with key
+
+        :param key: Libnacl SigningKey instance
+
+        :return:
         """
-        key = keys[0]
-        signed = "InnerHash: {inner_hash}\nNonce: {nonce}\n".format(
+        string_to_sign = "InnerHash: {inner_hash}\nNonce: {nonce}\n".format(
             inner_hash=self.inner_hash,
             nonce=self.nonce,
         )
-        signing = base64.b64encode(key.signature(bytes(signed, "ascii")))
-        self.signatures = [signing.decode("ascii")]
+        signature = base64.b64encode(key.signature(bytes(string_to_sign, "ascii")))
+        self.signature = signature.decode("ascii")
 
     def __eq__(self, other: object) -> bool:
         if not isinstance(other, Block):
@@ -661,3 +698,27 @@ Nonce: {nonce}
         if not isinstance(other, Block):
             return False
         return self.blockUID >= other.blockUID
+
+    def check_signature(self, pubkey: str):
+        """
+        Check if the signature is from pubkey
+
+        :param pubkey: Base58 public key
+
+        :return:
+        """
+        if self.signature is None:
+            raise Exception("Signature is None, can not verify signature")
+
+        signature = base64.b64decode(self.signature)
+        content_to_verify = "InnerHash: {0}\nNonce: {1}\n".format(
+            self.inner_hash, self.nonce
+        )
+
+        prepended = signature + bytes(content_to_verify, "ascii")
+        verifying_key = VerifyingKey(pubkey)
+        try:
+            verifying_key.verify(prepended)
+            return True
+        except ValueError:
+            return False
diff --git a/duniterpy/documents/certification.py b/duniterpy/documents/certification.py
index dd087d42..b878e2c5 100644
--- a/duniterpy/documents/certification.py
+++ b/duniterpy/documents/certification.py
@@ -13,17 +13,17 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import base64
-import logging
 import re
 from typing import Optional, Type, TypeVar, Union
 
 from ..constants import BLOCK_ID_REGEX, BLOCK_UID_REGEX, PUBKEY_REGEX, SIGNATURE_REGEX
+
+# required to type hint cls in classmethod
+from ..key import SigningKey
 from .block_uid import BlockUID
 from .document import Document, MalformedDocumentError
 from .identity import Identity
 
-# required to type hint cls in classmethod
 CertificationType = TypeVar("CertificationType", bound="Certification")
 
 
@@ -116,7 +116,7 @@ class Certification(Document):
         certification = cls(version, currency, pubkey_from, identity, timestamp)
 
         # return certification with signature
-        certification.signatures = [signature]
+        certification.signature = signature
         return certification
 
     @classmethod
@@ -154,7 +154,7 @@ class Certification(Document):
         certification = cls(version, currency, pubkey_from, pubkey_to, timestamp)
 
         # return certification with signature
-        certification.signatures = [signature]
+        certification.signature = signature
         return certification
 
     def raw(self) -> str:
@@ -182,28 +182,21 @@ CertTimestamp: {timestamp}
             certified_pubkey=self.identity.pubkey,
             certified_uid=self.identity.uid,
             certified_ts=self.identity.timestamp,
-            certified_signature=self.identity.signatures[0],
+            certified_signature=self.identity.signature,
             timestamp=self.timestamp,
         )
 
-    def sign(self, keys: list) -> None:
+    def sign(self, key: SigningKey) -> None:
         """
-        Sign the current document with the keys for the certified Identity given
+        Sign the current document with the key for the certified Identity given
 
-        Warning : current signatures will be replaced with the new ones.
-
-        :param keys: List of libnacl key instances
+        :param key: Libnacl key instance
         """
         if not isinstance(self.identity, Identity):
             raise MalformedDocumentError(
                 "Can not return full certification document created from inline"
             )
-
-        self.signatures = []
-        for key in keys:
-            signing = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
-            logging.debug("Signature : \n%s", signing.decode("ascii"))
-            self.signatures.append(signing.decode("ascii"))
+        super().sign(key)
 
     def signed_raw(self) -> str:
         """
@@ -213,12 +206,13 @@ CertTimestamp: {timestamp}
         """
         if not isinstance(self.identity, Identity):
             raise MalformedDocumentError(
-                "Can not return full certification document created from inline"
+                "Identity is a pubkey or None, can not create raw format"
             )
+        if self.signature is None:
+            raise MalformedDocumentError("Signature is None, can not create raw format")
 
         raw = self.raw()
-        signed = "\n".join(self.signatures)
-        signed_raw = raw + signed + "\n"
+        signed_raw = raw + self.signature + "\n"
         return signed_raw
 
     def inline(self) -> str:
@@ -228,5 +222,5 @@ CertTimestamp: {timestamp}
         :return:
         """
         return "{0}:{1}:{2}:{3}".format(
-            self.pubkey_from, self.pubkey_to, self.timestamp.number, self.signatures[0]
+            self.pubkey_from, self.pubkey_to, self.timestamp.number, self.signature
         )
diff --git a/duniterpy/documents/document.py b/duniterpy/documents/document.py
index 1d9f5536..0aff4376 100644
--- a/duniterpy/documents/document.py
+++ b/duniterpy/documents/document.py
@@ -17,9 +17,11 @@ import base64
 import hashlib
 import logging
 import re
-from typing import Any, List, Type, TypeVar
+from typing import Any, Dict, Optional, Type, TypeVar
+from typing.re import Pattern
 
 from ..constants import SIGNATURE_REGEX
+from ..key import SigningKey, VerifyingKey
 
 
 class MalformedDocumentError(Exception):
@@ -47,7 +49,7 @@ class Document:
         "({signature_regex})\n".format(signature_regex=SIGNATURE_REGEX)
     )
 
-    fields_parsers = {
+    fields_parsers: Dict[str, Pattern] = {
         "Version": re_version,
         "Currency": re_currency,
         "Signature": re_signature,
@@ -62,7 +64,7 @@ class Document:
         """
         self.version = version
         self.currency = currency
-        self.signatures: List[str] = list()
+        self.signature: Optional[str] = None
 
     @classmethod
     def parse_field(cls: Type[DocumentType], field_name: str, line: str) -> Any:
@@ -82,19 +84,15 @@ class Document:
             raise MalformedDocumentError(field_name) from AttributeError
         return value
 
-    def sign(self, keys: list) -> None:
+    def sign(self, key: SigningKey) -> None:
         """
-        Sign the current document.
+        Sign the current document with key
 
-        Warning : current signatures will be replaced with the new ones.
-
-        :param keys: List of libnacl keys instance
+        :param key: Libnacl key instance
         """
-        self.signatures = []
-        for key in keys:
-            signing = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
-            logging.debug("Signature : \n%s", signing.decode("ascii"))
-            self.signatures.append(signing.decode("ascii"))
+        signature = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
+        logging.debug("Signature : \n%s", signature.decode("ascii"))
+        self.signature = signature.decode("ascii")
 
     def raw(self) -> str:
         """
@@ -104,13 +102,12 @@ class Document:
 
     def signed_raw(self) -> str:
         """
-        If keys are None, returns the raw + current signatures
-        If keys are present, returns the raw signed by these keys
         :return:
         """
+        if self.signature is None:
+            raise MalformedDocumentError("Signature is None, can not create raw format")
         raw = self.raw()
-        signed = "\n".join(self.signatures)
-        signed_raw = raw + signed + "\n"
+        signed_raw = raw + self.signature + "\n"
         return signed_raw
 
     @property
@@ -121,3 +118,26 @@ class Document:
         :return:
         """
         return hashlib.sha256(self.signed_raw().encode("ascii")).hexdigest().upper()
+
+    def check_signature(self, pubkey: str):
+        """
+        Check if the signature is from pubkey
+
+        :param pubkey: Base58 public key
+
+        :return:
+        """
+        if self.signature is None:
+            raise Exception("Signature is None, can not verify signature")
+
+        signature = base64.b64decode(self.signature)
+
+        content_to_verify = self.raw()
+        prepended = signature + bytes(content_to_verify, "ascii")
+
+        verifying_key = VerifyingKey(pubkey)
+        try:
+            verifying_key.verify(prepended)
+            return True
+        except ValueError:
+            return False
diff --git a/duniterpy/documents/identity.py b/duniterpy/documents/identity.py
index 85f0fdcb..16834db9 100644
--- a/duniterpy/documents/identity.py
+++ b/duniterpy/documents/identity.py
@@ -117,7 +117,7 @@ class Identity(Document):
         identity = cls(version, currency, pubkey, uid, blockstamp)
 
         # return identity with signature
-        identity.signatures = [signature]
+        identity.signature = signature
         return identity
 
     @classmethod
@@ -154,7 +154,7 @@ class Identity(Document):
         identity = cls(version, currency, pubkey, uid, blockstamp)
 
         # return identity with signature
-        identity.signatures = [signature]
+        identity.signature = signature
         return identity
 
     def raw(self) -> str:
@@ -185,7 +185,7 @@ Timestamp: {timestamp}
         """
         return "{pubkey}:{signature}:{timestamp}:{uid}".format(
             pubkey=self.pubkey,
-            signature=self.signatures[0],
+            signature=self.signature,
             timestamp=self.timestamp,
             uid=self.uid,
         )
@@ -220,7 +220,7 @@ Timestamp: {timestamp}
         signature = Identity.parse_field("IdtySignature", lines[n])
 
         identity = cls(version, currency, issuer, uid, timestamp)
-        identity.signatures = [signature]
+        identity.signature = signature
 
         return identity
 
@@ -254,7 +254,7 @@ Timestamp: {timestamp}
         signature = Identity.parse_field("IdtySignature", lines[n])
 
         identity = cls(version, currency, issuer, uid, timestamp)
-        identity.signatures = [signature]
+        identity.signature = signature
 
         return identity
 
@@ -287,6 +287,6 @@ Timestamp: {timestamp}
             uid=uid,
             timestamp=timestamp,
         )
-        identity.signatures = [signature]
+        identity.signature = signature
 
         return identity
diff --git a/duniterpy/documents/membership.py b/duniterpy/documents/membership.py
index 2b80ec5b..fe4ebe82 100644
--- a/duniterpy/documents/membership.py
+++ b/duniterpy/documents/membership.py
@@ -138,7 +138,7 @@ class Membership(Document):
         )
 
         # return membership with signature
-        membership.signatures = [signature]
+        membership.signature = signature
         return membership
 
     @classmethod
@@ -194,7 +194,7 @@ class Membership(Document):
         )
 
         # return membership with signature
-        membership.signatures = [signature]
+        membership.signature = signature
         return membership
 
     def raw(self) -> str:
@@ -228,7 +228,7 @@ CertTS: {6}
         """
         return "{0}:{1}:{2}:{3}:{4}".format(
             self.issuer,
-            self.signatures[0],
+            self.signature,
             self.membership_ts,
             self.identity_ts,
             self.uid,
diff --git a/duniterpy/documents/peer.py b/duniterpy/documents/peer.py
index b995c650..3189b154 100644
--- a/duniterpy/documents/peer.py
+++ b/duniterpy/documents/peer.py
@@ -83,7 +83,7 @@ class Peer(Document):
 
         self.pubkey = pubkey
         self.blockUID = block_uid
-        self.endpoints = endpoints
+        self.endpoints: List[Endpoint] = endpoints
 
     @classmethod
     def from_signed_raw(cls: Type[PeerType], raw: str) -> PeerType:
@@ -127,7 +127,7 @@ class Peer(Document):
         peer = cls(version, currency, pubkey, block_uid, endpoints)
 
         # return peer with signature
-        peer.signatures = [signature]
+        peer.signature = signature
         return peer
 
     def raw(self) -> str:
@@ -168,5 +168,5 @@ Endpoints:
         peer = cls(version, currency, pubkey, block_uid, endpoints)
 
         # return peer with signature
-        peer.signatures = [signature]
+        peer.signature = signature
         return peer
diff --git a/duniterpy/documents/revocation.py b/duniterpy/documents/revocation.py
index 2f7ba949..789a147e 100644
--- a/duniterpy/documents/revocation.py
+++ b/duniterpy/documents/revocation.py
@@ -13,15 +13,16 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import base64
 import re
 from typing import Type, TypeVar, Union
 
 from ..constants import BLOCK_UID_REGEX, PUBKEY_REGEX, SIGNATURE_REGEX
+
+# required to type hint cls in classmethod
+from ..key import SigningKey
 from .document import Document, MalformedDocumentError
 from .identity import Identity
 
-# required to type hint cls in classmethod
 RevocationType = TypeVar("RevocationType", bound="Revocation")
 
 
@@ -98,7 +99,7 @@ class Revocation(Document):
         revocation = cls(version, currency, pubkey)
 
         # return revocation with signature
-        revocation.signatures = [signature]
+        revocation.signature = signature
         return revocation
 
     @classmethod
@@ -128,7 +129,7 @@ class Revocation(Document):
         revocation = cls(version, currency, identity)
 
         # return revocation with signature
-        revocation.signatures = [signature]
+        revocation.signature = signature
         return revocation
 
     @staticmethod
@@ -147,7 +148,7 @@ class Revocation(Document):
 
         :return:
         """
-        return "{0}:{1}".format(self.pubkey, self.signatures[0])
+        return "{0}:{1}".format(self.pubkey, self.signature)
 
     def raw(self) -> str:
         """
@@ -173,26 +174,21 @@ IdtySignature: {signature}
             pubkey=self.identity.pubkey,
             uid=self.identity.uid,
             timestamp=self.identity.timestamp,
-            signature=self.identity.signatures[0],
+            signature=self.identity.signature,
         )
 
-    def sign(self, keys: list) -> None:
+    def sign(self, key: SigningKey) -> None:
         """
-        Sign the current document.
-        Warning : current signatures will be replaced with the new ones.
+        Sign the current document
 
-        :param keys: List of libnacl key instances
+        :param key: List of libnacl key instances
         :return:
         """
         if not isinstance(self.identity, Identity):
             raise MalformedDocumentError(
                 "Can not return full revocation document created from inline"
             )
-
-        self.signatures = []
-        for key in keys:
-            signing = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
-            self.signatures.append(signing.decode("ascii"))
+        super().sign(key)
 
     def signed_raw(self) -> str:
         """
@@ -202,10 +198,10 @@ IdtySignature: {signature}
         """
         if not isinstance(self.identity, Identity):
             raise MalformedDocumentError(
-                "Can not return full revocation document created from inline"
+                "Identity is a pubkey or None, can not create raw format"
             )
-
+        if self.signature is None:
+            raise MalformedDocumentError("Signature is None, can not create raw format")
         raw = self.raw()
-        signed = "\n".join(self.signatures)
-        signed_raw = raw + signed + "\n"
+        signed_raw = raw + self.signature + "\n"
         return signed_raw
diff --git a/duniterpy/documents/transaction.py b/duniterpy/documents/transaction.py
index 05434761..faf09bf7 100644
--- a/duniterpy/documents/transaction.py
+++ b/duniterpy/documents/transaction.py
@@ -12,7 +12,8 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
+import base64
+import logging
 import re
 from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
 
@@ -27,6 +28,7 @@ from ..constants import (
     TRANSACTION_HASH_REGEX,
 )
 from ..grammars import output
+from ..key import SigningKey, VerifyingKey
 from .block_uid import BlockUID
 from .document import Document, MalformedDocumentError
 
@@ -570,6 +572,7 @@ class Transaction(Document):
         self.outputs = outputs
         self.comment = comment
         self.time = time
+        self.signatures: List[str] = list()
 
     def __eq__(self, other: Any) -> bool:
         """
@@ -580,6 +583,7 @@ class Transaction(Document):
         return (
             self.version == other.version
             and self.currency == other.currency
+            and self.signature == other.signature
             and self.signatures == other.signatures
             and self.blockstamp == other.blockstamp
             and self.locktime == other.locktime
@@ -596,6 +600,7 @@ class Transaction(Document):
             (
                 self.version,
                 self.currency,
+                self.signature,
                 self.signatures,
                 self.blockstamp,
                 self.locktime,
@@ -732,7 +737,10 @@ Comment: {comment}
         )
 
         # return transaction with signatures
-        transaction.signatures = signatures
+        if len(signatures) > 1:
+            transaction.signatures = signatures
+        else:
+            transaction.signature = signatures[0]
         return transaction
 
     @classmethod
@@ -822,7 +830,11 @@ Comment: {comment}
         )
 
         # return transaction with signatures
-        transaction.signatures = signatures
+        if len(signatures) > 1:
+            transaction.signatures = signatures
+        else:
+            transaction.signature = signatures[0]
+
         return transaction
 
     def raw(self) -> str:
@@ -898,11 +910,74 @@ Currency: {1}
             doc += "{0}\n".format(o.inline())
         if self.comment != "":
             doc += "{0}\n".format(self.comment)
-        for s in self.signatures:
-            doc += "{0}\n".format(s)
+        if self.signature is not None:
+            doc += "{0}\n".format(self.signature)
+        else:
+            for signature in self.signatures:
+                doc += "{0}\n".format(signature)
 
         return doc
 
+    def signed_raw(self) -> str:
+        """
+        Return signed raw format string
+
+        :return:
+        """
+        if self.signature is None and len(self.signatures) == 0:
+            raise MalformedDocumentError("No signature, can not create raw format")
+        raw = self.raw()
+
+        if self.signature is not None:
+            signed_raw = raw + self.signature + "\n"
+        else:
+            signed_raw = raw
+            for signature in self.signatures:
+                signed_raw += "{0}\n".format(signature)
+
+        return signed_raw
+
+    def multi_sign(self, keys: List[SigningKey]) -> None:
+        """
+        Sign the current document with multiple keys
+
+        Warning : current signatures will be replaced with the new ones.
+
+        :param keys: List of libnacl keys instance
+        """
+        self.signatures = list()
+        for key in keys:
+            signature = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
+            logging.debug("Signature : \n%s", signature.decode("ascii"))
+            self.signatures.append(signature.decode("ascii"))
+
+    def check_signatures(self, pubkeys: str):
+        """
+        Check if the signatures matches pubkeys
+
+        :param pubkeys: List of Base58 public keys
+
+        :return:
+        """
+        if self.signature is None:
+            raise Exception("Signature is None, can not verify signature")
+
+        signature = base64.b64decode(self.signature)
+
+        content_to_verify = self.raw()
+        prepended = signature + bytes(content_to_verify, "ascii")
+
+        matches = 0
+        for pubkey in pubkeys:
+            verifying_key = VerifyingKey(pubkey)
+            try:
+                verifying_key.verify(prepended)
+                matches += 1
+            except ValueError:
+                pass
+
+        return matches == len(self.signatures)
+
 
 class SimpleTransaction(Transaction):
     """
diff --git a/duniterpy/documents/ws2p/heads.py b/duniterpy/documents/ws2p/heads.py
index a942b355..3158dce6 100644
--- a/duniterpy/documents/ws2p/heads.py
+++ b/duniterpy/documents/ws2p/heads.py
@@ -12,7 +12,7 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
+import base64
 import re
 
 import attr
@@ -26,6 +26,7 @@ from ...constants import (
     WS2P_PUBLIC_PREFIX_REGEX,
     WS2PID_REGEX,
 )
+from ...key import VerifyingKey
 from ..block_uid import BlockUID
 from ..document import MalformedDocumentError
 
@@ -126,6 +127,23 @@ class HeadV0(Head):
         )
         return "{0}:{1}".format(str(self.api), ":".join(values))
 
+    def check_signature(self) -> bool:
+        """
+        Check if Head signature if from head pubkey
+
+        :return:
+        """
+        signature = base64.b64decode(self.signature)
+        inline = self.inline()
+        prepended = signature + bytes(inline, "ascii")
+
+        verifying_key = VerifyingKey(self.pubkey)
+        try:
+            verifying_key.verify(prepended)
+            return True
+        except ValueError:
+            return False
+
 
 @attr.s()
 class HeadV1(HeadV0):
diff --git a/duniterpy/documents/ws2p/messages.py b/duniterpy/documents/ws2p/messages.py
index a42803aa..90b2c1c3 100644
--- a/duniterpy/documents/ws2p/messages.py
+++ b/duniterpy/documents/ws2p/messages.py
@@ -51,7 +51,7 @@ class HandshakeMessage(Document):
             self.challenge = challenge
 
         if signature is not None:
-            self.signatures = [signature]
+            self.signature = signature
             # verify signature
             verifying_key = VerifyingKey(self.pubkey)
             verifying_key.verify_document(self)
@@ -77,12 +77,12 @@ class HandshakeMessage(Document):
 
         :return:
         """
-        self.sign([signing_key])
+        self.sign(signing_key)
         data = {
             "auth": self.auth,
             "pub": self.pubkey,
             "challenge": self.challenge,
-            "sig": self.signatures[0],
+            "sig": self.signature,
         }
         return json.dumps(data)
 
@@ -122,8 +122,8 @@ class Ack(HandshakeMessage):
 
         :return:
         """
-        self.sign([signing_key])
-        data = {"auth": self.auth, "pub": self.pubkey, "sig": self.signatures[0]}
+        self.sign(signing_key)
+        data = {"auth": self.auth, "pub": self.pubkey, "sig": self.signature}
         return json.dumps(data)
 
 
@@ -155,8 +155,8 @@ class Ok(HandshakeMessage):
 
         :return:
         """
-        self.sign([signing_key])
-        data = {"auth": self.auth, "sig": self.signatures[0]}
+        self.sign(signing_key)
+        data = {"auth": self.auth, "sig": self.signature}
         return json.dumps(data)
 
 
diff --git a/duniterpy/helpers/network.py b/duniterpy/helpers/network.py
index 430838a7..c647d61b 100644
--- a/duniterpy/helpers/network.py
+++ b/duniterpy/helpers/network.py
@@ -93,7 +93,7 @@ def get_available_nodes(client: Client) -> List[List[Dict[str, Any]]]:
                 continue
 
             # set signature in Document
-            peer.signatures = [bma_peer["signature"]]
+            peer.signature = bma_peer["signature"]
             #  if peer signature not valid
             if VerifyingKey(head.pubkey).verify_document(peer) is False:
                 # skip this node
diff --git a/duniterpy/key/verifying_key.py b/duniterpy/key/verifying_key.py
index 18e23b4d..92600ab5 100644
--- a/duniterpy/key/verifying_key.py
+++ b/duniterpy/key/verifying_key.py
@@ -12,16 +12,9 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import base64
-from typing import Any
-
 import libnacl.encode
 import libnacl.sign
 
-from duniterpy.documents import Document
-from duniterpy.documents.block import Block
-
 from .base58 import Base58Encoder
 
 
@@ -38,46 +31,9 @@ class VerifyingKey(libnacl.sign.Verifier):
         key = libnacl.encode.hex_encode(Base58Encoder.decode(pubkey))
         super().__init__(key)
 
-    def verify_document(self, document: Document) -> bool:
-        """
-        Check specified document
-        :param duniterpy.documents.Document document:
-        :return:
-        """
-        signature = base64.b64decode(document.signatures[0])
-        if isinstance(document, Block):
-            content_to_verify = "InnerHash: {0}\nNonce: {1}\n".format(
-                document.inner_hash, document.nonce
-            )
-        else:
-            content_to_verify = document.raw()
-        prepended = signature + bytes(content_to_verify, "ascii")
-
-        try:
-            self.verify(prepended)
-            return True
-        except ValueError:
-            return False
-
-    def verify_ws2p_head(self, head: Any) -> bool:
-        """
-        Check specified document
-        :param Any head:
-        :return:
-        """
-        signature = base64.b64decode(head.signature)
-        inline = head.inline()
-        prepended = signature + bytes(inline, "ascii")
-
-        try:
-            self.verify(prepended)
-            return True
-        except ValueError:
-            return False
-
     def get_verified_data(self, data: bytes) -> bytes:
         """
-        Check specified signed data signature and return data
+        Check specified signed data signature and return data without signature
 
         Raise exception if signature is not valid
 
diff --git a/examples/save_revoke_document.py b/examples/save_revoke_document.py
index 511ac6e4..ae555a5d 100644
--- a/examples/save_revoke_document.py
+++ b/examples/save_revoke_document.py
@@ -87,7 +87,7 @@ def get_signed_raw_revocation_document(
     revocation = Revocation(PROTOCOL_VERSION, identity.currency, identity)
 
     key = SigningKey.from_credentials(salt, password)
-    revocation.sign([key])
+    revocation.sign(key)
     return revocation.signed_raw()
 
 
diff --git a/examples/send_identity.py b/examples/send_identity.py
index c777a75e..cefcecc5 100644
--- a/examples/send_identity.py
+++ b/examples/send_identity.py
@@ -60,7 +60,7 @@ def get_identity_document(
     )
 
     # sign document
-    identity.sign([key])
+    identity.sign(key)
 
     return identity
 
diff --git a/examples/send_membership.py b/examples/send_membership.py
index 14e60938..3d49beb9 100644
--- a/examples/send_membership.py
+++ b/examples/send_membership.py
@@ -68,7 +68,7 @@ def get_membership_document(
     )
 
     # sign document
-    membership.sign([key])
+    membership.sign(key)
 
     return membership
 
diff --git a/tests/documents/test_block.py b/tests/documents/test_block.py
index dfcf032e..b2639e20 100644
--- a/tests/documents/test_block.py
+++ b/tests/documents/test_block.py
@@ -1955,9 +1955,9 @@ AywstQpC0S5iaA/YQvbz2alpP6zTYG3tjkWpxy1jgeCo028Te2V327bBZbfDGDzsjxOrF4UVmEBiGsgb
 
     def test_block_signature(self):
         block = Block.from_signed_raw(raw_block_to_sign)
-        orig_sig = block.signatures[0]
-        block.sign([key_raw_block_to_sign])
-        self.assertEqual(orig_sig, block.signatures[0])
+        orig_sig = block.signature
+        block.sign(key_raw_block_to_sign)
+        self.assertEqual(orig_sig, block.signature)
 
     def test_from_parsed_json_block_0(self):
         parsed_json_block = json.loads(json_block_0)
diff --git a/tests/documents/test_certification.py b/tests/documents/test_certification.py
index 315be00a..e5fcee3b 100644
--- a/tests/documents/test_certification.py
+++ b/tests/documents/test_certification.py
@@ -50,7 +50,7 @@ class TestCertification(unittest.TestCase):
             selfcert.pubkey, "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"
         )
         self.assertEqual(
-            selfcert.signatures[0],
+            selfcert.signature,
             "h/H8tDIEbfA4yxMQcvfOXVDQhi1sUa9qYtPKrM59Bulv97ouwbAvAsEkC1Uyit1IOpeAV+CQQs4IaAyjE8F1Cw==",
         )
         self.assertEqual(
@@ -61,7 +61,7 @@ class TestCertification(unittest.TestCase):
         selfcert = Identity.from_inline(version, currency, selfcert_inlines[1])
         self.assertEqual(selfcert.pubkey, "RdrHvL179Rw62UuyBrqy2M1crx7RPajaViBatS59EGS")
         self.assertEqual(
-            selfcert.signatures[0],
+            selfcert.signature,
             "Ah55O8cvdkGS4at6AGOKUjy+wrFwAq8iKRJ5xLIb6Xdi3M8WfGOUdMjwZA6GlSkdtlMgEhQPm+r2PMebxKrCBg==",
         )
         self.assertEqual(
@@ -78,7 +78,7 @@ class TestCertification(unittest.TestCase):
         signature = "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci"
 
         selfcert = Identity(version, currency, issuer, uid, timestamp)
-        selfcert.signatures = [signature]
+        selfcert.signature = signature
 
         result = """Version: 2
 Type: Identity
@@ -104,7 +104,7 @@ J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBf
         self.assertEqual(cert.timestamp.number, 0)
         self.assertEqual(cert.timestamp.sha_hash, EMPTY_HASH)
         self.assertEqual(
-            cert.signatures[0],
+            cert.signature,
             "TgmDuMxZdyutroj9jiLJA8tQp/389JIzDKuxW5+h7GIfjDu1ZbwI7HNm5rlUDhR2KreaV/QJjEaItT4Cf75rCQ==",
         )
 
@@ -123,7 +123,7 @@ J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBf
             cert.timestamp.sha_hash, "DB30D958EE5CB75186972286ED3F4686B8A1C2CD"
         )
         self.assertEqual(
-            cert.signatures[0],
+            cert.signature,
             "qn/XNJjaGIwfnR+wGrDME6YviCQbG+ywsQWnETlAsL6q7o3k1UhpR5ZTVY9dvejLKuC+1mUEXVTmH+8Ib55DBA==",
         )
 
@@ -141,14 +141,12 @@ J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBf
             "lolcat",
             BlockUID(32, "DB30D958EE5CB75186972286ED3F4686B8A1C2CD"),
         )
-        identity.signatures = [
-            "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci"
-        ]
+        identity.signature = "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci"
 
         certification = Certification(
             version, currency, pubkey_from, identity, timestamp
         )
-        certification.signatures = [signature]
+        certification.signature = signature
 
         result = """Version: 2
 Type: Certification
@@ -174,7 +172,7 @@ SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneN
             revokation.pubkey, "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU"
         )
         self.assertEqual(
-            revokation.signatures[0],
+            revokation.signature,
             "TgmDuMxZdyutroj9jiLJA8tQp/389JIzDKuxW5+h7GIfjDu1ZbwI7HNm5rlUDhR2KreaV/QJjEaItT4Cf75rCQ==",
         )
 
@@ -190,12 +188,10 @@ SoKwoa8PFfCDJWZ6dNCv7XstezHcc2BbKiJgVDXv82R5zYR83nis9dShLgWJ5w48noVUHimdngzYQneN
             "lolcat",
             BlockUID(32, "DB30D958EE5CB75186972286ED3F4686B8A1C2CD"),
         )
-        identity.signatures = [
-            "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci"
-        ]
+        identity.signature = "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci"
 
         revokation = Revocation(version, currency, identity)
-        revokation.signatures = [signature]
+        revokation.signature = signature
 
         result = """Version: 2
 Type: Revocation
diff --git a/tests/documents/test_membership.py b/tests/documents/test_membership.py
index 8d9919bb..0fd90c9f 100644
--- a/tests/documents/test_membership.py
+++ b/tests/documents/test_membership.py
@@ -50,7 +50,7 @@ class TestMembership(unittest.TestCase):
         )
         self.assertEqual(membership.uid, "cgeek")
         self.assertEqual(
-            membership.signatures[0],
+            membership.signature,
             "dkaXIiCYUJtCg8Feh/BKvPYf4uFH9CJ/zY6J4MlA9BsjmcMe4YAblvNt/gJy31b1aGq3ue3h14mLMCu84rraDg==",
         )
         self.assertEqual(membership.membership_type, "IN")
@@ -71,7 +71,7 @@ class TestMembership(unittest.TestCase):
         )
         self.assertEqual(membership.uid, "cgeek")
         self.assertEqual(
-            membership.signatures[0],
+            membership.signature,
             "dkaXIiCYUJtCg8Feh/BKvPYf4uFH9CJ/zY6J4MlA9BsjmcMe4YAblvNt/gJy31b1aGq3ue3h14mLMCu84rraDg==",
         )
         self.assertEqual(membership.membership_type, "IN")
@@ -95,7 +95,7 @@ class TestMembership(unittest.TestCase):
         )
         self.assertEqual(from_rendered_membership.uid, "cgeek")
         self.assertEqual(
-            from_rendered_membership.signatures[0],
+            from_rendered_membership.signature,
             "dkaXIiCYUJtCg8Feh/BKvPYf4uFH9CJ/zY6J4MlA9BsjmcMe4YAblvNt/gJy31b1aGq3ue3h14mLMCu84rraDg==",
         )
         self.assertEqual(from_rendered_membership.membership_type, "IN")
diff --git a/tests/documents/test_peer.py b/tests/documents/test_peer.py
index 0c73c2ad..1a55014f 100644
--- a/tests/documents/test_peer.py
+++ b/tests/documents/test_peer.py
@@ -72,7 +72,7 @@ class TestPeer(unittest.TestCase):
         self.assertEqual(peer.endpoints[2].port, 20902)
 
         self.assertEqual(
-            peer.signatures[0],
+            peer.signature,
             "dkaXIiCYUJtCg8Feh/BKvPYf4uFH9CJ/zY6J4MlA9BsjmcMe4YAblvNt/gJy31b1aGq3ue3h14mLMCu84rraDg==",
         )
 
@@ -110,7 +110,7 @@ class TestPeer(unittest.TestCase):
         self.assertEqual(peer.endpoints[2].port, 20902)
 
         self.assertEqual(
-            from_rendered_peer.signatures[0],
+            from_rendered_peer.signature,
             "dkaXIiCYUJtCg8Feh/BKvPYf4uFH9CJ/zY6J4MlA9BsjmcMe4YAblvNt/gJy31b1aGq3ue3h14mLMCu84rraDg==",
         )
         self.assertEqual(rawpeer, from_rendered_peer.signed_raw())
diff --git a/tests/key/test_verifying_key.py b/tests/key/test_verifying_key.py
index 36684635..9bf1671e 100644
--- a/tests/key/test_verifying_key.py
+++ b/tests/key/test_verifying_key.py
@@ -54,8 +54,7 @@ BASIC_MERKLED_API testnet.duniter.inso.ovh 80
 """
         peer = Peer.from_signed_raw(signed_raw)
         pubkey = "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU"
-        verifying_key = VerifyingKey(pubkey)
-        self.assertTrue(verifying_key.verify_document(peer))
+        self.assertTrue(peer.check_signature(pubkey))
 
     def test_ws2p_headv0(self):
         headv0, _ = HeadV0.from_inline(
@@ -65,8 +64,7 @@ BASIC_MERKLED_API testnet.duniter.inso.ovh 80
             "+BaXzmArj7kwlItbdGUs4fc9QUG5Lp4TwPS7nhOM5t1Kt6CA==",
         )
 
-        verifying_key = VerifyingKey(headv0.pubkey)
-        self.assertTrue(verifying_key.verify_ws2p_head(headv0))
+        self.assertTrue(headv0.check_signature())
 
     def test_ws2p_headv1(self):
         headv1, _ = HeadV1.from_inline(
@@ -77,8 +75,7 @@ BASIC_MERKLED_API testnet.duniter.inso.ovh 80
             "+hdZRqf0iUWRNuhxlequ68kkwMaE6ymBw==",
         )
 
-        verifying_key = VerifyingKey(headv1.pubkey)
-        self.assertTrue(verifying_key.verify_ws2p_head(headv1))
+        self.assertTrue(headv1.check_signature())
 
     def test_ws2p_headv2(self):
         headv2, _ = HeadV2.from_inline(
@@ -89,8 +86,7 @@ BASIC_MERKLED_API testnet.duniter.inso.ovh 80
             "+yNoHonqBSqirAQ==",
         )
 
-        verifying_key = VerifyingKey(headv2.pubkey)
-        self.assertTrue(verifying_key.verify_ws2p_head(headv2))
+        self.assertTrue(headv2.check_signature())
 
     def test_block_document(self):
         block_document = """Version: 10
@@ -121,8 +117,7 @@ Nonce: 10300000099432
 """
         block_signature = "Uxa3L+/m/dWLex2xSh7Jv1beAn4f99BmoYAs7iX3Lr+t1l5jzJpd9m4iI1cHppIizCgbg6ztaiZedQ+Mp6KuDg=="
         block = Block.from_signed_raw(block_document + block_signature + "\n")
-        verifying_key = VerifyingKey(block.issuer)
-        self.assertTrue(verifying_key.verify_document(block))
+        self.assertTrue(block.check_signature(block.issuer))
 
     def test_transaction_document(self):
         transaction_document = """TX:10:1:6:6:2:1:0
@@ -146,5 +141,4 @@ Solde huile Millepertuis
 rgjOmzFH5h+hkDbJLk1b88X7Z83HMgTa5rBckeMSdF/yZtItN3zMn09MphcXjffdrKcK+MebwoisLJqV+jXrDg==
 """
         tx = Transaction.from_compact("g1", transaction_document)
-        verifying_key = VerifyingKey(tx.issuers[0])
-        self.assertTrue(verifying_key.verify_document(tx))
+        self.assertTrue(tx.check_signature(tx.issuers[0]))
-- 
GitLab