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
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:
The user’s browser verifies the domain certificate and displays appropriate warnings if overtaken
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
The config file contains an array of objects of the form { id, alg, publicKey }
For signed requests, the dapp first securely signs the payload with a private key, for example by submitting a request to its backend
The original payload, signature, and public key id are sent to the wallet via the wallet_signedRequest RPC method
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’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’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: TWIT=/.well-known/twit.json
Example TWIT manifest at https://my-crypto-dapp.invalid.com/twit.json:
Here’s a non-normative example of calling wallet_signedRequest using the EIP-1193 provider interface:
constkeyId='1';constrequestPayload:RequestPayload<TransactionParams>={method:'eth_sendTransaction',params:[{/* ... */},],};constsignature:`0x${string}`=awaitgetSignature(requestPayload,keyId);// Using the EIP-1193 provider interfaceconstresult=awaitethereum.request({method:'wallet_signedRequest',params:[requestPayload,signature,keyId],});
Signature verification
Upon receiving an EIP-1193 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
If TWIT is NOT configured for the sender.tab.url domain, then proceed as usual
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
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)
asyncfunctionsignedRequest(requestPayload:RequestPayload<unknown>,signature:`0x${string}`,keyId:string,):Promise<unknown>{// 1. Get the domain of the sender.tab.urlconstdomain=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'`constdoh=require('dohjs');constresolver=newdoh.DohResolver('https://1.1.1.1/dns-query');letmanifestPath='';constdnsResp=awaitresolver.query(domain,'TXT');for(recordofdnsResp.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`constmanifestReq=awaitfetch(manifestPath);constmanifest=awaitmanifestReq.json();constkeyData=manifest.publicKeys.filter((x)=>x.id==keyId);if(!keyData){thrownewError('Could not find the signing key');}constkey=keyData.publicKey;constalg=keyData.alg;// 4. Verify the signatureconstvalid=awaitcrypto.verify(alg,key,signature,requestPayload);if(!valid){thrownewError('The data was tampered with');}returnawaitprocessRequest(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 and EIP-1193. 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:
a signed payload can be replayed multiple times
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-712signTypedData 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.