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