-
Notifications
You must be signed in to change notification settings - Fork 34
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
sapphire: initial notes on authenticated calls & contract authentication #476
Changes from 23 commits
8cc1219
8e75004
ec319e8
8262f9b
42f6187
583057b
d13bae5
66623ed
8f18d71
f84db5e
191172b
25bee5c
4161625
bcd2ea3
365c51f
080447e
f6fc788
170ea89
8700eb5
78381b1
2fd74ab
e83da70
49e898a
d5c5f9a
d67b20e
1e73595
d2d6591
dba0d02
9158558
e9a8410
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,209 @@ | ||||||||||
--- | ||||||||||
description: Authenticate users with your confidential contracts | ||||||||||
--- | ||||||||||
|
||||||||||
# View-Call Authentication | ||||||||||
|
||||||||||
User impersonation on Ethereum and other "Transparent EVMs" isn't a problem | ||||||||||
because **everybody** can see **all** data however the Sapphire confidential | ||||||||||
EVM prevents contracts from revealing confidential information to the wrong | ||||||||||
party (account or contract) - for this reason we cannot allow arbitrary | ||||||||||
impersonation of any `msg.sender`. | ||||||||||
|
||||||||||
In Sapphire, there are four types of contract calls: | ||||||||||
|
||||||||||
1. Contract to contract calls (also known as *internal calls*) | ||||||||||
2. Unauthenticted view calls (queries using `eth_call`) | ||||||||||
3. Authenticated view calls (signed queries) | ||||||||||
4. Transactions (authenticated by signature) | ||||||||||
|
||||||||||
Intra-contract calls always set `msg.sender` appropriately, if a contract calls | ||||||||||
another contract in a way which could reveal sensitive information, the calling | ||||||||||
contract must implement access control or authentication. | ||||||||||
|
||||||||||
By default all `eth_call` queries used to invoke contract functions have the | ||||||||||
`msg.sender` parameter set to `address(0x0)`. In contrast, authenticated calls are | ||||||||||
signed by a keypair and will have the `msg.sender` parameter correctly initialized | ||||||||||
(more on that later). Also, when a transaction is | ||||||||||
submitted it is signed by a keypair (thus costs gas and can make state updates) | ||||||||||
and the `msg.sender` will be set to the signing account. | ||||||||||
|
||||||||||
## Sapphire Wrapper | ||||||||||
|
||||||||||
The [@oasisprotocol/sapphire-paratime][sp-npm] Ethereum provider wrapper | ||||||||||
`sapphire.wrap` function will **automatically end-to-end encrypt calldata** when | ||||||||||
interacting with contracts on Sapphire, this is an easy way to ensure the | ||||||||||
calldata of your dApp transactions remain confidential - although the `from`, | ||||||||||
`to`, and `gasprice` parameters are not encrypted. | ||||||||||
|
||||||||||
[sp-npm]: https://www.npmjs.com/package/@oasisprotocol/sapphire-paratime | ||||||||||
|
||||||||||
:::tip Unauthenticated calls and Encryption | ||||||||||
|
||||||||||
Although the calls may be unauthenticated, they can still be encrypted! | ||||||||||
|
||||||||||
::: | ||||||||||
|
||||||||||
However, if the Sapphire wrapper has been attached to a signer then subsequent | ||||||||||
view calls via `eth_call` will request that the user signs them (e.g. a | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
MetaMask popup), these are called **signed queries** meaning `msg.sender` will be | ||||||||||
set to the signing account and can be used for authentication or to implement | ||||||||||
access control. This may add friction to the end-user experience and can result | ||||||||||
in frequent pop-ups requesting they sign queries which wouldn't normally require | ||||||||||
any interaction on Transparent EVMs. | ||||||||||
|
||||||||||
Let's see how Sapphire interprets different contract calls. Suppose the | ||||||||||
following solidity code: | ||||||||||
|
||||||||||
```solidity | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
contract Example { | ||||||||||
address owner; | ||||||||||
constructor () { | ||||||||||
owner = msg.sender; | ||||||||||
} | ||||||||||
function isOwner () public view returns (bool) { | ||||||||||
return msg.sender == owner; | ||||||||||
} | ||||||||||
} | ||||||||||
``` | ||||||||||
|
||||||||||
In the sample above, assuming we're calling from the same contract or account | ||||||||||
which created the contract, calling `isOwner` will return: | ||||||||||
|
||||||||||
* `false`, for `eth_call` | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
* `false`, with `sapphire.wrap` but without an attached signer | ||||||||||
* `true`, with `sapphire.wrap` and an attached signer | ||||||||||
* `true`, if called via the contract which created it | ||||||||||
* `true`, if called via transaction | ||||||||||
|
||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
## Caching Signed Queries | ||||||||||
|
||||||||||
When using signed queries the blockchain will be queried each time, however | ||||||||||
the Sapphire wrapper will cache signatures for signed queries with the same | ||||||||||
parameters to avoid asking the user to sign the same thing multiple times. | ||||||||||
|
||||||||||
Behind the scenes the signed queries use a "leash" to specify validity conditions | ||||||||||
so the query can only be performed within a block and account `nonce` range. | ||||||||||
These parameters are visible in the EIP-712 popup signed by the user. Queries | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the query is cached, then no user interaction is required? Where does EIP-712 popup then come from? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
with the same parameters will use the same leash. | ||||||||||
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Perhaps this would be a good place to have a screenshot of Metamask showing some EIP-712 content? |
||||||||||
|
||||||||||
## Daily Sign-In with EIP-712 | ||||||||||
|
||||||||||
One strategy which can be used to reduce the number of transaction signing | ||||||||||
prompts when a user interacts with contracts via a dApp is to use | ||||||||||
[EIP-712][eip-712] to "sign-in" once per day (or per-session), in combination | ||||||||||
with using two wrapped providers: | ||||||||||
|
||||||||||
[eip-712]: https://eips.ethereum.org/EIPS/eip-712 | ||||||||||
|
||||||||||
1. Provider to perform encrypted but unauthenticated view calls | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
2. Another provider to perform encrypted and authenticated transactions (or view calls) | ||||||||||
- The user will be prompted to sign each action. | ||||||||||
|
||||||||||
The two-provider pattern, in conjunction with a daily EIP-712 sign-in prompt | ||||||||||
ensures all transactions are end-to-end encrypted and the contract can | ||||||||||
authenticate users in view calls without frequent annoying popups. | ||||||||||
|
||||||||||
The code sample below uses an `authenticated` modifier to verify the sign-in: | ||||||||||
|
||||||||||
```solidity | ||||||||||
// SPDX-License-Identifier: UNLICENSED | ||||||||||
pragma solidity ^0.8.0; | ||||||||||
|
||||||||||
struct SignatureRSV { | ||||||||||
bytes32 r; | ||||||||||
bytes32 s; | ||||||||||
uint256 v; | ||||||||||
} | ||||||||||
|
||||||||||
contract SignInExample { | ||||||||||
bytes32 public constant EIP712_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); | ||||||||||
string public constant SIGNIN_TYPE = "SignIn(address user, uint32 time)"; | ||||||||||
bytes32 public constant SIGNIN_TYPEHASH = keccak256(bytes(SIGNIN_TYPE)); | ||||||||||
bytes32 public immutable DOMAIN_SEPARATOR; | ||||||||||
|
||||||||||
constructor () { | ||||||||||
DOMAIN_SEPARATOR = keccak256(abi.encode( | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
EIP712_DOMAIN_TYPEHASH, | ||||||||||
keccak256("SignInExample.SignIn"), | ||||||||||
keccak256("1"), | ||||||||||
block.chainid, | ||||||||||
address(this) | ||||||||||
)); | ||||||||||
} | ||||||||||
|
||||||||||
struct SignIn { | ||||||||||
address user; | ||||||||||
uint32 time; | ||||||||||
SignatureRSV rsv; | ||||||||||
} | ||||||||||
|
||||||||||
modifier authenticated(SignIn calldata auth) | ||||||||||
{ | ||||||||||
// Must be signed within 24 hours ago. | ||||||||||
require( auth.time > (block.timestamp - (60*60*24)) ); | ||||||||||
|
||||||||||
// Validate EIP-712 sign-in authentication. | ||||||||||
bytes32 authdataDigest = keccak256(abi.encodePacked( | ||||||||||
"\x19\x01", | ||||||||||
DOMAIN_SEPARATOR, | ||||||||||
keccak256(abi.encode( | ||||||||||
SIGNIN_TYPEHASH, | ||||||||||
auth.user, | ||||||||||
auth.time | ||||||||||
)) | ||||||||||
)); | ||||||||||
|
||||||||||
address recovered_address = ecrecover( | ||||||||||
authdataDigest, uint8(auth.rsv.v), auth.rsv.r, auth.rsv.s); | ||||||||||
|
||||||||||
require( auth.user == recovered_address, "Invalid Sign-In" ); | ||||||||||
|
||||||||||
_; | ||||||||||
} | ||||||||||
|
||||||||||
function authenticatedViewCall( | ||||||||||
SignIn calldata auth, | ||||||||||
... args | ||||||||||
) | ||||||||||
external view | ||||||||||
authenticated(auth) | ||||||||||
returns (bytes memory output) | ||||||||||
{ | ||||||||||
// Use `auth.user` instead of `msg.sender` | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
} | ||||||||||
} | ||||||||||
``` | ||||||||||
|
||||||||||
With the above contract code deployed, let's look at the frontend dApp and how | ||||||||||
it can request the user to sign-in using EIP-712. You may wish to add additional | ||||||||||
parameters which are authenticated such as the domain name. The following code | ||||||||||
example uses Ethers: | ||||||||||
|
||||||||||
```typescript | ||||||||||
const time = new Date().getTime(); | ||||||||||
const user = await eth.signer.getAddress(); | ||||||||||
|
||||||||||
// Ask user to "Sign-In" every 24 hours | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
const signature = await eth.signer._signTypedData({ | ||||||||||
name: "SignInExample.SignIn", | ||||||||||
version: "1", | ||||||||||
chainId: import.meta.env.CHAINID, | ||||||||||
verifyingContract: contract.address | ||||||||||
}, { | ||||||||||
SignIn: [ | ||||||||||
{ name: 'user', type: "address" }, | ||||||||||
{ name: 'time', type: 'uint32' }, | ||||||||||
] | ||||||||||
}, { | ||||||||||
user, | ||||||||||
time: time | ||||||||||
}); | ||||||||||
const rsv = ethers.utils.splitSignature(signature); | ||||||||||
const auth = {user, time, rsv}; | ||||||||||
// The `auth` variable can then be cached | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
// Then in future, authenticated view calls can be performed by | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
// passing auth without further user interaction authenticated data | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
await contract.authenticatedViewCall(auth, ...args); | ||||||||||
``` | ||||||||||
CedarMist marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By default? I don't think you can easily change this behavior in Sapphire so I would remove "By default".