-
Notifications
You must be signed in to change notification settings - Fork 445
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 ERC: Tamperproof Web Immutable Transaction (TWIT) #585
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
c826fdb
TWIT erc draft
uni-guillaume 3ca21f6
Change to RPC method, various edits
rekmarks 7e72a2e
Respond to review
rekmarks f5daee9
Merge pull request #2 from rekmarks/twit
uni-guillaume d2e5503
Merge branch 'ethereum:master' into twit
uni-guillaume 1e926b3
Update ERCS/erc-XXXX.md
uni-guillaume 712c1ef
Updating with ERC number
uni-guillaume df128d4
fix lints
uni-guillaume 99d9318
Merge branch 'master' into twit
uni-guillaume e6a1ab2
Merge branch 'master' into twit
uni-guillaume 5ec8fc1
Merge branch 'master' into twit
uni-guillaume c140521
Merge branch 'master' into twit
uni-guillaume 6e1ef5e
Merge branch 'master' into twit
uni-guillaume 3fc6431
Merge branch 'master' into twit
uni-guillaume 0cd1608
Merge branch 'master' into twit
uni-guillaume 7c51a71
Merge branch 'master' into twit
uni-guillaume 8c550be
Merge branch 'master' into twit
uni-guillaume 802c11d
Merge branch 'master' into twit
uni-guillaume b82a1a2
Adressing comments
uni-guillaume File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
--- | ||
eip: 7754 | ||
title: Tamperproof Web Immutable Transaction (TWIT) | ||
description: Provides a mechanism for dapps to use the extension wallets API in a tamperproof way | ||
author: Erik Marks (@remarks), Guillaume Grosbois (@uni-guillaume) | ||
discussions-to: https://ethereum-magicians.org/t/erc-7754-tamperproof-web-immutable-transaction-twit/20767 | ||
status: Draft | ||
type: Standards Track | ||
category: ERC | ||
created: 2024-07-29 | ||
requires: 1193 | ||
--- | ||
|
||
## Abstract | ||
|
||
Introduces a new RPC method to be implemented by wallets, `wallet_signedRequest`, that | ||
enables dapps to interact with wallets in a tamperproof manner via "signed requests". The | ||
dapp associates a public key with its DNS record and uses the corresponding private key to | ||
sign payloads sent to the wallet via `wallet_signedRequest`. Wallets can then use use the | ||
public key in the DNS record to validate the integrity of the payload. | ||
|
||
## Motivation | ||
|
||
This standard aims to enhance the end user's experience by granting them confidence that requests from their dapps have not been tampered with. | ||
In essence, this is similar to how HTTPS is used in the web. | ||
|
||
Currently, the communication channel between dapps and wallets is vulnerable to man in the middle attacks. | ||
Specifically, attackers can intercept RPC requests by injecting JavaScript code in the page, | ||
via e.g. an XSS vulnerability or due to a malicious extension. | ||
Once an RPC request is intercepted, it can be modified in a number of pernicious ways, including: | ||
|
||
- Editing the calldata in order to siphon funds or otherwise change the transaction outcome | ||
- Modifying the parameters of an [EIP-712](./eip-712.md) request | ||
- Obtaining a replayable signature from the wallet | ||
|
||
Even if the user realizes that requests from the dapp may be tampered with, they have little to no recourse to mitigate the problem. | ||
Overall, the lack of a chain of trust between the dapp and the wallet hurts the ecosystem as a whole: | ||
|
||
- Users cannot simply trust otherwise honest dapps, and are at risk of losing funds | ||
- Dapp maintainers are at risk of hurting their reputations if an attacker finds a viable MITM attack | ||
|
||
For these reasons, we recommend that wallets implement the `wallet_signedRequest` RPC method. | ||
This method provides dapp developers with a way to explicitly ask the wallet to verify the | ||
integrity of a payload. This is a significant improvement over the status quo, which forces | ||
dapps to rely on implicit approaches such as argument bit packing. | ||
|
||
## Specification | ||
|
||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. | ||
|
||
### Overview | ||
|
||
We propose to use the dapp's domain certificate of a root of trust to establish a trust chain as follow: | ||
|
||
1. The user's browser verifies the domain certificate and displays appropriate warnings if overtaken | ||
2. The DNS record of the dapp hosts a TXT field pointing to a URL where a JSON manifest is hosted | ||
- This file SHOULD be at a well known address such as `https://example.com/.well-known/twit.json` | ||
3. The config file contains an array of objects of the form `{ id, alg, publicKey }` | ||
4. For signed requests, the dapp first securely signs the payload with a private key, for example by submitting a request to its backend | ||
5. The original payload, signature, and public key id are sent to the wallet via the `wallet_signedRequest` RPC method | ||
6. The wallet verifies the signature before processing the request normally | ||
|
||
### Wallet integration | ||
|
||
#### Key discovery | ||
|
||
Attested public keys are necessary for the chain of trust to be established. | ||
Since this is traditionally done via DNS certificates, we propose the addition of a DNS record containing the public keys. | ||
This is similar to [RFC-6636](https://www.rfc-editor.org/rfc/rfc6376.html)'s DKIM, but the use of a manifest file provides more flexibility for future improvements, as well as support for multiple algorithm and key pairs. | ||
|
||
Similarly to standard [RFC-7519](https://www.rfc-editor.org/rfc/rfc7519.html)'s JWT practices, the wallet could eagerly cache dapp keys. | ||
However, in the absence of a revocation mechanism, a compromised key could still be used until caches have expired. | ||
To mitigate this, wallets SHOULD NOT cache dapp public keys for more than 2 hours. | ||
This practice establishes a relatively short vulnerability window, and manageable overhead for both wallet and dapp maintainers. | ||
|
||
Example DNS record for `my-crypto-dapp.invalid`: | ||
|
||
```txt | ||
... | ||
TXT: TWIT=/.well-known/twit.json | ||
``` | ||
|
||
Example TWIT manifest at `https://my-crypto-dapp.invalid.com/twit.json`: | ||
|
||
```json | ||
{ | ||
"publicKeys": [ | ||
{ "id": "1", "alg": "ECDSA", "publicKey": "0xaf34..." }, | ||
{ "id": "2", "alg": "RSA-PSS", "publicKey": "0x98ab..." } | ||
] | ||
} | ||
``` | ||
|
||
Dapps SHOULD only rely on algorithms available via [SubtleCrypto](https://www.w3.org/TR/2017/REC-WebCryptoAPI-20170126/), since they are present in every browser. | ||
|
||
#### Manifest schema | ||
|
||
We propose a simple and extensible schema: | ||
|
||
```json | ||
{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"title": "TWIT manifest", | ||
"type": "object", | ||
"properties": { | ||
"publicKeys": { | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"id": { "type": "string" }, | ||
"alg": { "type": "string" }, | ||
"publicKey": { "type": "string" } | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
#### RPC method | ||
|
||
The parameters of `wallet_signedRequest` are specified by this TypeScript interface: | ||
|
||
```typescript | ||
type RequestPayload<Params> = { method: string; params: Params }; | ||
|
||
type SignedRequestParameters<Params> = [ | ||
requestPayload: RequestPayload<Params>, | ||
signature: `0x${string}`, | ||
keyId: string, | ||
]; | ||
``` | ||
|
||
Here's a non-normative example of calling `wallet_signedRequest` using the [EIP-1193](./eip-1193.md) provider interface: | ||
|
||
```typescript | ||
const keyId = '1'; | ||
const requestPayload: RequestPayload<TransactionParams> = { | ||
method: 'eth_sendTransaction', | ||
params: [ | ||
{ | ||
/* ... */ | ||
}, | ||
], | ||
}; | ||
const signature: `0x${string}` = await getSignature(requestPayload, keyId); | ||
|
||
// Using the EIP-1193 provider interface | ||
const result = await ethereum.request({ | ||
method: 'wallet_signedRequest', | ||
params: [requestPayload, signature, keyId], | ||
}); | ||
``` | ||
|
||
#### Signature verification | ||
|
||
1. Upon receiving an [EIP-1193](./eip-1193.md) call, the wallet MUST check of the existence of the TWIT manifest for the `sender.tab.url` domain | ||
a. The wallet MUST verify that the manifest is hosted on the `sender.tab.url` domain | ||
b. The wallet SHOULD find the DNS TXT record to find the manifest location | ||
b. The wallet MAY first try the `/.well-known/twit.json` location | ||
2. If TWIT is NOT configured for the `sender.tab.url` domain, then proceed as usual | ||
3. If TWIT is configured and the `request` method is used, then the wallet SHOULD display a visible and actionable warning to the user | ||
a. If the user opts to ignore the warning, then proceed as usual | ||
b. If the user opts to cancel, then the wallet MUST cancel the call | ||
4. If TWIT is configured and the `wallet_signedRequest` method is used with the parameters `requestPayload`, `signature` and `keyId` then: | ||
a. The wallet MAY display a visible cue indicating that this interaction is signed | ||
b. The wallet MUST verify that the keyId exists in the TWIT manifest and find the associated key record | ||
c. From the key record, the wallet MUST use the `alg` field and the `publicKey` field to verify `requestPayload` integrity by calling `crypto.verify(alg, key, signature, requestPayload)` | ||
d. If the signature is invalid, the wallet MUST display a visible and actionable warning to the user | ||
i. If the user opts to ignore the warning, then proceed to call `request` with the argument `requestPayload` | ||
ii. If the user opts to cancel, then the wallet MUST cancel the call | ||
e. If the signature is valid, the wallet MUST call `request` with the argument `requestPayload` | ||
|
||
#### Example method implementation (wallet) | ||
|
||
```typescript | ||
async function signedRequest( | ||
requestPayload: RequestPayload<unknown>, | ||
signature: `0x${string}`, | ||
keyId: string, | ||
): Promise<unknown> { | ||
// 1. Get the domain of the sender.tab.url | ||
const domain = getDappDomain(); | ||
|
||
// 2. Get the manifest for the current domain | ||
// It's possible to use RFC 8484 for the actual DNS-over-HTTPS specification, see https://datatracker.ietf.org/doc/html/rfc8484. | ||
// However, here we are doing it with DoHjs. | ||
// This step is optional, and you could go directly to the well-known address first at `domain + '/.well-known/twit.json'` | ||
const doh = require('dohjs'); | ||
const resolver = new doh.DohResolver('https://1.1.1.1/dns-query'); | ||
|
||
let manifestPath = ''; | ||
const dnsResp = await resolver.query(domain, 'TXT'); | ||
for (record of dnsResp.answers) { | ||
if (!record.data.startsWith('TWIT=')) continue; | ||
|
||
manifestPath = record.data.substring(5); // This should be domain + '/.well-known/twit.json' | ||
break; | ||
} | ||
|
||
// 3. Parse the manifest and get they key and algo based on `keyId` | ||
const manifestReq = await fetch(manifestPath); | ||
const manifest = await manifestReq.json(); | ||
const keyData = manifest.publicKeys.filter((x) => x.id == keyId); | ||
if (!keyData) { | ||
throw new Error('Could not find the signing key'); | ||
} | ||
|
||
const key = keyData.publicKey; | ||
const alg = keyData.alg; | ||
|
||
// 4. Verify the signature | ||
const valid = await crypto.verify(alg, key, signature, requestPayload); | ||
if (!valid) { | ||
throw new Error('The data was tampered with'); | ||
} | ||
return await processRequest(requestPayload); | ||
} | ||
``` | ||
|
||
### Wallet UX suggestion | ||
|
||
Similarly to the padlock icon for HTTPS, wallets should display a visible indication when TWIT is configured on a domain. This will improve the UX of the end user who will immediately be able to tell | ||
that interactions between the dapp they are using and the wallet are secure, and this will encourage dapp developer to adopt TWIT, making the overall ecosystem more secure | ||
|
||
When dealing with insecure request, either because the dapp (or an attacker) uses `request` on a domain where TWIT is configured, or because the signature does not match, wallets should warn the user but | ||
not block: an eloquently worded warning will increase the transparency enough that end user may opt to cancel the interaction or proceed with the unsafe call. | ||
|
||
## Rationale | ||
|
||
The proposed implementation does not modify any of the existing functionalities offered by [EIP-712](./eip-712.md) and [EIP-1193](./eip-1193.md). Its additive | ||
nature makes it inherently backward compatible. Its core design is modeled after existing solutions to existing problems (such as DKIM). As a result the proposed specification will be non disruptive, easy to | ||
implements for both wallets and dapps, with a predictable threat model. | ||
|
||
## Security Considerations | ||
|
||
### Replay prevention | ||
|
||
While signing the `requestArg` payload guarantees data integrity, it does not prevent replay attacks in itself: | ||
|
||
1. a signed payload can be replayed multiple times | ||
2. a signed payload can be replayed across multiple chains | ||
|
||
_Effective_ time replay attacks as described in `1.` are generally prevented by the transaction nonce. | ||
Cross chain replay can be prevented by leveraging the [EIP-712](./eip-712.md) `signTypedData` method. | ||
|
||
Replay attack would still be possible on any method that is not protected by either: this affects effectively all the "readonly" methods | ||
which are of very limited value for an attacker. | ||
|
||
For these reason, we do not recommend a specific replay protection mechanism at this time. If/when the need arise, the extensibility of | ||
the manifest will provide the necessary room to enforce a replay protection envelope (eg:JWT) for affected dapp. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via [CC0](../LICENSE.md). |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Should probably move this to the Reference Implementation section and link to it there from here.
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.
because of the use of dohjs library, it is more an example than a reference implementation. Happy to move it if you think that is ok.
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.
It's up to you. It's short enough that it's probably fine to stay where it is.