From fdfd3e20b7ee7972d3e1ad02705d0d1e4c86e89f Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Thu, 5 Oct 2023 21:23:19 -0400 Subject: [PATCH 1/8] Added extended signing key support for cip8 --- pycardano/cip/cip8.py | 41 +++++++++++++++++++++++++++---- test/pycardano/test_cip8.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/pycardano/cip/cip8.py b/pycardano/cip/cip8.py index 67536d35..691a2c3c 100644 --- a/pycardano/cip/cip8.py +++ b/pycardano/cip/cip8.py @@ -1,5 +1,7 @@ from typing import Optional, Union +from cbor2 import CBORTag, dumps + from cose.algorithms import EdDSA from cose.headers import KID, Algorithm from cose.keys import CoseKey @@ -10,9 +12,12 @@ from cose.messages import CoseMessage, Sign1Message from pycardano.address import Address +from pycardano.crypto import BIP32ED25519PublicKey from pycardano.key import ( PaymentVerificationKey, SigningKey, + ExtendedSigningKey, + ExtendedVerificationKey, StakeExtendedSigningKey, StakeSigningKey, StakeVerificationKey, @@ -25,7 +30,7 @@ def sign( message: str, - signing_key: SigningKey, + signing_key: Union[ExtendedSigningKey, SigningKey], attach_cose_key: bool = False, network: Network = Network.MAINNET, ) -> Union[str, dict]: @@ -45,7 +50,9 @@ def sign( """ # derive the verification key - verification_key = VerificationKey.from_signing_key(signing_key) + verification_key = signing_key.to_verification_key() + if isinstance(verification_key, ExtendedVerificationKey): + verification_key = verification_key.to_non_extended() if isinstance(signing_key, StakeSigningKey) or isinstance( signing_key, StakeExtendedSigningKey @@ -85,7 +92,20 @@ def sign( msg.key = cose_key # attach the key to the message - encoded = msg.encode() + if isinstance(signing_key, ExtendedSigningKey): + message = [ + msg.phdr_encoded, + msg.uhdr_encoded, + msg.payload, + signing_key.sign(msg._sig_structure), + ] + + encoded = dumps( + CBORTag(msg.cbor_tag, message), default=msg._custom_cbor_encoder + ) + + else: + encoded = msg.encode() # turn the enocded message into a hex string and remove the first byte # which is always "d2" @@ -108,7 +128,9 @@ def sign( def verify( - signed_message: Union[str, dict], attach_cose_key: Optional[bool] = None + signed_message: Union[str, dict], + attach_cose_key: Optional[bool] = None, + extended=False, ) -> dict: """Verify the signature of a COSESign1 message and decode its contents following CIP-0008. Supports messages signed by browser wallets or `Message.sign()`. @@ -175,7 +197,16 @@ def verify( # attach the key to the decoded message decoded_message.key = cose_key - signature_verified = decoded_message.verify_signature() + if extended: + vk = BIP32ED25519PublicKey( + public_key=verification_key[:32], chain_code=verification_key[32:] + ) + vk.verify( + signature=decoded_message.signature, message=decoded_message._sig_structure + ) + signature_verified = True + else: + signature_verified = decoded_message.verify_signature() message = decoded_message.payload.decode("utf-8") diff --git a/test/pycardano/test_cip8.py b/test/pycardano/test_cip8.py index 035d7b10..631715bb 100644 --- a/test/pycardano/test_cip8.py +++ b/test/pycardano/test_cip8.py @@ -1,5 +1,8 @@ from pycardano.cip.cip8 import sign, verify +from pycardano.crypto.bip32 import BIP32ED25519PrivateKey, HDWallet from pycardano.key import ( + ExtendedSigningKey, + ExtendedVerificationKey, PaymentSigningKey, PaymentVerificationKey, StakeSigningKey, @@ -7,6 +10,24 @@ ) from pycardano.network import Network + +EXTENDED_SK = ExtendedSigningKey.from_json( + """{ + "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32", + "description": "Payment Signing Key", + "cborHex": "5880e8428867ab9cc9304379a3ce0c238a592bd6d2349d2ebaf8a6ed2c6d2974a15ad59c74b6d8fa3edd032c6261a73998b7deafe983b6eeaff8b6fb3fab06bdf8019b693a62bce7a3cad1b9c02d22125767201c65db27484bb67d3cee7df7288d62c099ac0ce4a215355b149fd3114a2a7ef0438f01f8872c4487a61b469e26aae4" + }""" +) + +EXTENDED_VK = ExtendedVerificationKey.from_json( + """{ + "type": "PaymentExtendedVerificationKeyShelley_ed25519_bip32", + "description": "Payment Verification Key", + "cborHex": "58409b693a62bce7a3cad1b9c02d22125767201c65db27484bb67d3cee7df7288d62c099ac0ce4a215355b149fd3114a2a7ef0438f01f8872c4487a61b469e26aae4" + }""" +) + + SK = PaymentSigningKey.from_json( """{ "type": "GenesisUTxOSigningKey_ed25519", @@ -138,6 +159,33 @@ def test_sign_and_verify(): assert verification["signing_address"].payment_part == VK.hash() +def test_extended_sign_and_verify(): + # try first with no cose key attached + + message = "Pycardano is cool." + signed_message = sign( + message, + signing_key=EXTENDED_SK, + attach_cose_key=False, + network=Network.TESTNET, + ) + + verification = verify(signed_message, extended=True) + assert verification["verified"] + assert verification["message"] == "Pycardano is cool." + assert verification["signing_address"].payment_part == EXTENDED_VK.hash() + + # try again but attach cose key + signed_message = sign( + message, signing_key=EXTENDED_SK, attach_cose_key=True, network=Network.TESTNET + ) + + verification = verify(signed_message) + assert verification["verified"] + assert verification["message"] == "Pycardano is cool." + assert verification["signing_address"].payment_part == EXTENDED_VK.hash() + + def test_sign_and_verify_stake(): # try first with no cose key attached message = "Pycardano is cool." From 01230ac1b99c6b1d7f87d891229376e198f108bb Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Fri, 6 Oct 2023 09:31:39 -0400 Subject: [PATCH 2/8] Fixed unused imports, flake8 checks pass. --- pycardano/cip/cip8.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pycardano/cip/cip8.py b/pycardano/cip/cip8.py index 691a2c3c..14508632 100644 --- a/pycardano/cip/cip8.py +++ b/pycardano/cip/cip8.py @@ -21,7 +21,6 @@ StakeExtendedSigningKey, StakeSigningKey, StakeVerificationKey, - VerificationKey, ) from pycardano.network import Network From 143e843e3c6386837aa346cc72f2504c89ce66db Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Wed, 11 Oct 2023 09:12:09 -0400 Subject: [PATCH 3/8] Fixed mypy error for overloaded variable --- pycardano/cip/cip8.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycardano/cip/cip8.py b/pycardano/cip/cip8.py index 14508632..68ff0305 100644 --- a/pycardano/cip/cip8.py +++ b/pycardano/cip/cip8.py @@ -92,7 +92,7 @@ def sign( msg.key = cose_key # attach the key to the message if isinstance(signing_key, ExtendedSigningKey): - message = [ + _message = [ msg.phdr_encoded, msg.uhdr_encoded, msg.payload, @@ -100,7 +100,7 @@ def sign( ] encoded = dumps( - CBORTag(msg.cbor_tag, message), default=msg._custom_cbor_encoder + CBORTag(msg.cbor_tag, _message), default=msg._custom_cbor_encoder ) else: From 049c6bf3ca21a11e1fe9c67142b8b0079326507c Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Tue, 17 Oct 2023 20:43:27 -0400 Subject: [PATCH 4/8] Remove extraneous parameter for verify --- pycardano/cip/cip8.py | 8 +++----- test/pycardano/test_cip8.py | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pycardano/cip/cip8.py b/pycardano/cip/cip8.py index 68ff0305..951473fb 100644 --- a/pycardano/cip/cip8.py +++ b/pycardano/cip/cip8.py @@ -1,7 +1,6 @@ from typing import Optional, Union from cbor2 import CBORTag, dumps - from cose.algorithms import EdDSA from cose.headers import KID, Algorithm from cose.keys import CoseKey @@ -14,10 +13,10 @@ from pycardano.address import Address from pycardano.crypto import BIP32ED25519PublicKey from pycardano.key import ( - PaymentVerificationKey, - SigningKey, ExtendedSigningKey, ExtendedVerificationKey, + PaymentVerificationKey, + SigningKey, StakeExtendedSigningKey, StakeSigningKey, StakeVerificationKey, @@ -129,7 +128,6 @@ def sign( def verify( signed_message: Union[str, dict], attach_cose_key: Optional[bool] = None, - extended=False, ) -> dict: """Verify the signature of a COSESign1 message and decode its contents following CIP-0008. Supports messages signed by browser wallets or `Message.sign()`. @@ -196,7 +194,7 @@ def verify( # attach the key to the decoded message decoded_message.key = cose_key - if extended: + if len(verification_key) > 32: vk = BIP32ED25519PublicKey( public_key=verification_key[:32], chain_code=verification_key[32:] ) diff --git a/test/pycardano/test_cip8.py b/test/pycardano/test_cip8.py index 631715bb..7ffdedc4 100644 --- a/test/pycardano/test_cip8.py +++ b/test/pycardano/test_cip8.py @@ -10,7 +10,6 @@ ) from pycardano.network import Network - EXTENDED_SK = ExtendedSigningKey.from_json( """{ "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32", @@ -170,7 +169,7 @@ def test_extended_sign_and_verify(): network=Network.TESTNET, ) - verification = verify(signed_message, extended=True) + verification = verify(signed_message) assert verification["verified"] assert verification["message"] == "Pycardano is cool." assert verification["signing_address"].payment_part == EXTENDED_VK.hash() From 1bf3f8192f356cecfc518c244fd91d8bac1271c6 Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Thu, 2 Nov 2023 21:14:17 -0400 Subject: [PATCH 5/8] Added ByteString to _restored_typed_primitive --- pycardano/serialization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index 7b302e04..a01f9e34 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -532,6 +532,8 @@ def _restore_typed_primitive( if not isinstance(v, list): raise DeserializeException(f"Expected type list but got {type(v)}") return IndefiniteList([_restore_typed_primitive(t, w) for w in v]) + elif isclass(t) and t == ByteString: + return ByteString(v) elif isclass(t) and issubclass(t, IndefiniteList): try: return IndefiniteList(v) From 67add7ad4ec7dde4f0285e43fa4487601c9863fa Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Fri, 3 Nov 2023 16:30:59 -0400 Subject: [PATCH 6/8] Added type checking --- pycardano/serialization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index a01f9e34..0063d944 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -533,6 +533,8 @@ def _restore_typed_primitive( raise DeserializeException(f"Expected type list but got {type(v)}") return IndefiniteList([_restore_typed_primitive(t, w) for w in v]) elif isclass(t) and t == ByteString: + if not isinstance(v, bytes): + raise DeserializeException(f"Expected type bytes but got {type(v)}") return ByteString(v) elif isclass(t) and issubclass(t, IndefiniteList): try: From 733d12062b377529a987280608089bfe2dc752d4 Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Tue, 9 Jan 2024 22:16:33 -0500 Subject: [PATCH 7/8] Fixed bug where script inputs for transactions could not be deserialized --- pycardano/serialization.py | 5 +++++ test/pycardano/test_plutus.py | 8 ++++---- test/pycardano/test_serialization.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index 0063d944..e268cac5 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -536,6 +536,11 @@ def _restore_typed_primitive( if not isinstance(v, bytes): raise DeserializeException(f"Expected type bytes but got {type(v)}") return ByteString(v) + elif isclass(t) and t.__name__ in ["PlutusV1Script", "PlutusV2Script"]: + if not isinstance(v, bytes): + raise DeserializeException(f"Expected type bytes but got {type(v)}") + return t(v) + elif isclass(t) and issubclass(t, IndefiniteList): try: return IndefiniteList(v) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index f16df8fe..78d43cef 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -12,17 +12,17 @@ from pycardano.exception import DeserializeException from pycardano.plutus import ( COST_MODELS, + Datum, ExecutionUnits, PlutusData, RawPlutusData, Redeemer, RedeemerTag, - plutus_script_hash, - id_map, - Datum, Unit, + id_map, + plutus_script_hash, ) -from pycardano.serialization import IndefiniteList, RawCBOR, ByteString +from pycardano.serialization import ByteString, IndefiniteList, RawCBOR @dataclass diff --git a/test/pycardano/test_serialization.py b/test/pycardano/test_serialization.py index 8da4fa6b..fa8f9244 100644 --- a/test/pycardano/test_serialization.py +++ b/test/pycardano/test_serialization.py @@ -7,6 +7,7 @@ from pycardano import Datum, RawPlutusData from pycardano.exception import DeserializeException +from pycardano.plutus import PlutusV1Script, PlutusV2Script from pycardano.serialization import ( ArrayCBORSerializable, CBORSerializable, @@ -286,3 +287,17 @@ class Test2(MapCBORSerializable): with pytest.raises(TypeError): Test2(a=Test1(a=1)).to_cbor_hex() + + +def test_script_deserialize(): + @dataclass + class Test(MapCBORSerializable): + script_1: PlutusV1Script + script_2: PlutusV2Script + + datum = Test( + script_1=PlutusV1Script(b"dummy test script"), + script_2=PlutusV2Script(b"dummy test script"), + ) + + datum.from_cbor(datum.to_cbor()) From 4d94b3b17f27aa1835d33a38cf4a118fbff2b17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 10 Jan 2024 10:18:23 +0100 Subject: [PATCH 8/8] Stronger assumption in test for script serialization --- test/pycardano/test_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pycardano/test_serialization.py b/test/pycardano/test_serialization.py index fa8f9244..d9f24370 100644 --- a/test/pycardano/test_serialization.py +++ b/test/pycardano/test_serialization.py @@ -300,4 +300,4 @@ class Test(MapCBORSerializable): script_2=PlutusV2Script(b"dummy test script"), ) - datum.from_cbor(datum.to_cbor()) + assert datum == datum.from_cbor(datum.to_cbor())