3

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));
3

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
    Aug 28 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
    Aug 28 at 20:16
  • I added a node script with a working solution using the emurgo serialization and message signing libraries. Thanks!
    – Jeroen
    Aug 29 at 14:59
  • Nice well done!! Any time =^)
    – Noah Jones
    Aug 29 at 18:00
1

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));
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')); }

New contributor
Manik Jain is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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