How to create a verifiable signature

I would like to create an ECDSA_P256 signature in offchain and verify it in Cadence.
So I have tried the following code using Node.js, but the result fails (false returns).

Can you please tell me what is wrong with this code and how I can sign it correctly in JavaScript (or Golang)? Am I missing something?

js:

const { SHA3 } = require('sha3');
const EC = require('elliptic').ec;
const ec = new EC('p256');

function signWithKey(privateKey, msg) {
  const key = ec.keyFromPrivate(Buffer.from(privateKey, 'hex'));
  console.log('publicKey:', key.getPublic('hex').replace(/^04/, ''))
  const sig = key.sign(hashMsg(msg));
  const n = 32;
  const r = sig.r.toArrayLike(Buffer, 'be', n);
  const s = sig.s.toArrayLike(Buffer, 'be', n);
  return Buffer.concat([r, s]).toString('hex');
};

function hashMsg(msg) {
  const sha = new SHA3(256);
  sha.update(Buffer.from(msg, 'hex'));
  return sha.digest();
};

const privateKey = '9a5080ac4d1323609357a29a755254f770c3e9dcb5e2becdbe8f13f4edd688f5'
const message = '666f6f'; // 'foo'
const signature = signWithKey(privateKey, message);
console.log('signature:', signature);

Cadence script:

import Crypto

pub fun main(): Bool {
    let keyList = Crypto.KeyList()
    let publicKeyA = Crypto.PublicKey(
        publicKey: "8653bdf116189ef4963a6250ba9f1d25daaa028e39569547976a545ca5ff72d31b6bd54955ce51ce579c583a8e10aef12d4b53037660b2fe2b12df0876789ece".decodeHex(),
        signatureAlgorithm: Crypto.ECDSA_P256
    )
    keyList.add(publicKeyA, hashAlgorithm: Crypto.SHA3_256, weight: 1.0)
    let signatureSet = [
        Crypto.KeyListSignature(
            keyIndex: 0,
            signature: "d82b75754be1593c0ab4344e62f83b43df7ef854c198181de547e3b4ba320ae2f97160445b6364a6557a1768064c3e45e1c17a9ccfb22183b2135eaf7c989b19".decodeHex()
        )
    ]
    // "foo", encoded as UTF-8, in hex representation
    let signedData = "666f6f".decodeHex()
    let isValid = keyList.isValid(signatureSet: signatureSet, signedData: signedData)
    return isValid
}

*Note that this key pair was generated by the flow keys generate command.

By looking at the Go SDK signature example code, I found that the message needed a 32-byte tag.
I created a JS SDK version of this and verified that signing and its verification in Cadence correctly.

ts :

import * as fs from 'fs';
import * as path from 'path';
import * as fcl from '@onflow/fcl';
import * as t from '@onflow/types';
import { ec as EC } from 'elliptic';
import { SHA3 } from 'sha3';

const ec: EC = new EC('p256'); // or 'secp256k1'

const toBytesWithTag = (str: string) => {
  // Tag: '464c4f572d56302e302d75736572000000000000000000000000000000000000'
  // ref: https://github.com/onflow/flow-go-sdk/blob/9bb50d/sign.go
  const tagBytes = Buffer.alloc(32);
  Buffer.from('FLOW-V0.0-user').copy(tagBytes);
  const strBytes = Buffer.from(str);
  return Buffer.concat([tagBytes, strBytes]);
}

const hashMsg = (msg: string) => {
  const sha = new SHA3(256);
  return sha.update(toBytesWithTag(msg)).digest();
};

const sign = (privKey: string, msg: string) => {
  const key = ec.keyFromPrivate(Buffer.from(privKey, 'hex'));
  const sig = key.sign(hashMsg(msg));
  const n = 32;
  const r = sig.r.toArrayLike(Buffer, 'be', n);
  const s = sig.s.toArrayLike(Buffer, 'be', n);
  return Buffer.concat([r, s]).toString('hex');
};

const toHexStr = (str: string): string => {
  return Buffer.from(str).toString('hex');
}

const verifySig = async (pubKey: string, msg: string, sig: string) => {
  fcl.config().put('accessNode.api', 'http://127.0.0.1:8080');
  const script = fs.readFileSync(path.join(__dirname, './scripts/verify_sig.cdc'), 'utf8');
  const response = await fcl.send([
    fcl.script`${script}`,
    fcl.args([
      fcl.arg([pubKey], t.Array(t.String)),
      fcl.arg(['1.0'], t.Array(t.UFix64)),
      fcl.arg([sig], t.Array(t.String)),
      fcl.arg(toHexStr(msg), t.String),
    ])
  ]);
  return await fcl.decode(response);
}

const main = async () => {
  const pubKey = '7be160dcfc5b4e9044b473fc479c4c5528d71fa2bbd9dc4f740b4a80c749830ed2ee0445f0fa707b4ea888313953b90de8b47fa388302c4268f5fdbc6329754f';
  const privKey = '9a3259d7c18fd98ccf51356a48df5b63d7d544153db49079c46d152ea9739539';
  const msg = 'test message';
  const sig = sign(privKey, msg);
  const isValid = await verifySig(pubKey, msg, sig);
  console.log({ pubKey, privKey, msg, sig, isValid });
};

main().catch(e => console.error(e));

Cadence script :

import Crypto

pub fun main(rawPublicKeys: [String], weights: [UFix64], signatures: [String], signedData: String): Bool {
  let keyList = Crypto.KeyList()
  var i = 0
  for rawPublicKey in rawPublicKeys {
    keyList.add(
      PublicKey(
        publicKey: rawPublicKey.decodeHex(),
        signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 // or SignatureAlgorithm.ECDSA_Secp256k1
      ),
      hashAlgorithm: HashAlgorithm.SHA3_256,
      weight: weights[i],
    )
    i = i + 1
  }

  let signatureSet: [Crypto.KeyListSignature] = []
  var j = 0
  for signature in signatures {
    signatureSet.append(
      Crypto.KeyListSignature(
        keyIndex: j,
        signature: signature.decodeHex()
      )
    )
    j = j + 1
  }

  return keyList.isValid(
    signatureSet: signatureSet,
    signedData: signedData.decodeHex(),
  )
}

I hope this information is useful to someone.

2 Likes

@avcd Thank you so much for figuring this out! Someone is working on the JVM SDK and we tried to figure out why the verification fails. This is very useful! We should document this better

1 Like

Still having some trouble with this on the JVM SDK side. Running this test:

β€œFalse” is all that is ever returned.

There is a destructive change in Cadence and I have modified the sample code.

return keyList.isValid(...)

↓

return keyList.verify(...)