Skip to content

Commit e8bc64e

Browse files
Feature: support Micheline-style signatures for Tezos (#330)
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. Co-authored-by: Mike Hukiewitz <[email protected]>
1 parent e17a06c commit e8bc64e

File tree

2 files changed

+102
-3
lines changed

2 files changed

+102
-3
lines changed

src/aleph/chains/tezos.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import datetime as dt
12
import json
23
import logging
4+
from enum import Enum
35

46
from aleph_pytezos.crypto.key import Key
57

@@ -10,13 +12,83 @@
1012
LOGGER = logging.getLogger(__name__)
1113
CHAIN_NAME = "TEZOS"
1214

15+
# Default dApp URL for Micheline-style signatures
16+
DEFAULT_DAPP_URL = "aleph.im"
17+
18+
19+
class TezosSignatureType(str, Enum):
20+
RAW = "raw"
21+
MICHELINE = "micheline"
22+
23+
24+
def timestamp_to_iso_8601(timestamp: float) -> str:
25+
"""
26+
Returns the timestamp formatted to ISO-8601, JS-style.
27+
28+
Compared to the regular `isoformat()`, this function only provides precision down
29+
to milliseconds and prints a "Z" instead of +0000 for UTC.
30+
This format is typically used by JavaScript applications, like our TS SDK.
31+
32+
Example: 2022-09-23T14:41:19.029Z
33+
34+
:param timestamp: The timestamp to format.
35+
:return: The formatted timestamp.
36+
"""
37+
38+
return (
39+
dt.datetime.utcfromtimestamp(timestamp).isoformat(timespec="milliseconds") + "Z"
40+
)
41+
42+
43+
def micheline_verification_buffer(
44+
verification_buffer: bytes,
45+
timestamp: float,
46+
dapp_url: str,
47+
) -> bytes:
48+
"""
49+
Computes the verification buffer for Micheline-type signatures.
50+
51+
This verification buffer is used when signing data with a Tezos web wallet.
52+
See https://tezostaquito.io/docs/signing/#generating-a-signature-with-beacon-sdk.
53+
54+
:param verification_buffer: The original (non-Tezos) verification buffer for the Aleph message.
55+
:param timestamp: Timestamp of the message.
56+
:param dapp_url: The URL of the dApp, for use as part of the verification buffer.
57+
:return: The verification buffer used for the signature by the web wallet.
58+
"""
59+
60+
prefix = b"Tezos Signed Message:"
61+
timestamp = timestamp_to_iso_8601(timestamp).encode("utf-8")
62+
63+
payload = b" ".join(
64+
(prefix, dapp_url.encode("utf-8"), timestamp, verification_buffer)
65+
)
66+
hex_encoded_payload = payload.hex()
67+
payload_size = str(len(hex_encoded_payload)).encode("utf-8")
68+
69+
return b"\x05" + b"\x01\x00" + payload_size + payload
70+
71+
72+
def get_tezos_verification_buffer(
73+
message: BasePendingMessage, signature_type: TezosSignatureType, dapp_url: str
74+
) -> bytes:
75+
verification_buffer = get_verification_buffer(message)
76+
77+
if signature_type == TezosSignatureType.RAW:
78+
return verification_buffer
79+
elif signature_type == TezosSignatureType.MICHELINE:
80+
return micheline_verification_buffer(
81+
verification_buffer, message.time, dapp_url
82+
)
83+
84+
raise ValueError(f"Unsupported signature type: {signature_type}")
85+
1386

1487
async def verify_signature(message: BasePendingMessage) -> bool:
1588
"""
1689
Verifies the cryptographic signature of a message signed with a Tezos key.
1790
"""
1891

19-
verification_buffer = get_verification_buffer(message)
2092
try:
2193
signature_dict = json.loads(message.signature)
2294
except json.JSONDecodeError:
@@ -30,6 +102,9 @@ async def verify_signature(message: BasePendingMessage) -> bool:
30102
LOGGER.exception("'%s' key missing from Tezos signature dictionary.", e.args[0])
31103
return False
32104

105+
signature_type = TezosSignatureType(signature_dict.get("signingType", "raw"))
106+
dapp_url = signature_dict.get("dAppUrl", DEFAULT_DAPP_URL)
107+
33108
key = Key.from_encoded_key(public_key)
34109
# Check that the sender ID is equal to the public key hash
35110
public_key_hash = key.public_key_hash()
@@ -41,6 +116,10 @@ async def verify_signature(message: BasePendingMessage) -> bool:
41116
public_key_hash,
42117
)
43118

119+
verification_buffer = get_tezos_verification_buffer(
120+
message, signature_type, dapp_url
121+
)
122+
44123
# Check the signature
45124
try:
46125
key.verify(signature, verification_buffer)

tests/chains/test_tezos.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
from aleph.network import verify_signature
44
from aleph.schemas.pending_messages import parse_message
5+
from aleph.chains import (
6+
tezos,
7+
) # TODO: this import is currently necessary because of circular dependencies
58

69

710
@pytest.mark.asyncio
8-
async def test_tezos_verify_signature():
11+
async def test_tezos_verify_signature_raw():
912
message_dict = {
1013
"chain": "TEZOS",
1114
"channel": "TEST",
@@ -28,7 +31,7 @@ async def test_tezos_verify_signature():
2831

2932

3033
@pytest.mark.asyncio
31-
async def test_tezos_verify_signature_ed25519():
34+
async def test_tezos_verify_signature_raw_ed25519():
3235
message_dict = {
3336
"chain": "TEZOS",
3437
"sender": "tz1SmGHzna3YhKropa3WudVq72jhTPDBn4r5",
@@ -43,3 +46,20 @@ async def test_tezos_verify_signature_ed25519():
4346

4447
message = parse_message(message_dict)
4548
await verify_signature(message)
49+
50+
51+
@pytest.mark.asyncio
52+
async def test_tezos_verify_signature_micheline():
53+
message_dict = {
54+
"chain": "TEZOS",
55+
"sender": "tz1VrPqrVdMFsgykWyhGH7SYcQ9avHTjPcdD",
56+
"type": "POST",
57+
"channel": "ALEPH-TEST",
58+
"signature": '{"signingType":"micheline","signature":"sigXD8iT5ivdawgPzE1AbtDwqqAjJhS5sHS1psyE74YjfiaQnxWZsATNjncdsuQw3b9xaK79krxtsC8uQoT5TcUXmo66aovT","publicKey":"edpkvapDnjnasrNcmUdMZXhQZwpX6viPyuGCq6nrP4W7ZJCm7EFTpS"}',
59+
"time": 1663944079.029,
60+
"item_type": "storage",
61+
"item_hash": "72b2722b95582419cfa71f631ff6c6afc56344dc6a4609e772877621813040b7",
62+
}
63+
64+
message = parse_message(message_dict)
65+
await verify_signature(message)

0 commit comments

Comments
 (0)