5

I am playing around with the nami cardano wallet. I am able to link the wallet in the frontend and now want to link it to the backend.

First action the frontend sends an auth request with the cardano address to the backend. The backend returns a nonce to the frontend. The front end then lets nami wallet sign this nonce and sends the signature back to the backend.

In this step, the backend should validate if the signature is valid.

Now, how can I let the backend validate this signature?

This is the information I have access to:

  • balance
  • change address
  • network id
  • reward address
  • unused addresses
  • used addresses

Can I use this info to validate the signature? Or do I need to access the blockchain from the backend code?

(I will most likely be adding blockfrost api later, so nami-wallet + blockfrost will be how I access the blockchain. Unless there are other libraries available for the backend (most likely node.js), nami+blockfrost will be how I access the blockchain.

I am also using @emurgo/cardano-serialization-lib-browser - maybe here something is possible?

Based on response below I made a quick node script:

const {COSESign1, Label} = require("./extra_modules/@emurgo/cardano-message-signing-nodejs");
const {Address} = require("@emurgo/cardano-serialization-lib-nodejs");
const {Buffer} = require('buffer');
let signedRaw = "845869a30127045820674d11e432450118d70ea78673d5e31d5cc1aec63de0ff6284784876544be3406761646472657373583901d2eb831c6cad4aba700eb35f86966fbeff19d077954430e32ce65e8da79a3abe84f4ce817fad066acc1435be2ffc6bd7dce2ec1cc6cca6cba166686173686564f44568656c6c6f5840a3b5acd99df5f3b5e4449c5a116078e9c0fcfc126a4d4e2f6a9565f40b0c77474cafd89845e768fae3f6eec0df4575fcfe7094672c8c02169d744b415c617609";
let message = COSESign1.from_bytes(Buffer.from(signedRaw, 'hex'));
let headers = message.headers().protected().deserialized_headers();
let address = Address.from_bytes(headers.header(Label.new_text('address')).as_bytes());
let publicKey = headers.key_id();
let signature = message.signature();

OK, finally got a working solution, sharing a node script that works for the next person with the same problem:

const {COSESign1, Label} = require("./extra_modules/@emurgo/cardano-message-signing-nodejs");
const {Address, Ed25519Signature, PublicKey, BaseAddress, StakeCredential, RewardAddress} = require('@emurgo/cardano-serialization-lib-nodejs');
const {Buffer} = require('buffer');

class SignedData
{
    constructor(signed)
    {
        let message = COSESign1.from_bytes(Buffer.from(Buffer.from(signed, 'hex'), 'hex'));
        let headermap = message.headers().protected().deserialized_headers();
        this.headers = {
            algorithmId: headermap.algorithm_id().as_int().as_i32(),
            address: Address.from_bytes(headermap.header(Label.new_text('address')).as_bytes()),
            publicKey: PublicKey.from_bytes(headermap.key_id())
        };
        this.payload = message.payload();
        this.signature = Ed25519Signature.from_bytes(message.signature());
        this.data = message.signed_data().to_bytes();
    }

    verify(address, payload)
    {
        if (!this.verifyPayload(payload)) {
            throw new Error('Payload does not match');
        }
        if (!this.verifyAddress(address)) {
            throw new Error('Could not verify because of address mismatch');
        }
        return this.headers.publicKey.verify(this.data, this.signature);
    };

    verifyPayload(payload)
    {
        return Buffer.from(this.payload, 'hex').compare(Buffer.from(payload, 'hex'));
    }

    verifyAddress(address)
    {
        const checkAddress = Address.from_bech32(address);
        if (this.headers.address.to_bech32() !== checkAddress.to_bech32()) {
            console.log('FASLE1');
            return false;
        }
        // check if BaseAddress
        try {
            const baseAddress = BaseAddress.from_address(this.headers.address);
            //reconstruct address
            const paymentKeyHash = this.headers.publicKey.hash();
            const stakeKeyHash = baseAddress.stake_cred().to_keyhash();
            const reconstructedAddress = BaseAddress.new(
                checkAddress.network_id(),
                StakeCredential.from_keyhash(paymentKeyHash),
                StakeCredential.from_keyhash(stakeKeyHash)
            );
            return checkAddress.to_bech32() === reconstructedAddress.to_address().to_bech32();
        } catch (e) {
        }

        try {
            const stakeKeyHash = this.headers.address.hash();
            const reconstructedAddress = RewardAddress.new(
                checkAddress.network_id(),
                StakeCredential.from_keyhash(stakeKeyHash)
            );
            return checkAddress.to_bech32() === reconstructedAddress.to_address().to_bech32();
        } catch (e) {
        }
        return false;
    };
}
let signed = "845869a30127045820674d11e432450118d70ea78673d5e31d5cc1aec63de0ff6284784876544be3406761646472657373583901d2eb831c6cad4aba700eb35f86966fbeff19d077954430e32ce65e8da79a3abe84f4ce817fad066acc1435be2ffc6bd7dce2ec1cc6cca6cba166686173686564f44568656c6c6f5840a3b5acd99df5f3b5e4449c5a116078e9c0fcfc126a4d4e2f6a9565f40b0c77474cafd89845e768fae3f6eec0df4575fcfe7094672c8c02169d744b415c617609";
let signedData = new SignedData(signed);
let address = 'addr1q8fwhqcudjk54wnsp6e4lp5kd7l07xwsw725gv8r9nn9ard8ngatap85e6qhltgxdtxpgdd79l7xh47uutkpe3kv5m9smpyc34';
let payload = 'hello';
console.log(signedData.verify(address, payload));

6 Answers 6

6

Script with the solution (node JS):

const {COSESign1, Label} = require("./extra_modules/@emurgo/cardano-message-signing-nodejs");
const {Address, Ed25519Signature, PublicKey, BaseAddress, StakeCredential, RewardAddress} = require('@emurgo/cardano-serialization-lib-nodejs');
const {Buffer} = require('buffer');

class SignedData
{
    constructor(signed)
    {
        let message = COSESign1.from_bytes(Buffer.from(Buffer.from(signed, 'hex'), 'hex'));
        let headermap = message.headers().protected().deserialized_headers();
        this.headers = {
            algorithmId: headermap.algorithm_id().as_int().as_i32(),
            address: Address.from_bytes(headermap.header(Label.new_text('address')).as_bytes()),
            publicKey: PublicKey.from_bytes(headermap.key_id())
        };
        this.payload = message.payload();
        this.signature = Ed25519Signature.from_bytes(message.signature());
        this.data = message.signed_data().to_bytes();
    }

    verify(address, payload)
    {
        if (!this.verifyPayload(payload)) {
            throw new Error('Payload does not match');
        }
        if (!this.verifyAddress(address)) {
            throw new Error('Could not verify because of address mismatch');
        }
        return this.headers.publicKey.verify(this.data, this.signature);
    };

    verifyPayload(payload)
    {
        return Buffer.from(this.payload, 'hex').compare(Buffer.from(payload, 'hex'));
    }

    verifyAddress(address)
    {
        const checkAddress = Address.from_bech32(address);
        if (this.headers.address.to_bech32() !== checkAddress.to_bech32()) {
            console.log('FASLE1');
            return false;
        }
        // check if BaseAddress
        try {
            const baseAddress = BaseAddress.from_address(this.headers.address);
            //reconstruct address
            const paymentKeyHash = this.headers.publicKey.hash();
            const stakeKeyHash = baseAddress.stake_cred().to_keyhash();
            const reconstructedAddress = BaseAddress.new(
                checkAddress.network_id(),
                StakeCredential.from_keyhash(paymentKeyHash),
                StakeCredential.from_keyhash(stakeKeyHash)
            );
            return checkAddress.to_bech32() === reconstructedAddress.to_address().to_bech32();
        } catch (e) {
        }

        try {
            const stakeKeyHash = this.headers.address.hash();
            const reconstructedAddress = RewardAddress.new(
                checkAddress.network_id(),
                StakeCredential.from_keyhash(stakeKeyHash)
            );
            return checkAddress.to_bech32() === reconstructedAddress.to_address().to_bech32();
        } catch (e) {
        }
        return false;
    };
}
let signed = "845869a30127045820674d11e432450118d70ea78673d5e31d5cc1aec63de0ff6284784876544be3406761646472657373583901d2eb831c6cad4aba700eb35f86966fbeff19d077954430e32ce65e8da79a3abe84f4ce817fad066acc1435be2ffc6bd7dce2ec1cc6cca6cba166686173686564f44568656c6c6f5840a3b5acd99df5f3b5e4449c5a116078e9c0fcfc126a4d4e2f6a9565f40b0c77474cafd89845e768fae3f6eec0df4575fcfe7094672c8c02169d744b415c617609";
let signedData = new SignedData(signed);
let address = 'addr1q8fwhqcudjk54wnsp6e4lp5kd7l07xwsw725gv8r9nn9ard8ngatap85e6qhltgxdtxpgdd79l7xh47uutkpe3kv5m9smpyc34';
let payload = 'hello';
console.log(signedData.verify(address, payload));
2
  • 1
    I had some trouble implementing this - outlined the issues I found in a post linked below, incase anyone is able to provide some insight, please. cardano.stackexchange.com/questions/8708/…
    – zing
    Commented Jul 24, 2022 at 18:37
  • @zing your link is dead?
    – Displee
    Commented May 6, 2023 at 17:39
5

You will need to send your verification key from Nami to the backend during the handshake in order to validate the signature.

cardano.signData(address: BaseAddress|RewardAddress, payload: string) : CoseSign1

The returned CoseSign1 object contains the payload, signature and the following protected headers:

key_id => PublicKey, address => BaseAddress | RewardAddress algorithm_id => EdDSA(0) (the algorithm used for Cardano addresses).

Source: Nami Readme

When you run the cardano.SignData method, you are returned an object that includes the public key for the private key that you used to sign the payload. Thus, when you send the signed object to the backend, the backend has everything it needs in the header to validate that transaction. To avoid hacking, you should also ensure that the public key is constant throughout communication with the same entity or in the same session.

The output is a hex-encoded byte string adhering to the CBOR format standard. Here is a link to a playground that will let you convert between the CBOR that is the output (e.g., "CoseSign1" from the readme) and something readable. Here is an example output (don't forget to check all of the boxes to get the full result).

[<< {1: -8, 4: h'ABF29B244C2E1C4DD3CA2F8DB27D87C6B73DA1A87D08389101D5D1ECEBDD0079', "address": h'E1DC0C1BD97FCD0ED6905EBA692C1DDEFF27DA43D59C5A1F2EA766442E'} >>, {"hashed": false}, 'this is a test\n', h'1F19958A76C7414F311F5176A52A544A4BE9D59FC5F3870F41B00D8B8C52C9B819953D10AE3F23C0D906A138D37D6C70DDADE77DB94D7054D730618106F18108']

It is a JSON array with two parts: the first is the header, and the second is the contents. According to CIP-0008, the block is expected to contain the following.

COSE_Sign1 = [
    Headers,
    payload : bstr / nil,
    signature : bstr
]

My guess is that the pub key is mapped to 4: ABF29B244C2E1C4DD3CA2F8DB27D87C6B73DA1A87D08389101D5D1ECEBDD0079

Hope this helps!

4
  • I had noticed that indeed, and I have tried the past 2h to extract that info, but as far as I found this is a cbor encoded message and I just cannot extract it. i see the data i tried to sign in it though (Buffer.from_bytes(data, 'hex').to_string('ascii') :) : Xi#'X gMd2EW'sUc\A.F=`bxHvTKc@gaddressX9Rkl-J:p3_o>PwD0c,f^ ':>tN-jL5>/|kW\blFL&K!fhashedtKhello worldX@CW1f4\FcWB0IrK _\#wMYl{I;__# 56._TV68'Vu DWf0)
    – Jeroen
    Commented Aug 28, 2021 at 18:28
  • I completed the explanation =^). Please see edits. Basically, it's just in CBOR, and you will have to convert it. I worked an example. Let me know if you need anything else!
    – Noah Jones
    Commented Aug 28, 2021 at 20:16
  • I added a node script with a working solution using the emurgo serialization and message signing libraries. Thanks!
    – Jeroen
    Commented Aug 29, 2021 at 14:59
  • Nice well done!! Any time =^)
    – Noah Jones
    Commented Aug 29, 2021 at 18:00
