EOSCommunity.org Forums

How to sign "arbitrary" data using ESR and anchor-link

The Scatter SDK had a signArbitrary method dApp developers could use to sign a message using the logged in users private key, anchor-link does not have this functionality for several reasons:

  • It can be very insecure if implemented not correctly (in fact yours truly discovered and reported a now-patched vulnerability in Scatter that used the signArbitrary method in combination with another exploit that allowed an attacker to take over someones account without them knowing they even signed something).
  • It does not work with all hardware wallets, the ledger app for example will not sign anything that is not formatted as an EOSIO transaction.
  • There is no standard so re-producing the message the wallet actually signed requires you to handle all different wallet implementations of it.

That said it was a useful feature that many dApp developers took advantage of to implement off-chain logic like logging in to backend applications and proving authenticity of chat messages.

Fortunately EOSIO already has “arbitrary” data specified, the action data. We can use that to sign any off-chain message in a safe and standardized way. It boils down to setting up an ABI on chain describing the structure of the data you want to sign and calling the transact method with broadcast set to false.

This is also how the EEP-7 identity requests works, except the data ABI is defined in the specification instead of uploaded to the chain (If you only need to authenticate users on a backend you should look into using that instead of a custom action).

In code:

import Link, {Struct, Action, PlaceholderAuth} from 'anchor-link'
import Transport from 'anchor-link-browser-transport'

const link = new Link({
    transport: new Transport(),
    chains: [
        {
            nodeUrl: 'https://jungle3.greymass.com',
            chainId: '2a02a0053e5a8cf73a56ba0fda11e4d92e0238a4a2aa74fccf46d5a910746840',
        },
    ],
})

@Struct.type('sign_data')
class SignData extends Struct {
    @Struct.field('string') declare message: string
}

const action = Action.from({
    account: 'example.gm',
    name: 'sign',
    authorization: [PlaceholderAuth],
    data: SignData.from({message: 'hello world'}),
})

const result = await link.transact({action}, {broadcast: false})

console.log('message signed', result.transaction.signingDigest(result.chain.chainId))
console.log('signing auth', result.signer)
console.log('signature', result.signatures[0])

The ABI deployed to example.gm (does not need to have an actual contract deployed).

{
  "version": "eosio::abi/1.1",
  "structs": [{
      "name": "sign",
      "base": "",
      "fields": [{
          "name": "message",
          "type": "string"
       }]
    }
  ],
  "actions": [{
      "name": "sign",
      "type": "sign",
      "ricardian_contract": ""
    }
  ]
}