flujo de trabajo al firmar una cadena con clave privada, seguido de verificación de firma con clave pública

Sé que esta pregunta ha sido formulada y respondida, pero al intentar que todo funcione, me he encontrado con algunos problemas. Esto es lo que estoy tratando de hacer:

1. take a target string 'Schoolbus'
2. use JSON with geth to eth_sign it
3. obtain v,r,s of signature
4. attempt to verify with a solidity contract, need the hash of 'Schoolbus'

Así que esto es lo que tengo. En primer lugar, no todos podemos usar la misma clave privada, así que si alguien puede verificar mi trabajo y obtener una idea general de mi problema, sería genial.

Fingiendo que mi clave privada es '0xd1ade25ccd3d550a7eb532ac759cac7be09c2719', para firmar 'Schoolbus', uso

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_sign","params":["0xd1ade25ccd3d550a7eb532ac759cac7be09c2719", "Schoolbus"],"id":1}'

donde saco el resultado

    0x2ac19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601

Estoy usando (podría estar equivocado aquí)

v=2a
r=c19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407
s=466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601

Luego preparé mi contrato, que es una variación de una respuesta en un hilo relacionado:

contract Auth {      
    function verify( bytes32 hash, uint8 v, bytes32 r, bytes32 s) constant returns(address retAddr) {
        retAddr= ecrecover(hash, v, r, s);
    }
}

Como nunca obtuve el hash de 'Schoolbus', probé un par de cosas en web3.js (el UTF8 me confundió, esperaba que la versión {encoding:'hex'} fuera la correcta):

console.log('1 '+ web3.sha3(web3.toHex('Schoolbus'))); //05ab39621b81764697fcfb6ae4fcf6b023cd644721c67c13a49fbd769c75671c
console.log('2 '+ web3.sha3(web3.toHex('Schoolbus'),{encoding:'hex'}));//d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('3 '+ web3.sha3('Schoolbus'));//d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('4 '+  web3.sha3(  unescape(encodeURIComponent('Schoolbus'))  ) ); //to UTF8 //d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('5 '+  web3.sha3(  unescape(encodeURIComponent('Schoolbus')), {encoding:'hex'} ) ); //to UTF8 //8f1cbe7efcf383ffeb1aeaf1e826c778a087153344cbeba144fbe967ad3ab11a

Terminé usando esto, pero no sé por qué:

0xd030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c

Entonces llamé al contrato:

var contDep=web3.eth.contract( [abi def] ).at( contractAddress);

console.log(
    contDep.verify('d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c', 2a,'c19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407', '466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601')
);

Aquí está mi problema. Sigo recibiendo esta extraña dirección de vuelta. Comienza con 0x, tiene 20 bytes, pero no tiene [af]:

0x3433663632613135303262303137663531633431

Si cambié r y s, obtengo casi el mismo resultado.
Me preguntaba si alguien puede verificar mi experiencia o señalar qué estaba haciendo mal. Me siento como un loco aquí.

Gracias por toda tu ayuda.

Respuestas (4)

Estaba teniendo el mismo problema anteriormente, así que voy a dar una respuesta extensa sobre cómo funciona esto. Supongo que estás usando geth como cliente. Hay un problema abierto donde el cliente geth regresa ven el formato incorrecto, así que tengamos en cuenta que si obtenemos un vque es 0o 1debemos agregarlo 27. Si está ejecutando node y ha conectado web3 a su cliente favorito:

var msg = web3.sha3('Schoolbus')
var signature = web3.eth.sign(web3.eth.accounts[0], msg)

En mi caso, la firma es:

0x28c412923e03982efdff078f78bb70eaefe32c11751b0c23858191c18dddc4ba72c3667c07672b97c022beb857afb99c49b7084da1608e20392c274adc7dd5851c

La cadena representa r, sy vrespectivamente en ese orden . Sin embargo, para alimentarlo a su contrato de autenticación, debe convertirlo va un uint8y agregarlo, asegúrese de tener el prefijo hexadecimal en 0xtodas partes:

var r = signature.slice(0, 66)
var s = '0x' + signature.slice(66, 130)
var v = '0x' + signature.slice(130, 132)
v = web3.toDecimal(v)
msg = '0x' + msg

Recuerde que vdebe ser 27o 28! Si no es así, establezca v = v + 27. Ahora puede llamar a su función de verificación como:

var addr = Auth_instance.verify.call(msg, v, r, s)

y puedes comprobar que addrtiene el mismo valor que web3.eth.accounts[0].