2

I've created javascript package that could help you with this. The examples in the README should show you how to sign data on frontend and validate the signature on the backend.

2

WARNING

The provided solution in the question is a great start, however

 verifyPayload(payload)
{
    return Buffer.from(this.payload, 'hex').compare(Buffer.from(payload, 'hex'));
}

Does not do a correct comparison since in the example the payload to check is not hexencoded but rather a string, so use this instead

    verifyPayload(payload)
{
    let hexMessage=Buffer.from(this.payload, 'hex');
    let stringMessage=hexMessage.toString();
    return stringMessage===payload;
}
1
  • It is a bad idea to refer to other solutions. They may get edited and then your answer becomes weird. Better to comment on them directly when you have enough reputation. It is always better to focus on making your answer entirely about the question. Take the tour and look at How to Answer for great advice.
    – gRebel
    Commented Jan 18, 2022 at 22:53
0

I would like to suggest a modification in the verifyPayload function.

verifyPayload(payload) { return Buffer.from(this.payload, 'hex').equals(Buffer.from(payload, 'utf8')); }

0

Given that this question - and all of the answers so far - are from mid 2021, I don't think the solutions are up to date. CIP-0030 brought a change to signData, which forced me to update the approach I use to verify wallets with my backend. I actually still use the CIP-0008 approach for Nami because of a bug on their end, but it is technically deprecated. This post explains what I do now.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.