From 5f364fd9e2d38fcfe5b54168e65cdcd903a0a295 Mon Sep 17 00:00:00 2001 From: Olivier Desenfans Date: Sat, 24 Sep 2022 23:23:37 +0200 Subject: [PATCH 1/4] Feature: support Micheline-style signatures for Tezos Problem: web wallets do not allow signing raw messages. Instead, they require binary payloads in a specific format. Solution: support Micheline-style signatures, i.e. signatures supported by wallets like Beacon. Users can now use Micheline or raw signatures by specifying the `signature.signingType` field to "micheline" or "raw". By default, "raw" is assumed. --- src/aleph/chains/tezos.py | 80 +++++++++++++++++++++++++++++++++++++- tests/chains/test_tezos.py | 24 +++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/aleph/chains/tezos.py b/src/aleph/chains/tezos.py index 765f1ddc7..ae41e08ef 100644 --- a/src/aleph/chains/tezos.py +++ b/src/aleph/chains/tezos.py @@ -1,5 +1,7 @@ +import datetime as dt import json import logging +from enum import Enum from aleph_pytezos.crypto.key import Key @@ -10,13 +12,82 @@ LOGGER = logging.getLogger(__name__) CHAIN_NAME = "TEZOS" +# Default dApp URI for Micheline-style signatures +DEFAULT_DAPP_URI = "aleph.im" + + +class TezosSignatureType(str, Enum): + RAW = "raw" + MICHELINE = "micheline" + + +def timestamp_to_iso_8601(timestamp: float) -> str: + """ + Returns the timestamp formatted to ISO-8601, JS-style. + + Compared to the regular `isoformat()`, this function only provides precision down + to milliseconds and prints a "Z" instead of +0000 for UTC. + This format is typically used by JavaScript applications, like our TS SDK. + + Example: 2022-09-23T14:41:19.029Z + + :param timestamp: The timestamp to format. + :return: The formatted timestamp. + """ + + return ( + dt.datetime.utcfromtimestamp(timestamp).isoformat(timespec="milliseconds") + "Z" + ) + + +def micheline_verification_buffer( + verification_buffer: bytes, + timestamp: float, + dapp_uri: str, +) -> bytes: + """ + Computes the verification buffer for Micheline-type signatures. + + This verification buffer is used when signing data with a Tezos web wallet. + See https://tezostaquito.io/docs/signing/#generating-a-signature-with-beacon-sdk. + + :param verification_buffer: The original (non-Tezos) verification buffer for the Aleph message. + :param timestamp: Timestamp of the message. + :return: The verification buffer used for the signature by the web wallet. + """ + + prefix = b"Tezos Signed Message:" + timestamp = timestamp_to_iso_8601(timestamp).encode("utf-8") + + payload = b" ".join( + (prefix, dapp_uri.encode("utf-8"), timestamp, verification_buffer) + ) + hex_encoded_payload = payload.hex() + payload_size = str(len(hex_encoded_payload)).encode("utf-8") + + return b"\x05" + b"\x01\x00" + payload_size + payload + + +def get_tezos_verification_buffer( + message: BasePendingMessage, signature_type: TezosSignatureType, dapp_uri: str +) -> bytes: + verification_buffer = get_verification_buffer(message) + + if signature_type == TezosSignatureType.RAW: + return verification_buffer + elif signature_type == TezosSignatureType.MICHELINE: + return micheline_verification_buffer( + verification_buffer, message.time, dapp_uri + ) + + raise ValueError(f"Unsupported signature type: {signature_type}") + async def verify_signature(message: BasePendingMessage) -> bool: """ Verifies the cryptographic signature of a message signed with a Tezos key. """ - verification_buffer = get_verification_buffer(message) try: signature_dict = json.loads(message.signature) except json.JSONDecodeError: @@ -30,6 +101,9 @@ async def verify_signature(message: BasePendingMessage) -> bool: LOGGER.exception("'%s' key missing from Tezos signature dictionary.", e.args[0]) return False + signature_type = TezosSignatureType(signature_dict.get("signingType", "raw")) + dapp_uri = signature_dict.get("dappUri", DEFAULT_DAPP_URI) + key = Key.from_encoded_key(public_key) # Check that the sender ID is equal to the public key hash public_key_hash = key.public_key_hash() @@ -41,6 +115,10 @@ async def verify_signature(message: BasePendingMessage) -> bool: public_key_hash, ) + verification_buffer = get_tezos_verification_buffer( + message, signature_type, dapp_uri + ) + # Check the signature try: key.verify(signature, verification_buffer) diff --git a/tests/chains/test_tezos.py b/tests/chains/test_tezos.py index 4c6be4c37..14ca20402 100644 --- a/tests/chains/test_tezos.py +++ b/tests/chains/test_tezos.py @@ -2,10 +2,13 @@ from aleph.network import verify_signature from aleph.schemas.pending_messages import parse_message +from aleph.chains import ( + tezos, +) # TODO: this import is currently necessary because of circular dependencies @pytest.mark.asyncio -async def test_tezos_verify_signature(): +async def test_tezos_verify_signature_raw(): message_dict = { "chain": "TEZOS", "channel": "TEST", @@ -28,7 +31,7 @@ async def test_tezos_verify_signature(): @pytest.mark.asyncio -async def test_tezos_verify_signature_ed25519(): +async def test_tezos_verify_signature_raw_ed25519(): message_dict = { "chain": "TEZOS", "sender": "tz1SmGHzna3YhKropa3WudVq72jhTPDBn4r5", @@ -43,3 +46,20 @@ async def test_tezos_verify_signature_ed25519(): message = parse_message(message_dict) await verify_signature(message) + + +@pytest.mark.asyncio +async def test_tezos_verify_signature_micheline(): + message_dict = { + "chain": "TEZOS", + "sender": "tz1VrPqrVdMFsgykWyhGH7SYcQ9avHTjPcdD", + "type": "POST", + "channel": "ALEPH-TEST", + "signature": '{"signingType":"micheline","signature":"sigXD8iT5ivdawgPzE1AbtDwqqAjJhS5sHS1psyE74YjfiaQnxWZsATNjncdsuQw3b9xaK79krxtsC8uQoT5TcUXmo66aovT","publicKey":"edpkvapDnjnasrNcmUdMZXhQZwpX6viPyuGCq6nrP4W7ZJCm7EFTpS"}', + "time": 1663944079.029, + "item_type": "storage", + "item_hash": "72b2722b95582419cfa71f631ff6c6afc56344dc6a4609e772877621813040b7", + } + + message = parse_message(message_dict) + await verify_signature(message) From 25cce5e88fec50b86d9c55add0d71f6b76d899fc Mon Sep 17 00:00:00 2001 From: Mike Hukiewitz <70762838+MHHukiewitz@users.noreply.github.com> Date: Mon, 26 Sep 2022 13:42:12 +0200 Subject: [PATCH 2/4] Update src/aleph/chains/tezos.py minum required change --- src/aleph/chains/tezos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/chains/tezos.py b/src/aleph/chains/tezos.py index ae41e08ef..fe27f6f4a 100644 --- a/src/aleph/chains/tezos.py +++ b/src/aleph/chains/tezos.py @@ -102,7 +102,7 @@ async def verify_signature(message: BasePendingMessage) -> bool: return False signature_type = TezosSignatureType(signature_dict.get("signingType", "raw")) - dapp_uri = signature_dict.get("dappUri", DEFAULT_DAPP_URI) + dapp_uri = signature_dict.get("dAppUrl", DEFAULT_DAPP_URI) key = Key.from_encoded_key(public_key) # Check that the sender ID is equal to the public key hash From 86c5a027ff1336a174a7c22b5181529eb71ad530 Mon Sep 17 00:00:00 2001 From: Olivier Desenfans Date: Mon, 26 Sep 2022 13:47:52 +0200 Subject: [PATCH 3/4] fixes for review --- src/aleph/chains/tezos.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/aleph/chains/tezos.py b/src/aleph/chains/tezos.py index fe27f6f4a..947c52b69 100644 --- a/src/aleph/chains/tezos.py +++ b/src/aleph/chains/tezos.py @@ -12,8 +12,8 @@ LOGGER = logging.getLogger(__name__) CHAIN_NAME = "TEZOS" -# Default dApp URI for Micheline-style signatures -DEFAULT_DAPP_URI = "aleph.im" +# Default dApp URL for Micheline-style signatures +DEFAULT_DAPP_URL = "aleph.im" class TezosSignatureType(str, Enum): @@ -43,7 +43,7 @@ def timestamp_to_iso_8601(timestamp: float) -> str: def micheline_verification_buffer( verification_buffer: bytes, timestamp: float, - dapp_uri: str, + dapp_url: str, ) -> bytes: """ Computes the verification buffer for Micheline-type signatures. @@ -53,6 +53,7 @@ def micheline_verification_buffer( :param verification_buffer: The original (non-Tezos) verification buffer for the Aleph message. :param timestamp: Timestamp of the message. + :param dapp_url: The URL of the dApp, for use as part of the verification buffer. :return: The verification buffer used for the signature by the web wallet. """ @@ -60,7 +61,7 @@ def micheline_verification_buffer( timestamp = timestamp_to_iso_8601(timestamp).encode("utf-8") payload = b" ".join( - (prefix, dapp_uri.encode("utf-8"), timestamp, verification_buffer) + (prefix, dapp_url.encode("utf-8"), timestamp, verification_buffer) ) hex_encoded_payload = payload.hex() payload_size = str(len(hex_encoded_payload)).encode("utf-8") @@ -69,7 +70,7 @@ def micheline_verification_buffer( def get_tezos_verification_buffer( - message: BasePendingMessage, signature_type: TezosSignatureType, dapp_uri: str + message: BasePendingMessage, signature_type: TezosSignatureType, dapp_url: str ) -> bytes: verification_buffer = get_verification_buffer(message) @@ -77,7 +78,7 @@ def get_tezos_verification_buffer( return verification_buffer elif signature_type == TezosSignatureType.MICHELINE: return micheline_verification_buffer( - verification_buffer, message.time, dapp_uri + verification_buffer, message.time, dapp_url ) raise ValueError(f"Unsupported signature type: {signature_type}") @@ -102,7 +103,7 @@ async def verify_signature(message: BasePendingMessage) -> bool: return False signature_type = TezosSignatureType(signature_dict.get("signingType", "raw")) - dapp_uri = signature_dict.get("dAppUrl", DEFAULT_DAPP_URI) + dapp_url = signature_dict.get("dappUrl", DEFAULT_DAPP_URL) key = Key.from_encoded_key(public_key) # Check that the sender ID is equal to the public key hash @@ -116,7 +117,7 @@ async def verify_signature(message: BasePendingMessage) -> bool: ) verification_buffer = get_tezos_verification_buffer( - message, signature_type, dapp_uri + message, signature_type, dapp_url ) # Check the signature From 62ca2b67f5d309107aa78eb43c0d35fcf6abaee3 Mon Sep 17 00:00:00 2001 From: Mike Hukiewitz <70762838+MHHukiewitz@users.noreply.github.com> Date: Mon, 26 Sep 2022 14:10:40 +0200 Subject: [PATCH 4/4] revert reading field: now "dAppUrl" --- src/aleph/chains/tezos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/chains/tezos.py b/src/aleph/chains/tezos.py index 947c52b69..f061378c9 100644 --- a/src/aleph/chains/tezos.py +++ b/src/aleph/chains/tezos.py @@ -103,7 +103,7 @@ async def verify_signature(message: BasePendingMessage) -> bool: return False signature_type = TezosSignatureType(signature_dict.get("signingType", "raw")) - dapp_url = signature_dict.get("dappUrl", DEFAULT_DAPP_URL) + dapp_url = signature_dict.get("dAppUrl", DEFAULT_DAPP_URL) key = Key.from_encoded_key(public_key) # Check that the sender ID is equal to the public key hash