Last active
May 25, 2022 23:20
-
-
Save astrojarred/0ab3247c11d59e38fe56725f56d019b1 to your computer and use it in GitHub Desktop.
Signing and Verifying messages with Pycardano and COSE
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Signing and Verifying messages with COSE | |
from binascii import hexlify, unhexlify | |
from typing import Tuple, Union | |
import pycardano | |
from cose.algorithms import EdDSA | |
from cose.headers import KID, Algorithm | |
from cose.keys import CoseKey | |
from cose.keys.curves import Ed25519 | |
from cose.keys.keyops import SignOp, VerifyOp | |
from cose.keys.keyparam import KpKeyOps, KpKty, OKPKpCurve, OKPKpD, OKPKpX | |
from cose.keys.keytype import KtyOKP | |
from cose.messages import CoseMessage, Sign1Message | |
# some examping outputs from Nami and CCvault to play around with | |
# both contain the same message and are signed with the same address | |
# nami provides a dict object | |
# the key is the entire COSE key needed to decode the signature, including the wallet verification key | |
nami_signature = { | |
"key": "a401010327200621582079464729926aeba53960944f155aed9a8526a74e0ca688f9988d9e00a4568be2", | |
"signature": "845846a20127676164647265737358390152ac4d56d3fa81fd1db7c0a793d27751249cc5d8a1bcf4e3c71bbcd522b1748fb1534a6b8eba3a3a21068e63c2a01f9051d8b2ca511aa5dba166686173686564f44f707963617264616e6f20726f636b7358409728af5a71092c3027386b275879abc1396845beab1386c73108e506847d00b1a0bb0135e02028beb60b133b758c97c32e12588bbf35b07dbfb93d5eb4593f0a", | |
} | |
# CCvault provides just the signature, the key is implied to be a standard template | |
# the `sign_message()` function below provides output in the same format | |
# in this case, the wallet verification key is attached to the headers of the signature | |
ccvault_signature = "845869a3012704582079464729926aeba53960944f155aed9a8526a74e0ca688f9988d9e00a4568be2676164647265737358390152ac4d56d3fa81fd1db7c0a793d27751249cc5d8a1bcf4e3c71bbcd522b1748fb1534a6b8eba3a3a21068e63c2a01f9051d8b2ca511aa5dba166686173686564f44f707963617264616e6f20726f636b73584053144591651830a0f724c6934323e667bd444415a7bc28f6567bc961c8c752df577ef09fe6dd628e0d366ee7b86dba8d40194e004d45c5bbcd123acdb5c19a0b" | |
def sign_message( | |
message: str, | |
payment_signing_key: pycardano.key.PaymentSigningKey, | |
payment_verification_key: pycardano.key.PaymentVerificationKey, | |
) -> str: | |
""" | |
Sign a message with your payment verificaiton key. | |
Parameters: | |
message (str): Message to be signed | |
payment_signing_key (pycardano.key.PaymentSigningKey): Key which is used to sign the message | |
payment_verification_key (pycardano.key.PaymentVerificationKey): Key which will be attached so others can verify the signature | |
Returns: | |
signed_message (str): a hex-encoded string containing the signed message and verification key | |
""" | |
# create the message object, attach verification key to the header | |
msg = Sign1Message( | |
phdr={Algorithm: EdDSA, KID: payment_verification_key.payload}, | |
payload=message.encode("utf-8"), | |
) | |
# build the CoseSign1 signing key from a dictionary | |
cose_key = { | |
KpKty: KtyOKP, | |
OKPKpCurve: Ed25519, | |
KpKeyOps: [SignOp, VerifyOp], | |
OKPKpD: payment_signing_key.payload, # private key | |
OKPKpX: payment_verification_key.payload, # public key | |
} | |
cose_key = CoseKey.from_dict(cose_key) | |
msg.key = cose_key # attach the key to the message | |
encoded = msg.encode() | |
# turn the enocded message into a hex string and remove the first byte | |
# which is always "d2" | |
signed_message = hexlify(encoded).decode("utf-8")[2:] | |
return signed_message | |
def verify_signed_message( | |
signed_payload: Union[str, dict] | |
) -> Tuple[bool, bool, str, pycardano.Address]: | |
""" | |
Verify the signature of a COSESign1 message and decode. | |
Supports messages signed by Nami, CCVault, or `sign_message()`. | |
Parameters: | |
signed_payload (str): A hex-encoded string | |
Returns : | |
verified (bool): If the signature is verified | |
addresses_match (bool): Whether the address provided belongs to the verification key used to sign the message | |
message (str): The contents of the signed message | |
address (pycardano.Address): The address to which the signing keys belong | |
Note: Both `verified` and `address_match` should be True. | |
""" | |
# nami prvoides us with a dictionary, and ccvault with a string | |
if isinstance(signed_payload, dict): | |
# this is nami, the cose key is attached as a dict object which contains the verification key | |
# the headers of the signature are emtpy | |
key = signed_payload.get("key") | |
signed_message = signed_payload.get("signature") | |
else: | |
# CCVault and `sign_message()` | |
key = "" # key will be extracted later from the payload headers | |
signed_message = signed_payload | |
# Add back the "D2" header byte and decode | |
decoded_message = CoseMessage.decode(unhexlify("d2" + signed_message)) | |
# in `sign_message()` and CCVault the verification key is | |
# attached to the header, and the key is generated by us from a template | |
# generate/extract the cose key | |
if not key: | |
# i.e. not Nami | |
# get the verification key from the headers | |
verification_key = decoded_message.phdr[KID] | |
# generate the COSE key | |
cose_key = { | |
KpKty: KtyOKP, | |
OKPKpCurve: Ed25519, | |
KpKeyOps: [SignOp, VerifyOp], | |
OKPKpX: verification_key, # public key | |
} | |
cose_key = CoseKey.from_dict(cose_key) | |
else: | |
# nami, key is attached | |
cose_key = CoseKey.decode(unhexlify(key)) | |
verification_key = cose_key[OKPKpX] | |
# attach the key to the decoded message | |
decoded_message.key = cose_key | |
verified = decoded_message.verify_signature() | |
data = decoded_message.payload.decode("utf-8") | |
address = pycardano.Address.from_primitive(decoded_message.phdr["address"]) | |
# check that the address atatched in the headers matches the one of the verification key used to sign the message | |
addresses_match = ( | |
address.payment_part | |
== pycardano.PaymentVerificationKey.from_primitive(verification_key).hash() | |
) | |
# normally you want to check both that the message is verified and that the addresses match | |
return verified, addresses_match, data, address |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment