Skip to content

Instantly share code, notes, and snippets.

@astrojarred
Last active May 25, 2022 23:20
Show Gist options
  • Save astrojarred/0ab3247c11d59e38fe56725f56d019b1 to your computer and use it in GitHub Desktop.
Save astrojarred/0ab3247c11d59e38fe56725f56d019b1 to your computer and use it in GitHub Desktop.
Signing and Verifying messages with Pycardano and COSE
# 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