Gracias por tu increíble visión. Tuve dificultades para encontrar información sobre el pedido de r,s,v en eth_sign. Sin embargo, después de corregir r,s,v y enviar como v,r,s usando 27 para 00 y 28 para 01 para v, sigo recibiendo direcciones que no coinciden con la dirección de mi cuenta eth. Más importante aún, el hexadecimal de 20 bytes que obtengo no tiene caracteres [af], por lo que parecen decimales, excepto que tienen el prefijo '0x'.
¿Sigues usando esa clave privada que diste en el ejemplo? No parece estar generado correctamente. Una clave privada debe tener 64 caracteres hexadecimales (sin incluir el prefijo), la suya tiene 40. Además, eth_sign toma una dirección como entrada, no una clave privada. También puede jugar con las funciones ecsign y ecrecover desde aquí: github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md . Se ocupan explícitamente de claves y direcciones públicas y privadas. Avísame si todavía tienes problemas
Gracias por señalar el problema de la clave privada. Mi clave privada (o dirección en este caso) proviene directamente de la documentación JSON-RPC.md para eth_sign. Asumí que si enviaba mi dirección como argumento, geth usaría automáticamente la clave privada asociada con esa dirección para firmar el mensaje. Llamar a eth_sign requiere que desbloquee mi cuenta, por eso asumí. Revisaré el documento que usted señaló para investigar más, lo aprecio. Los documentos parecen estar en muchos lugares diferentes que no son tan obvios.
Como puede ver en la respuesta de @MrChico, el primer argumento web3.eth.sign(...)es la dirección de la cuenta con cuya clave privada desea iniciar sesión. Entonces, el valor que está enviando, que es de 40 caracteres hexadecimales (es decir, 20 bytes), es la longitud correcta para una dirección. Por tu comentario, parece que realmente estás enviando la dirección. Simplemente no lo llames "clave privada". Una dirección es en gran medida una pieza pública de información. (Por lo general, se genera de alguna manera a partir de la clave pública .
¿Hay un web3.eth.verify(0x..) ?
Hay un web3.personal.ecRecover() que le permite verificar la firma independientemente de Solidity. La respuesta anterior tampoco funciona para mí. Resulta que también necesita editar el contrato de Solidity. Consulte esta respuesta para saber cómo debe llamarse ecrecover en el contrato de Solidity: ethereum.stackexchange.com/questions/15364/…

Aquí hay un ejemplo de trabajo que probé usando trufa:

Ejemplo.sol

pragma solidity ^0.4.0;

contract Example {
    function testRecovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) returns (address) {
       /* prefix might be needed for geth only
        * https://github.com/ethereum/go-ethereum/issues/3731
        */
        // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        // h = sha3(prefix, h);
        address addr = ecrecover(h, v, r, s);

        return addr;
    }
}

Aquí hay algunos ejemplos que demuestran cómo obtener los valores v, r y s mediante el corte y la prueba de que ecrecover devuelve la dirección que firmó el mensaje:

var Example = artifacts.require('./Example.sol')

var Web3 = require('web3')
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))

contract('Example', (accounts) => {
  var address = accounts[0]

  it('ecrecover result matches address', async function() {
    var instance = await Example.deployed()
    var msg = '0x8CbaC5e4d803bE2A3A5cd3DbE7174504c6DD0c1C'

    var h = web3.sha3(msg)
    var sig = web3.eth.sign(address, h).slice(2)
    var r = `0x${sig.slice(0, 64)}`
    var s = `0x${sig.slice(64, 128)}`
    var v = web3.toDecimal(sig.slice(128, 130)) + 27

    var result = await instance.testRecovery.call(h, v, r, s)
    assert.equal(result, address)
  })
})

Prueba de carrera:

$ truffle test

Using network 'development'.

Compiling ./contracts/Example.sol...


  Contract: Example
    ✓ ecrecover result matches address (132ms)


  1 passing (147ms)

Probablemente sea mejor hacer el prefijo en el nivel de la aplicación en lugar de en el contrato de solidez, ya que será más económico.


Aquí hay una biblioteca de ayuda con un método que acepta el hash de los datos y la firma y devuelve la dirección de firma. El contrato inteligente maneja la obtención de los valores v, r y s en lugar de hacerlo a nivel de aplicación:

ECVerify.sol

pragma solidity ^0.4.4;

/*
 * @credit https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d
  */
library ECVerify {
  function ecrecovery(bytes32 hash, bytes sig) public returns (address) {
    bytes32 r;
    bytes32 s;
    uint8 v;

    if (sig.length != 65) {
      return 0;
    }

    assembly {
      r := mload(add(sig, 32))
      s := mload(add(sig, 64))
      v := and(mload(add(sig, 65)), 255)
    }

    // https://github.com/ethereum/go-ethereum/issues/2053
    if (v < 27) {
      v += 27;
    }

    if (v != 27 && v != 28) {
      return 0;
    }

    /* prefix might be needed for geth only
     * https://github.com/ethereum/go-ethereum/issues/3731
     */
    // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
    // hash = sha3(prefix, hash);

    return ecrecover(hash, v, r, s);
  }

  function ecverify(bytes32 hash, bytes sig, address signer) public returns (bool) {
    return signer == ecrecovery(hash, sig);
  }
}

Aquí hay algunos ejemplos sobre cómo crear firmas con web3 y probarlas:

var ECVerify = artifacts.require('./ECVerify.sol')

