diff --git a/pycardano/cip/cip8.py b/pycardano/cip/cip8.py index 67536d35..951473fb 100644 --- a/pycardano/cip/cip8.py +++ b/pycardano/cip/cip8.py @@ -1,5 +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 @@ -10,13 +11,15 @@ from cose.messages import CoseMessage, Sign1Message from pycardano.address import Address +from pycardano.crypto import BIP32ED25519PublicKey from pycardano.key import ( + ExtendedSigningKey, + ExtendedVerificationKey, PaymentVerificationKey, SigningKey, StakeExtendedSigningKey, StakeSigningKey, StakeVerificationKey, - VerificationKey, ) from pycardano.network import Network @@ -25,7 +28,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 +48,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 +90,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 +126,8 @@ 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, ) -> 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 +194,16 @@ def verify( # attach the key to the decoded message decoded_message.key = cose_key - signature_verified = decoded_message.verify_signature() + if len(verification_key) > 32: + 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..7ffdedc4 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,23 @@ ) 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 +158,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) + 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."