NEP | Title | Author | Status | Type | Category | Created |
---|---|---|---|---|---|---|
413 |
Near Wallet API - support for signMessage method |
Philip Obosi <philip@near.org>, Guillermo Gallardo <guillermo@near.org> |
Approved |
Standards Track |
Wallet |
25-Oct-2022 |
A standardized Wallet API method, namely signMessage
, that allows users to sign a message for a specific recipient using their NEAR account.
NEAR users want to create messages destined to a specific recipient using their accounts. This has multiple applications, one of them being authentication in third-party services.
Currently, there is no standardized way for wallets to sign a message destined to a specific recipient.
Users want to sign messages for a specific recipient without incurring in GAS fees, nor compromising their account's security. This means that the message being signed:
- Must be signed off-chain, with no transactions being involved.
- Must include the recipient's name and a nonce.
- Cannot represent a valid transaction.
- Must be signed using a Full Access Key.
- Should be simple to produce/verify, and transmitted securely.
So the user would not incur in GAS fees, nor the signed message gets broadcasted into a public network.
An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it.
In NEAR, transactions are encoded in Borsh before being signed. The first attribute of a transaction is a signerId: string
, which is encoded as: (1) 4 bytes representing the string's length, (2) N bytes representing the string itself.
By prepending the prefix tag
To stop a malicious app from requesting the user to sign a message for them, only to relay it to a third-party. Including the recipient and making sure the user knows about it should mitigate these kind of attacks.
Meanwhile, including a nonce helps to mitigate replay attacks, in which an attacker can delay or re-send a signed message.
Why using a FullAccess Key? Why Not Simply Creating an FunctionCall Key for Signing?
The most common flow for NEAR user authentication into a Web3 frontend involves the creation of a FunctionCall Key.
One might feel tempted to reproduce such process here, for example, by creating a key that can only be used to call a non-existing method in the user's account. This is a bad idea because:
- The user would need to expend gas in creating a new key.
- Any third-party can ask the user to create a
FunctionCall Key
, thus opening an attack vector.
Using a FullAccess key allows us to be sure that the challenge was signed by the user (since nobody should have access to their FullAccess Key
), while keeping the constraints of not expending gas in the process (because no new key needs to be created).
Including a state helps to mitigate CSRF attacks. This way, if a message needs to be signed for authentication purposes, the auth service can keep a state to make sure the auth request comes from the right author.
Sending the signed message in a query string to an arbitrary URL (even within the correct domain) is not secure as the data can be leaked (e.g. through headers, etc). Using URL fragments instead will improve security, since URL fragments are not included in the Referer
.
NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see near/nearcore#2835). Any protocol that signs anything with NEAR account keys should use the same signature format.
Wallets must implement a signMessage
method, which takes a message
destined to a specific recipient
and transform it into a verifiable signature.
signMessage
must implement the following input interface:
interface SignMessageParams {
message: string ; // The message that wants to be transmitted.
recipient: string; // The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
nonce: [u8; 32] ; // A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`.
state?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). A state for authentication purposes.
}
signMessage
must embed the input message
, recipient
and nonce
into the following predefined structure:
struct Payload {
message: string; // The same message passed in `SignMessageParams.message`
nonce: [u8; 32]; // The same nonce passed in `SignMessageParams.nonce`
recipient: string; // The same recipient passed in `SignMessageParams.recipient`
callbackUrl?: string // The same callbackUrl passed in `SignMessageParams.callbackUrl`
}
In order to create a signature, signMessage
must:
- Create a
Payload
object. - Convert the
payload
into its Borsh Representation. - Prepend the 4-bytes borsh representation of
$2^{31}+413$ , as the prefix tag. - Compute the
SHA256
hash of the serialized-prefix + serialized-tag. - Sign the resulting
SHA256
hash from step 3 using a full-access key.
If the wallet does not hold any
full-access
keys, then it must return an error.
Assuming that the signMessage
method was invoked, and that:
- The input
message
is"hi"
- The input
nonce
is[0,...,31]
- The input
recipient
is"myapp.com"
- The callbackUrl is
"myapp.com/callback"
- The wallet stores a full-access private key
The wallet must construct and sign the following SHA256
hash:
// 2**31 + 413 == 2147484061
sha256.hash(Borsh.serialize<u32>(2147484061) + Borsh.serialize(Payload{message:"hi", nonce:[0,...,31], recipient:"myapp.com", callbackUrl: "myapp.com/callback"}))
signMessage
must return an object containing the base64 representation of the signature
, and all the data necessary to verify such signature.
interface SignedMessage {
accountId: string; // The account name to which the publicKey corresponds as plain text (e.g. "alice.near")
publicKey: string; // The public counterpart of the key used to sign, expressed as a string with format "<key-type>:<base58-key-bytes>" (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y")
signature: string; // The base64 representation of the signature.
state?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The same state passed in SignMessageParams.
}
Web Wallets, such as MyNearWallet, should directly return the SignedMessage
to the SignMessageParams.callbackUrl
, passing the accountId
,publicKey
, signature
and the state as URL fragments. This is: <callbackUrl>#accountId=<accountId>&publicKey=<publicKey>&signature=<signature>&state=<state>
.
If the signing process fails, then the wallet must return an error message and the state as string fragments: <callbackUrl>#error=<error-message-string>&state=<state>
.
Non-web Wallets, such as Ledger can directly return the SignedMessage
(in preference as a JSON object) and raise an error on failure.
A full example on how to implement the signMessage
method can be found here.
Accounts that do not hold a FullAccess Key will not be able to sign this kind of messages. However, this is a necessary tradeoff for security since any third-party can ask the user to create a FunctionAccess key.
At the time of writing this NEP, the NEAR ledger app is unable to sign this kind of messages, since currently it can only sign pure transactions. This however can be overcomed by modifying the NEAR ledger app implementation in the near future.
Non-expert subjects could use this standard to authenticate users in an unsecure way. To anyone implementing an authentication service, we urge them to read about CSRF attacks, and make use of the state
field.
The Wallet Standards Working Group members approved this NEP on January 17, 2023 (meeting recording).
Important Security concerns were raised by a community member, driving us to change the proposed implementation.
- Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts.
- Makes it possible to authorize through jwt in web2 services using the NEAR account.
- Removes delays in adding transactions to the blockchain and makes the experience of using projects like NEAR Social better.
# | Concern | Resolution | Status |
---|---|---|---|
1 | Implementing the signMessage standard will divide wallets into those that will quickly add support for it and those that will take significantly longer. In this case, some services may not work correctly for some users | (1) Be careful when adding functionality with signMessage with legacy and ensure that alternative authorization methods are possible. For example by adding publicKey. (2) Oblige wallets to implement a standard in specific deadlines to save their support in wallet selector | Resolved |
2 | Large number of off-chain transactions will reduce activity in the blockchain and may negatively affect NEAR rate and attractiveness to third-party developers | There seems to be a general agreement that it is a good default | Resolved |
3 | receiver terminology can be misleading and confusing when existing functionality is taken into consideration (signTransaction ) |
It was recommended for the community to vote for a new name, and the NEP was updated changing receiver to recipient |
Resolved |
4 | The NEP should emphasize that nonce and receiver should be clearly displayed to the user in the signing requests by wallets to achieve the desired security from these params being included |
We strongly recommend the wallet to clearly display all the elements that compose the message being signed. However, this pertains to the wallet's UI and UX, and not to the method's specification, thus the NEP was not changed. | Resolved |
5 | NEP-408 (Injected Wallet API) should be extended with this new signMessage method |
It is not a blocker for this NEP, but a follow-up NEP-extension proposal is welcome. | Resolved |
Copyright and related rights waived via CC0.