Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isValidSignatureAndData to Bouncer to verify method calls #973

Merged

Conversation

aflesher
Copy link
Contributor

@aflesher aflesher commented Jun 1, 2018

Fixes #891

🚀 Description

SignatureBouncer.sol has two additional signature verification methods:

  1. isValidSignatureAndMethod which validates that the external method called matches the methodId in the signatures message.
  2. isValidSignatureAndData which validates that the external method called with params matches the methodId and param values in the signatures method.

Details

msg.data for any given external call is comprised of the methodId followed by the values of each parameter. A signature can append the required methodId and optionally the required parameter values to it's message. isValidSignatureAndMethod and isValidSignatureAndData validate that msg.data matches the signed data. SignatureBouncer.test.js demonstrates how to assemble the expected msg.data correctly and add it to the signature.

  • 📘 I've reviewed the OpenZeppelin Contributor Guidelines
  • ✅ I've added tests where applicable to test my new functionality.
  • 📖 I've made sure that my contracts are well-documented.
  • 🎨 I've run the JS/Solidity linters and fixed any issues (npm run lint:all:fix).

shrugs
shrugs previously approved these changes Jun 4, 2018
});
it('should accept valid method with valid params for valid user', async function () {
const methodId = getMethodId('checkValidSignatureAndData', 'address', 'uint256', 'bytes');
const val = 23;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's de-duplicate these variables by bringing them either into a context variable using a beforeEach within a context or just making them constants above these methods

@shrugs
Copy link
Contributor

shrugs commented Jun 4, 2018

Can we also add a @notice at the top of SignatureBouncer to talk about how _sig must be the last argument at all times? Good to have it on the individual methods as well.

*/
contract SignatureBouncer is Ownable, RBAC {
using ECRecovery for bytes32;

string public constant ROLE_BOUNCER = "bouncer";
uint constant METHOD_ID_SIZE = 4;
uint constant SIG_SIZE = 160;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrugs I forgot that I wanted to comment on this variable. The signature value is always 160 bytes within msg.data even though _sig.length is always much smaller (64 bytes I think?). I guess it pads this data to the max size? Regardless, it's always correct but I wanted to call attention to the fact that I wasn't totally sure why.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent a bit of time playing around with the abi documentation and realized that bytes are encoded as follows

[distance to the beginning of the argument data] -> 32 bytes
[the boolean true] -> 32 bytes
[length of the data] -> 32 bytes
[the data, right-padded to a multiple of 32] -> 64 bytes

and there we have our 160.


I have a question, though, after looking at the abi documentation: can we add a test case to see if it's possible to have a dynamic argument before the final bytes argument? i.e. can we sign a bouncer signature for

function test(bytes _someBytes, bytes _sig)
   onlyValidSignatureAndData(_sig)
{
}

my hunch is that this won't work immediately because with dynamically sized arguments, their location in the transaction data is included in the tx data before the argument itself.

i.e. I think a transaction for the above function looks like

0x12345678 // 8 bytes
  [ location of data for _someBytes ] // (8 bytes + 32 + 32)
  [ location of data for _sig ] // (8 + 32 + 32 + 32 + len(_someBytes))
  [ enc(len(_someBytes) ]
  [ _someBytes right-padded to modulo 32 ]
  [ enc(len(_sig)) ]
  [ _sig right-padded to module 32 ] // (64 bytes long in our case)

which means we're lucky that the examples only use fixed-sized arguments so we don't have to calculate stuff like "at what offset will the encoded signature be in the transaction data for this function call".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for shedding some light on this. I'll add that test and see what we get

@shrugs shrugs dismissed their stale review June 5, 2018 01:12

question about non-fixed-sized arguments before _sig

@aflesher
Copy link
Contributor Author

aflesher commented Jun 5, 2018

Ok, I altered those tests to verify that variable sized parameters will validate correctly.

The only change to the SignatureBouncer contract was to crop the last 128 bytes instead of the last 160 bytes. The signature is 65 bytes not 64 bytes which is why it gets padded up to 96 bytes. So this is cropped along with the 32 bytes for the signature length size.

@shrugs
Copy link
Contributor

shrugs commented Jun 6, 2018

Looks like it is necessary to construct the full tx data, sans signature, and then sign it, which is a little cumbersome in practice; it means we won't be able to use the default web3 tx construction code.

Looks like this calls for a little javascript helper in the future, as a separate package most likely.

This looks good to me! @frangio can you take a look as well?

* @dev See the tests Bouncer.test.js for specific usage examples.
* @notice A method that uses the `onlyValidSignatureAndData` modifier must make the _sig
* @notice parameter the "last" parameter. You cannot sign a message that has its own
* @notice signature in it so the last 160 bytes is of msg.data (which represents _sig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this to the new value of 128 and also mention that for this to be easy to construct signatures for, your arguments must be fixed-sized?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sorry, just took one last pass through it and I think it's worth adding a comment about this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrugs Ah, sorry, my bad. I'll get that changed. I actually went and posted a detailed answer on a stack overflow thread about how msg.data is constructed https://ethereum.stackexchange.com/a/50616/40921 . Do you want me to link this thread in the comments?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, that's a solid answer

@shrugs
Copy link
Contributor

shrugs commented Jun 7, 2018

Still want a second pair of eyes; will ask the maintainers and hopefully get this in soon; I want to start using it immediately :D

Copy link
Contributor

@come-maiz come-maiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very interesting. I couldn't find anything wrong on the code, but I recommend to get an extra review from somebody more experienced than me. Maybe @frangio can do at least a safety check.

As an aside, @aflesher @shrugs would you write a blog post explaining this feature? I think it would be a nice complement to get more people using and testing it.

* @notice signature in it so the last 128 bytes is of msg.data (which represents the
* @notice length of the _sig data and the _sig data itself) is ignored when validating.
* @notice Also non fixed sized parameters make constructing the data in the signature
* @notice much more complex. See https://bit.ly/2sGSPBc for more details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the original URLs instead of shorteners. Please update the link to:
https://ethereum.stackexchange.com/a/50616

* @notice A method that uses the `onlyValidSignatureAndData` modifier must make the _sig
* @notice parameter the "last" parameter. You cannot sign a message that has its own
* @notice signature in it so the last 128 bytes is of msg.data (which represents the
* @notice length of the _sig data and the _sig data itself) is ignored when validating.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having problems parsing this:
so the last 128 bytes is of msg.data [...] is ignored.
The two "is" verbs confuse me, is that a typo?

*/
contract SignatureBouncer is Ownable, RBAC {
using ECRecovery for bytes32;

string public constant ROLE_BOUNCER = "bouncer";
uint constant METHOD_ID_SIZE = 4;
// (signature length size) 32 bytes + (signature size 65 bytes padded) 96 bytes
uint constant SIG_SIZE = 128;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why SIG instead of SIGNATURE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just keeping this variable name consistent with our _sig params naming. SIGNATURE does make more sense though, I'll change it.

/**
* @dev requires that a valid signature with a specifed method of a bouncer was provided
*/
modifier onlyValidSignatureAndMethod(bytes _sig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use _signature here too. But for consistency, maybe it's better to leave it and update it in a future branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's follow this up with a change to _signature, yeah.

@aflesher
Copy link
Contributor Author

aflesher commented Jun 8, 2018

@ElOpio A blog post explaining this feature is a great idea. I was thinking about writing a javascript helper function to construct this bytecode as well. Would web3.js be the most logical place to propose this feature? Or maybe ethereumjs-util?

@shrugs
Copy link
Contributor

shrugs commented Jun 8, 2018

It would be neat to have to bundled in the core utils, but I doubt it's general-case enough that they'd merge it. Perhaps ethjs-utils? it may fit into the web3-utils sub-package of web3, though. Hard to say

@shrugs shrugs merged commit b0292cf into OpenZeppelin:master Jun 9, 2018
@frangio
Copy link
Contributor

frangio commented Jun 9, 2018

Sorry for taking a while to get to this. Great work @aflesher!

I have some comments/ideas about this but I will open another issue to discuss there.

@frangio
Copy link
Contributor

frangio commented Jun 11, 2018

Actualy, I feel more comfortable discussing here a bit because I don't have a concrete proposal, and I have some questions.

Is this complex procedure really necessary? I saw in the comments here that the web3 tx construction code couldn't be used, but I was actually able to use getData for this purpose just fine.

this.bouncer.contract.checkValidSignatureAndData.getData(
  authorizedUser,
  this.bytesValue,
  this.uintValue,
  '0x00000000000000000000000000000000'
).slice(2, -128);

(The .contract part is necessary to retrieve the underlyling web3.Contract instance from the truffle-contract instance, which doesn't expose getData.)

Before realizing that this was possible I was going to say that the huge complexity to use these functions was indicative of an API design problem. There's still something that feels odd to me, though it was already in the previous SignatureBouncer function: why is the _sig argument a dynamic-length bytes? It will always be a fixed size byte array. Making it fixed size would complicate a bit further the serialization of the arguments, though, because fixed size arrays are inlined where they appear instead of appended at the end.

Just wanna hear your thoughts and see if we can come up with some improvements to the usage of these new functions.

@shrugs
Copy link
Contributor

shrugs commented Jun 11, 2018

@frangio that's actually super smart, I'm amazed we missed that 😨

So we can definitely use the built-in data construction code, but can't use the built-in transaction signing code. So the utility function for this should look something like:

const voucherForCallTo = (fromAddress, contract, method, ...args) => {
  const data = contract.methods[method](...args).encodeABI().slice(0, -120)
  const hash = web3.utils.soliditySha3(`${fromAddress}${contract.address}${data}`)
  return web3.eth.personalSign(hash)
}

which looks super nice (but uses web3-1.0, so we have to update that syntax for the current version
(https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#methods-mymethod-encodeabi)


on a separate note, I want to explore the idea proposed by @PhABC where a contract can be the authority, but delegate authorizations to other arbitrary logic

so something like:


contract Bouncer {
  function isValidSignature(hash, _sig)
    internal
  {
    const signer = signerOf(_sig)
    if (signer.isContract()) {
      // ERC165 detect and call on isValidSignature
      return signer.isValidSignature(hash, _sig)
    } else {
      return hasRole(signer, ROLE_BOUNCER);
    }
  }
}

contract MyContract {
  function isValidSignature(data, signature)
    public
    returns (bool)
  {
    return isInWhitelist(data.recover(signature));
    // or like `return confirmedSignatures[signature] == true` for multisig authorizations
  }
}

@aflesher
Copy link
Contributor Author

@frangio @shrugs Is there any follow-up action needed here? The unit tests could definitely be cleaned up but it sounds like you guys want to explore a different authorization pattern.

@shrugs
Copy link
Contributor

shrugs commented Jun 13, 2018

@aflesher it's more of an extension to the authorization pattern; the ability to defer the "isValidSignature" logic to a contract rather than just an account. This is actually something like 0x is doing as part of v2, so I think it's a good add.

I'll be cleaning up the signature construction part of the tests later on when we decide how to structure the contract-as-bouncer logic.

Also in that PR I'm going to update the language because I've been using bouncer to mean both the contract that checks signature and the address that signs the permissions. Gonna call the signer something like Inviter or something, unless y'all have a better name.

@frangio
Copy link
Contributor

frangio commented Jun 13, 2018

I'd like to discuss that idea elsewhere, is there an issue already?

@shrugs
Copy link
Contributor

shrugs commented Jun 13, 2018

@frangio moving to #1005 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add isValidSignatureAndData to Bouncer to verify method calls.
4 participants