Magickbase / neuron-public-issues

Neuron Issues
6 stars 3 forks source link

Add docs of signing a message and code snippet of recovering pk from the message #78

Closed Keith-CY closed 1 year ago

Keith-CY commented 1 year ago

Metaforo is going to support DAO of Nervos and it requires users in the community to provide a message signed by Neuron to prove identities.

The public key recovered from the signed message will be the authenticated token as a member so the code snippet to recover a pk from a message is required.

Besides, a doc of how the message is signed is helpful

Keith-CY commented 1 year ago

Sign message

  private static signByPrivateKey(privateKey: string, message: string): string {
    const digest = SignMessage.signatureHash(message)
    const ecPair = new ECPair(privateKey)
    const signature = ecPair.signRecoverable(digest)
    return signature
  }

  private static signatureHash(message: string) {
    const buffer = Buffer.from(SignMessage.magicString + message, 'utf-8')
    const blake2b = new Blake2b()
    blake2b.updateBuffer(buffer)
    return blake2b.digest()
  }

  private static magicString = 'Nervos Message:'

Recover public key

const options = {
  r: signature.slice(2, 66),
  s: signature.slice(66, 130),
  recoveryParam: parseInt(signature.slice(-1))
}
const msgBuffer = Buffer.from(digest.slice(2), 'hex')
const publicKey ='0x' + SignMessage.ec.recoverPubKey(msgBuffer, options, options.recoveryParam).encode('hex', true)

Ref:

Generate address from public key

/**
 * @description payload to a full address of new version
 */
const payloadToAddress = (payload: Uint8Array, isMainnet = true) =>
  bech32m.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32m.toWords(payload), MAX_BECH32_LIMIT)

const scriptToPayload = ({ codeHash, hashType, args }: CKBComponents.Script): Uint8Array => {
  if (!args.startsWith('0x')) {
    throw new HexStringWithout0xException(args)
  }

  if (!codeHash.startsWith('0x') || codeHash.length !== 66) {
    throw new CodeHashException(codeHash)
  }

  enum HashType {
    data = '00',
    type = '01',
    data1 = '02',
  }

  if (!HashType[hashType]) {
    throw new HashTypeException(hashType)
  }

  return hexToBytes(`0x00${codeHash.slice(2)}${HashType[hashType]}${args.slice(2)}`)
}

/**
 * @deprecated please migrate to {@link https://lumos-website.vercel.app/api/modules/helpers.html#encodetoaddress @ckb-lumos/helpers/encodeToAddress} {@link https://lumos-website.vercel.app/migrations/migrate-form-ckb-sdk-utils#scripttoaddress example}
 * @function scriptToAddress
 * @description The only way recommended to generated a full address of new version
 * @param {object} script
 * @param {booealn} isMainnet
 * @returns {string} address
 */
export const scriptToAddress = (script: CKBComponents.Script, isMainnet = true) =>
  payloadToAddress(scriptToPayload(script), isMainnet)

Ref:

Test cases

https://github.com/nervosnetwork/neuron/blob/89666d10454698f693aadad69078eda7ed17d7dc/packages/neuron-wallet/tests/services/sign-message.test.ts#L36-L116

Code snippets of signing message, recovering public key and generating address

import { ec as EC } from 'elliptic'
import { blake2b, PERSONAL, scriptToAddress, systemScripts } from '@nervosnetwork/ckb-sdk-utils'
import blake160 from '@nervosnetwork/ckb-sdk-utils/lib/crypto/blake160'

const ec = new EC('secp256k1')
const BLAKE_2B_SIZE = 32
const MAGIC_STRING = `Nervos Message:`

/**
 * the progress of signing a message
 */
const signMessage = (message: string, sk: string) => {
  const msgToSign = Buffer.from(MAGIC_STRING + message, 'utf8')
  const digest = blake2b(BLAKE_2B_SIZE, null, null, PERSONAL).update(msgToSign).digest('binary')
  const ecPair = ec.keyFromPrivate(sk.replace(/^0x/, ''))
  const { r, s, recoveryParam } = ecPair.sign(digest, { canonical: true })
  if (recoveryParam === null) throw new Error()
  const fmtR = r.toString(16).padStart(64, '0')
  const fmtS = s.toString(16).padStart(64, '0')

  return `0x${fmtR}${fmtS}0${recoveryParam}`
}

/**
 * the progress of recovering the public key
 */
const recoverPk = (message: string, signature: string) => {
  const r = signature.slice(2, 66)
  const s = signature.slice(66, 130)
  const recoveryParam = parseInt(signature.slice(-1))
  const msg = Buffer.from(MAGIC_STRING + message, 'utf8')
  const digest = blake2b(BLAKE_2B_SIZE, null, null, PERSONAL).update(msg).digest('binary')

  return `0x` + ec.recoverPubKey(digest, { r, s }, recoveryParam).encode('hex', true)
}

/**
 * test cases from https://github.com/nervosnetwork/neuron/blob/89666d10454698f693aadad69078eda7ed17d7dc/packages/neuron-wallet/tests/services/sign-message.test.ts#L36-L116
 */

const fixture = {
  privateKey: '0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3',
  message: 'HelloWorld',
  digest: '0xdfb48ccf7126479c052f68cb4202cd094632d30198a322e3c3638679bc73858d',
  address: 'ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd',
  signture:
    '0x97ed8c48879eed50743532bf7cc53e641c501509d2be19d06e6496dd944a21b4509136f18c8e139cc4002822b2deb5cbaff8e44b8782769af3113ff7fb8bd92700',
}

/**
 * the address in fixture is in a deprecated format so it should be transformed to the effective one
 * the effective one could be found on Explorer: https://explorer.nervos.org/address/ckb1qyqrdsefa43s6m882pcj53m4gdnj4k440axqdt9rtd and click the "view in new address format" button next to the address
 * or it can be computed by `scriptToAddress(addressToScript(fixture.address))`
 */
const fixtureAddress = `ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqfkcv576ccddnn4quf2ga65xee2m26h7nqmzxl9m`

/**
 * Test signed message
 */
const signedMessage = signMessage(fixture.message, fixture.privateKey)
console.log(`Expect to have the same signed message: ${signedMessage === fixture.signture}`)

/**
 * Test recovered public key
 */
const recoveredPk = recoverPk(fixture.message, fixture.signture)
console.log(
  `Expect to have the same pk: ${
    ec.keyFromPrivate(fixture.privateKey.slice(2)).getPublic(true, 'hex') === recoveredPk.slice(2)
  }`
)

/**
 * Test recovered address
 */
const secp256k1ScriptArgs = '0x' + Buffer.from(blake160(recoveredPk)).toString('hex')
const recoveredAddress = scriptToAddress({ ...systemScripts.SECP256K1_BLAKE160, args: secp256k1ScriptArgs })

console.log(`Expect to have the same address: ${fixtureAddress === recoveredAddress}`)