From 63d12fde3231a11ef40c5a40bd281771d053249d 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([keys]) method. verifying_key.py is refactored heavily to avoid circular reference: * method VerifyingKey.verify_document() removed, use Document.check_signature(pubkey) or Transaction.check_signatures(pubkeys) instead * method VerifyingKey.verify_ws2p_head() is removed, use Document.check_signature(pubkey) instead. * Add VerifyingKey.check_signature() method --- Makefile | 2 +- duniterpy/documents/block.py | 81 +++++++++++++++++++++----- duniterpy/documents/certification.py | 36 +++++------- duniterpy/documents/document.py | 46 +++++++++------ duniterpy/documents/identity.py | 12 ++-- duniterpy/documents/membership.py | 6 +- duniterpy/documents/peer.py | 6 +- duniterpy/documents/revocation.py | 34 +++++------ duniterpy/documents/transaction.py | 83 +++++++++++++++++++++++++-- duniterpy/documents/ws2p/heads.py | 13 ++++- duniterpy/documents/ws2p/messages.py | 20 +++---- duniterpy/helpers/network.py | 7 +-- duniterpy/key/verifying_key.py | 54 +++++------------ 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/documents/test_transaction.py | 38 ++++++++++++ tests/key/test_verifying_key.py | 24 ++++---- 22 files changed, 327 insertions(+), 181 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..b7db3730 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,21 @@ 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") + + content_to_verify = "InnerHash: {0}\nNonce: {1}\n".format( + self.inner_hash, self.nonce + ) + + verifying_key = VerifyingKey(pubkey) + return verifying_key.check_signature(content_to_verify, self.signature) diff --git a/duniterpy/documents/certification.py b/duniterpy/documents/certification.py index 58d2b1f8..3f87f6d3 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") @@ -113,7 +113,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 @@ -151,7 +151,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: @@ -179,28 +179,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: """ @@ -210,12 +203,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: @@ -225,5 +219,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..22e14651 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,18 @@ class Document: :return: """ return hashlib.sha256(self.signed_raw().encode("ascii")).hexdigest().upper() + + def check_signature(self, pubkey: str) -> bool: + """ + 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 check signature") + + verifying_key = VerifyingKey(pubkey) + + return verifying_key.check_signature(self.raw(), self.signature) diff --git a/duniterpy/documents/identity.py b/duniterpy/documents/identity.py index 2ffe7534..de4c003a 100644 --- a/duniterpy/documents/identity.py +++ b/duniterpy/documents/identity.py @@ -117,7 +117,7 @@ class Identity(Document): identity = cls(pubkey, uid, timestamp, version=version, currency=currency) # return identity with signature - identity.signatures = [signature] + identity.signature = signature return identity @classmethod @@ -154,7 +154,7 @@ class Identity(Document): identity = cls(pubkey, uid, timestamp, version=version, currency=currency) # 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 @@ -295,7 +295,7 @@ Timestamp: {timestamp} uid=uid, timestamp=timestamp, ) - identity.signatures = [signature] + identity.signature = signature if identity is None: raise Exception("Identity pubkey not found") 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..c384f774 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,72 @@ 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: List[str]): + """ + Check if the signatures matches pubkeys + + :param pubkeys: List of Base58 public keys + + :return: + """ + if len(self.signatures) == 0: + raise Exception("No signatures, can not check signatures") + + if len(self.signatures) != len(pubkeys): + raise Exception("Number of pubkeys not equal to number of signatures") + + content_to_verify = self.raw() + + matches = 0 + for pubkey in pubkeys: + verifying_key = VerifyingKey(pubkey) + for signature in self.signatures: + if verifying_key.check_signature(content_to_verify, signature) is True: + matches += 1 + + return matches == len(self.signatures) + class SimpleTransaction(Transaction): """ diff --git a/duniterpy/documents/ws2p/heads.py b/duniterpy/documents/ws2p/heads.py index a942b355..c2ec8b58 100644 --- a/duniterpy/documents/ws2p/heads.py +++ b/duniterpy/documents/ws2p/heads.py @@ -12,7 +12,6 @@ # # 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 re import attr @@ -26,6 +25,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 +126,17 @@ class HeadV0(Head): ) return "{0}:{1}".format(str(self.api), ":".join(values)) + def check_signature(self, pubkey: str) -> bool: + """ + Check if Head signature is from head pubkey + + :param pubkey: Pubkey to check signature upon + :return: + """ + verifying_key = VerifyingKey(pubkey) + + return verifying_key.check_signature(self.inline(), self.signature) + @attr.s() class HeadV1(HeadV0): diff --git a/duniterpy/documents/ws2p/messages.py b/duniterpy/documents/ws2p/messages.py index a42803aa..1a7b75a2 100644 --- a/duniterpy/documents/ws2p/messages.py +++ b/duniterpy/documents/ws2p/messages.py @@ -17,7 +17,7 @@ import json from typing import Optional from duniterpy.documents import Document -from duniterpy.key import SigningKey, VerifyingKey +from duniterpy.key import SigningKey from duniterpy.tools import get_ws2p_challenge @@ -51,10 +51,8 @@ class HandshakeMessage(Document): self.challenge = challenge if signature is not None: - self.signatures = [signature] - # verify signature - verifying_key = VerifyingKey(self.pubkey) - verifying_key.verify_document(self) + self.signature = signature + self.check_signature(pubkey) def raw(self): """ @@ -77,12 +75,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 +120,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 +153,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..dc8dc635 100644 --- a/duniterpy/helpers/network.py +++ b/duniterpy/helpers/network.py @@ -20,7 +20,6 @@ from duniterpy.api import bma from duniterpy.api.client import Client from duniterpy.documents.peer import MalformedDocumentError, Peer from duniterpy.documents.ws2p.heads import HeadV2 -from duniterpy.key import VerifyingKey def get_available_nodes(client: Client) -> List[List[Dict[str, Any]]]: @@ -68,7 +67,7 @@ def get_available_nodes(client: Client) -> List[List[Dict[str, Any]]]: for head in list(group): # if head signature not valid... - if VerifyingKey(head.pubkey).verify_ws2p_head(head) is False: + if head.check_signature(head.pubkey) is False: # skip this node continue @@ -93,9 +92,9 @@ 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: + if peer.check_signature(head.pubkey) is False: # skip this node continue diff --git a/duniterpy/key/verifying_key.py b/duniterpy/key/verifying_key.py index 18e23b4d..c88680e5 100644 --- a/duniterpy/key/verifying_key.py +++ b/duniterpy/key/verifying_key.py @@ -12,16 +12,11 @@ # # 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,50 +33,29 @@ class VerifyingKey(libnacl.sign.Verifier): key = libnacl.encode.hex_encode(Base58Encoder.decode(pubkey)) super().__init__(key) - def verify_document(self, document: Document) -> bool: + def get_verified_data(self, data: bytes) -> bytes: """ - Check specified document - :param duniterpy.documents.Document document: + Check specified signed data signature and return data without signature + + Raise exception if signature is not valid + + :param data: Data + signature :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 + return self.verify(data) - def verify_ws2p_head(self, head: Any) -> bool: + def check_signature(self, data: str, signature: str) -> bool: """ - Check specified document - :param Any head: + Check if data signature is valid and from self.pubkey + + :param data: Data to check + :param signature: Signature to check :return: """ - signature = base64.b64decode(head.signature) - inline = head.inline() - prepended = signature + bytes(inline, "ascii") - + prepended = base64.b64decode(signature) + bytes(data, "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 - - Raise exception if signature is not valid - - :param data: Data + signature - :return: - """ - return self.verify(data) + return True diff --git a/examples/save_revoke_document.py b/examples/save_revoke_document.py index f7545004..c67a478b 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/documents/test_transaction.py b/tests/documents/test_transaction.py index 78367187..1adbcc41 100644 --- a/tests/documents/test_transaction.py +++ b/tests/documents/test_transaction.py @@ -28,6 +28,7 @@ from duniterpy.documents.transaction import ( reduce_base, ) from duniterpy.grammars import output +from duniterpy.key import SigningKey compact_change = """TX:10:1:1:1:1:1:0 13410-000041DF0CCA173F09B5FBA48F619D4BC934F12ADF1D0B798639EB2149C4A8CC @@ -452,3 +453,40 @@ class TestTransaction(unittest.TestCase): unlock1 = Unlock(0, [SIGParameter(0)]) unlock2 = Unlock.from_inline("0:SIG(0)") self.assertEqual(unlock1, unlock2) + + def test_check_signatures(self): + # create 3 wallets + signing_key_01 = SigningKey.from_credentials("1", "1") + signing_key_02 = SigningKey.from_credentials("2", "2") + signing_key_03 = SigningKey.from_credentials("3", "3") + + issuers = [ + signing_key_01.pubkey, + signing_key_02.pubkey, + signing_key_03.pubkey, + ] + + transaction = Transaction( + version=12, + currency="g1-test", + blockstamp=BlockUID( + 8979, "000041DF0CCA173F09B5FBA48F619D4BC934F12ADF1D0B798639EB2149C4A8CC" + ), + locktime=0, + issuers=issuers, + inputs=[InputSource.from_inline(input_source_str)], + unlocks=[Unlock(index=0, parameters=[SIGParameter(0)])], + outputs=[OutputSource.from_inline(output_source_str)], + comment="", + ) + + # multi-signature on the transaction + transaction.multi_sign( + [ + signing_key_01, + signing_key_02, + signing_key_03, + ] + ) + + self.assertTrue(transaction.check_signatures(issuers)) diff --git a/tests/key/test_verifying_key.py b/tests/key/test_verifying_key.py index 36684635..d0a3e79e 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,9 @@ 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("3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj") + ) def test_ws2p_headv1(self): headv1, _ = HeadV1.from_inline( @@ -77,8 +77,9 @@ 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("HbTqJ1Ts3RhJ8Rx4XkNyh1oSKmoZL1kY5U7t9mKTSjAB") + ) def test_ws2p_headv2(self): headv2, _ = HeadV2.from_inline( @@ -89,8 +90,9 @@ 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("D3krfq6J9AmfpKnS3gQVYoy7NzGCc61vokteTS8LJ4YH") + ) def test_block_document(self): block_document = """Version: 10 @@ -121,8 +123,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 +147,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