From fdfd3e20b7ee7972d3e1ad02705d0d1e4c86e89f Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Thu, 5 Oct 2023 21:23:19 -0400 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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 f9c8754c7fe5dcb0d8c81f432d823021ce26c772 Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Tue, 9 Jan 2024 22:55:05 -0500 Subject: [PATCH 7/9] Added support for CIP 14 --- pycardano/cip/cip14.py | 33 +++++++++++++++++++++ test/pycardano/test_cip14.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 pycardano/cip/cip14.py create mode 100644 test/pycardano/test_cip14.py diff --git a/pycardano/cip/cip14.py b/pycardano/cip/cip14.py new file mode 100644 index 00000000..be76da0b --- /dev/null +++ b/pycardano/cip/cip14.py @@ -0,0 +1,33 @@ +from typing import Union + +from nacl.encoding import RawEncoder +from nacl.hash import blake2b +from pycardano.crypto.bech32 import encode + + +def encode_asset(policy_id: Union[bytes, str], asset_name: Union[bytes, str]) -> str: + """Implementation of CIP14 asset fingerprinting + + This function encodes the asset policy and name into an asset fingerprint, which is + bech32 compliant. + + For more information: + https://developers.cardano.org/docs/governance/cardano-improvement-proposals/cip-0014/ + + Args: + policy_id: The asset policy as `bytes` or a hex `str` + asset_name: The asset name as `bytes` or a hex `str` + """ + if isinstance(policy_id, str): + policy_id = bytes.fromhex(policy_id) + + if isinstance(asset_name, str): + asset_name = bytes.fromhex(asset_name) + + asset_hash = blake2b( + policy_id + asset_name, + digest_size=20, + encoder=RawEncoder, + ) + + return encode("asset", asset_hash) diff --git a/test/pycardano/test_cip14.py b/test/pycardano/test_cip14.py new file mode 100644 index 00000000..420197d1 --- /dev/null +++ b/test/pycardano/test_cip14.py @@ -0,0 +1,56 @@ +import pytest + +from pycardano.cip.cip14 import encode_asset + + +@pytest.mark.parametrize( + "asset", + [ + { + "policy_id": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", + "asset_name": "", + "asset_fingerprint": "asset1rjklcrnsdzqp65wjgrg55sy9723kw09mlgvlc3", + }, + { + "policy_id": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc37e", + "asset_name": "", + "asset_fingerprint": "asset1nl0puwxmhas8fawxp8nx4e2q3wekg969n2auw3", + }, + { + "policy_id": "1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", + "asset_name": "", + "asset_fingerprint": "asset1uyuxku60yqe57nusqzjx38aan3f2wq6s93f6ea", + }, + { + "policy_id": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", + "asset_name": "504154415445", + "asset_fingerprint": "asset13n25uv0yaf5kus35fm2k86cqy60z58d9xmde92", + }, + { + "policy_id": "1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", + "asset_name": "504154415445", + "asset_fingerprint": "asset1hv4p5tv2a837mzqrst04d0dcptdjmluqvdx9k3", + }, + { + "policy_id": "1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", + "asset_name": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", + "asset_fingerprint": "asset1aqrdypg669jgazruv5ah07nuyqe0wxjhe2el6f", + }, + { + "policy_id": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", + "asset_name": "1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209", + "asset_fingerprint": "asset17jd78wukhtrnmjh3fngzasxm8rck0l2r4hhyyt", + }, + { + "policy_id": "7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373", + "asset_name": "0000000000000000000000000000000000000000000000000000000000000000", + "asset_fingerprint": "asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w", + }, + ], +) +def test_encode_asset(asset): + fingerprint = encode_asset( + policy_id=asset["policy_id"], asset_name=asset["asset_name"] + ) + + assert fingerprint == asset["asset_fingerprint"] From 8f80fbc943a9d4add336a89cfe0121cf9a63e60d Mon Sep 17 00:00:00 2001 From: Elder Millenial Date: Wed, 10 Jan 2024 08:36:57 -0500 Subject: [PATCH 8/9] Added support for ScriptHash and AssetName --- pycardano/cip/cip14.py | 14 +++++++++++--- test/pycardano/test_cip14.py | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pycardano/cip/cip14.py b/pycardano/cip/cip14.py index be76da0b..6126f23a 100644 --- a/pycardano/cip/cip14.py +++ b/pycardano/cip/cip14.py @@ -3,9 +3,13 @@ from nacl.encoding import RawEncoder from nacl.hash import blake2b from pycardano.crypto.bech32 import encode +from pycardano.hash import ScriptHash +from pycardano.transaction import AssetName -def encode_asset(policy_id: Union[bytes, str], asset_name: Union[bytes, str]) -> str: +def encode_asset( + policy_id: Union[ScriptHash, bytes, str], asset_name: Union[AssetName, bytes, str] +) -> str: """Implementation of CIP14 asset fingerprinting This function encodes the asset policy and name into an asset fingerprint, which is @@ -15,14 +19,18 @@ def encode_asset(policy_id: Union[bytes, str], asset_name: Union[bytes, str]) -> https://developers.cardano.org/docs/governance/cardano-improvement-proposals/cip-0014/ Args: - policy_id: The asset policy as `bytes` or a hex `str` - asset_name: The asset name as `bytes` or a hex `str` + policy_id: The asset policy as `ScriptHash`, `bytes`, or a hex `str` + asset_name: The asset name as `AssetName`, `bytes`, or a hex `str` """ if isinstance(policy_id, str): policy_id = bytes.fromhex(policy_id) + elif isinstance(policy_id, ScriptHash): + policy_id = policy_id.payload if isinstance(asset_name, str): asset_name = bytes.fromhex(asset_name) + elif isinstance(asset_name, AssetName): + asset_name = asset_name.payload asset_hash = blake2b( policy_id + asset_name, diff --git a/test/pycardano/test_cip14.py b/test/pycardano/test_cip14.py index 420197d1..09406550 100644 --- a/test/pycardano/test_cip14.py +++ b/test/pycardano/test_cip14.py @@ -1,8 +1,13 @@ import pytest from pycardano.cip.cip14 import encode_asset +from pycardano.hash import ScriptHash +from pycardano.transaction import AssetName +@pytest.mark.parametrize( + "input_types", [(str, str), (bytes, bytes), (ScriptHash, AssetName)] +) @pytest.mark.parametrize( "asset", [ @@ -48,7 +53,18 @@ }, ], ) -def test_encode_asset(asset): +def test_encode_asset(asset, input_types): + if isinstance(input_types[0], bytes): + policy_id = bytes.fromhex(asset["policy_id"]) + asset_name = bytes.fromhex(asset["asset_name"]) + elif isinstance(input_types[0], str): + policy_id = asset["policy_id"] + asset_name = asset["asset_name"] + + if isinstance(input_types[0], ScriptHash): + policy_id = ScriptHash(policy_id) + asset_name = AssetName(asset_name) + fingerprint = encode_asset( policy_id=asset["policy_id"], asset_name=asset["asset_name"] ) From b9e61207527351b02a545b4542970b2f710a4666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 10 Jan 2024 14:40:37 +0100 Subject: [PATCH 9/9] Add import to cips and docs --- docs/source/api/pycardano.cip.rst | 5 +++++ pycardano/cip/__init__.py | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/source/api/pycardano.cip.rst b/docs/source/api/pycardano.cip.rst index 1e2f7bd7..e978b929 100644 --- a/docs/source/api/pycardano.cip.rst +++ b/docs/source/api/pycardano.cip.rst @@ -7,3 +7,8 @@ Implementation of Cardano Improvement Proposals (CIPs) :members: :undoc-members: :show-inheritance: + +.. automodule:: pycardano.cip.cip14 + :members: + :undoc-members: + :show-inheritance: diff --git a/pycardano/cip/__init__.py b/pycardano/cip/__init__.py index a9087731..a3d1a71c 100644 --- a/pycardano/cip/__init__.py +++ b/pycardano/cip/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa from .cip8 import * +from .cip14 import *