cowprotocol / services

Off-chain services for CoW Protocol
https://cow.fi/
Other
148 stars 61 forks source link

`driver` - `autopilot` authentication and signing #885

Open nlordell opened 1 year ago

nlordell commented 1 year ago

Communication protocol

The driver already has a keypair, which it uses for broadcasting transactions. It can reuse this keypair or for the purpose of signing messages.

The autopilot will have a single keypair specifically for the purpose of singing messages. We will also make our pubkey known to the solvers and the community.

The raw string of the body of each request that the autopilot makes will be signed using the autopilot keypair. The signature will be in a header, e.g. X-Signature. The raw string of the body of each response that the driver returns will likewise be signed with the driver keypair and specified in the X-Signature header of the response.

This way the autopilot and driver will use signatures to authenticate themselves between each other. The signatures can also be used by either party at a later point in time to prove that they received a certain request/response from the other party.

Signatures

The protocol will use secp256k1 signatures over the keccak256 hash of the raw bytes of the body data encoded as UTF-8.

Tradeoffs

The downside is that all data is signed in its raw format. Of course the signature will be over a hash of the data, but the problem is that if a party wants to be able to prove at a later time that they did receive a certain request, they have to store all of the raw data of the request. In practice this is unlikely to be an issue, since storage tends to be relatively cheap.

The upside is that the protocol is simple and the signature can be checked before anything is done with the data, so we avoid the doom principle.

vkgnosis commented 1 year ago

I've thought about the signatures today. In any case, we're going to have some kind of public key cryptography scheme that allows us to ensure only the solver produced the message. Naturally that can be secp256k because that is used in the Ethereum ecosystem already. The harder question is what exactly the signed message should be:

  1. What Nic suggests above. Use EIP-712 on the high level representation of our API types.
  2. Come up with our own encoding scheme of the high level types and sign that.
  3. Sign the encoded json that the driver responds with.

The advantage of 3 is that we don't have to come up with any encoding. The driver signs exactly what it has sent. The downside of this is that it means we have to keep those bytes around too because that is the only way to get back to the original message. For example, after parsing into natural Rust types, we would still need to keep the original Vec<u8> around and store it in the database to later prove that this is what was sent.

3 requires a more awkward API specification too because we can't store the signature and the signed data in the same json struct because the signature depends on the encoding. If we want the API to return a single json object then we would need to double encode the message which is weird. For example, we would return an object { signature: "...", data: "..." } where the content of data is itself a string that is json encoded object.

1 and 2 apply a transformation on the high level data to turn it into signable bytes. This has the advantage that we can handle the data in whatever way is natural while keeping the ability to validate its signature.

A downside of 1 is that EIP-712 doesn't have an easy to use Rust library. We'd have to implement our own or do the encoding by hand. If we have a type like a BigInteger we would still need to model how it should be represented by EIP-712 types. The advantage is that with an use EIP-712 library, it would be easy to change the message type later.

With 2 we would pick our own encoding. This might be better than 1 because it can be simpler than full EIP-712. I would consider this if it turns out that the data we care about having signed is very simple. For example, the data that needs to be signed for /solve is only the auction id and the promised objective value. This is easy to manually transform into bytes.

But if we need to sign the full auction struct in /execute then this becomes unreasonable. Maybe for /execute we only need to sign a message like "i promise to execute with and i will tag my transaction with ", which would again make it simple. Do we need to sign the full auction data? I'm not sure. Autopilot doesn't react in any way to the auction so it seems like it doesn't need to be signed.

vkgnosis commented 1 year ago

It might not be obvious why you can't "just sign the json". This is partially explained in EIP-712. A short example of the problem is considering signing a single json integer. Signing usually works on raw bytes. The interface could look like fn sign(PrivateKey, data: &[u8]) -> Signature and fn validate_signature(PublicKey, data: &[u8]) -> bool.

How do we turn the json integer into bytes to use in this api? The naive approach is to use whatever json serializer you have and sign the serialized bytes. The problem is that in json the integers 42, 042, 42.0 are all semantically the same. If I receive the json integer, deserialize it into a Rust u64, store that in my database, then there is no guarantee that I will later be able to recreate the original message that was signed.

This is why in approach 3, the original message needs to be kept around.

vkgnosis commented 1 year ago

Niksha found an existing json signing standard like EIP-712 https://en.wikipedia.org/wiki/JSON_Web_Signature . This is another consideration.

sistemd commented 1 year ago

This is how I imagine the process to go:

The driver already has a keypair, which it uses for broadcasting transactions. It can reuse this keypair or generate a new one specifically for the purpose of signing messages - I think either approach is fine.

The autopilot will have a keypair specifically for the purpose of singing messages.

The raw string of the body of each request that the autopilot makes will be signed using the autopilot keypair. The signature will be in a header, e.g. X-Signature. The raw string of the body of each response that the driver returns is likewise signed with the driver keypair and specified in the X-Signature header of the response.

This way the autopilot and driver use signatures to authenticate themselves between each other. The signatures can also be used by either party at a later point in time to prove that they received a certain request/response from the other party.

The downside is that all data is signed in its raw format. Of course the signature will be over a hash of the data, but the problem is that if a party wants to be able to prove at a later time that they did receive a certain request, the party has to store all of the raw data of the request, which might not be the best choice for storage efficiency (i.e. it might be too wasteful).

The upside is that the protocol is simple and that the signature can be checked before anything is done with the data, so we avoid the doom principle. The extra storage required is unlikely to actually be a problem in practice since storage is comparatively cheap.

fleupold commented 9 months ago

Not part of the initial deliverable scope (not sure if we want to keep this issue around or close it for now)

fleupold commented 9 months ago

Something we don't need for now, but will revisit once decentralized drivers become a thing.

github-actions[bot] commented 2 months ago

This issue has been marked as stale because it has been inactive a while. Please update this issue or it will be automatically closed.

github-actions[bot] commented 3 weeks ago

This issue has been marked as stale because it has been inactive a while. Please update this issue or it will be automatically closed.