contract('ECVerify', (accounts) => {
  it('should return signing address from signature', async () => {
    var account = accounts[0]

    try {
      var instance = await ECVerify.deployed()

      var msg = 'some data'

      var hash = web3.sha3(msg)
      var sig = web3.eth.sign(account, hash)

      var signer = await instance.ecrecovery(hash, sig)
      assert.ok(signer)
    } catch(error) {
      console.error(error)
      assert.equal(error, undefined)
    }
  })

  it('should verify signature is from address', async () => {
    var account = accounts[0]

    try {
      var instance = await ECVerify.deployed()
      var msg = 'some data'

      var hash = web3.sha3(msg)
      var sig = web3.eth.sign(account, hash)

      var verified = await instance.ecverify.call(hash, sig, account)
      assert.ok(verified)
    } catch(error) {
      console.error(error)
      assert.equal(error, undefined)
    }
  })
})

Probando que funciona:

$ truffle test

Compiling ./contracts/ECVerify.sol...
Compiling ./contracts/Migrations.sol...


  Contract: ECVerify
    ✓ should return signing address from signature (182ms)
    ✓ should verify signature is from address (142ms)


  2 passing (342ms)

Relacionado

La respuesta de Miguel Mota cubre los detalles más destacados. Sin embargo, la siguiente línea -> var sig = web3.eth.sign(account, hash)arrojó un error. Error: Provided address is invalid, the capitalization checksum test failed, or its an indrect IBAN address which can't be converted.Aparentemente, en las versiones de web3.js, estos parámetros parecen estar invertidos y debe proporcionarlos msgcomo primer parámetro y account/addresscomo segundo. Aquí hay una prueba de ejemplo que funcionó para mí.

var address = accounts[0];
it('ecrecover result matches address', async function() {
    var instance = await Adoption.deployed()
    let msg = 'I really did make this message';
    let prefix = "\x19Ethereum Signed Message:\n" + msg.length
    let h = web3.utils.sha3(prefix+msg)
    console.log(`sha3 hash ${h}`);

    let sig1 = await web3.eth.sign(msg, address);
    console.log(`signature: ${sig1}`)
    var sig = sig1.slice(2)
    var r = `0x${sig.slice(0, 64)}`
    var s = `0x${sig.slice(64, 128)}`
    var v = web3.utils.toDecimal(sig.slice(128, 130)) + 27

    var result = await instance.recoverAddr.call(h, v, r, s)
    console.log(`address: ${address}, result ${result}`)
    assert.equal(result, address)
  })

Mi código de contrato correspondiente era:

function recoverAddr(bytes32 msgHash, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
        return ecrecover(msgHash, v, r, s);
    }

Para los usuarios de Geth, lo siguiente funciona. Lo probé hace poco. En primer lugar, el código del contrato de prueba es del comentario de nivel superior. Puedes pegar ese código en https://remix.ethereum.org

pragma solidity ^0.4.0;

contract Example {
    function testRecovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) returns (address) {
       /* prefix might be needed for geth only
        * https://github.com/ethereum/go-ethereum/issues/3731
        */
        // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        // h = sha3(prefix, h);
        address addr = ecrecover(h, v, r, s);

        return addr;
    }
}

En cualquier página web, cargue el archivo web3.js. Luego, en la consola de herramientas para desarrolladores, simplemente pegue estas funciones. La llamada a la función verificationScheme(msg)con el mensaje en texto plano ascii.

function tohex(msg){
    var hexmsg = "";
    for(var i=0; i<msg.length; i++){
        hexmsg += msg.charCodeAt(i).toString(16);
    }
    return "0x"+hexmsg;
}


function verificationScheme(str){
    var msghex = tohex(str);
    var sig = web3.eth.sign(web3.eth.accounts[0], msghex);

    var r = sig.slice(0, 66);
    var s = '0x' + sig.slice(66, 130);
    var v = '0x' + sig.slice(130, 132);
    v = web3.toDecimal(v);

    var verificationMessage = "\x19Ethereum Signed Message:\n" + str.length + str;
    var verificationMessageHash = web3.sha3(verificationMessage);

    return [verificationMessageHash, v, r, s];
}

Entonces, mis resultados fueron estos: eth.accounts[0]en el código de función apunta a "0xebbc50c7afc14c693bfc26868c490ce0819cef4f". Entonces, básicamente, esa dirección se usó para firmar, y ese debería ser el resultado final del contrato. llamé verificationScheme("hello")_

Recuperé esta matriz:["0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750", 27, "0x43653d23758f13a45c498fc96c8d5d07e9fc24123d967b0cb29c48cd48e4c907", "0x365d7cebd54f5f5298b740b44004f9808a2c9791a1c2d5a7137602a0a2742f28"]

Luego llamé a la función testRecovery del contrato de ejemplo con todos los argumentos en esa matriz en ese orden. Recuperé mi dirección original "0xebbc50c7afc14c693bfc26868c490ce0819cef4f"como salida.