From 1dbc4448c6bde487bbe3006f1896bf44f50c16d6 Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Wed, 12 Oct 2022 20:19:02 +0300 Subject: [PATCH 01/40] feat: add `verifyOwner` to Bridger Wallets Standards --- specs/Standards/Wallets/BridgeWallets.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/specs/Standards/Wallets/BridgeWallets.md b/specs/Standards/Wallets/BridgeWallets.md index 34c88764d..87465e98e 100644 --- a/specs/Standards/Wallets/BridgeWallets.md +++ b/specs/Standards/Wallets/BridgeWallets.md @@ -116,6 +116,28 @@ interface GetAccountsParams {} type GetAccountsResponse = Array; ``` +#### `verifyOwner` +Signs the message and verifies the owner. Message is not sent to blockchain. + +```ts +interface VerifyOwnerParams { + // The message requested sign. Defaults to `verify owner` string. + message?: string; + //Account ID used to sign the message. Defaults to the first account + signerId?: string; + //Public key used to sign the message. Defaults to the public key of the signed in account. + publicKey?: PublicKey; + // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. + callbackUrl?: string; + // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. + meta?: string; +} + +type VerifyOwnerResponse = Promise; + +// Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. +``` + ## Flows **Connect** From 432734818d14a4972cf32d0d6dfda4fee18e7f69 Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Tue, 25 Oct 2022 20:29:16 +0300 Subject: [PATCH 02/40] NEP Draft --- neps/nep-0413.md | 76 ++++++++++++++++++++++++ specs/Standards/Wallets/BridgeWallets.md | 5 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 neps/nep-0413.md diff --git a/neps/nep-0413.md b/neps/nep-0413.md new file mode 100644 index 000000000..2c9815760 --- /dev/null +++ b/neps/nep-0413.md @@ -0,0 +1,76 @@ +--- +NEP: 413 +Title: Near Wallet API - support for verifyOwner method +Author: Philip Obosi +DiscussionsTo: https://github.com/near/NEPs/discussions/329 +Status: Draft +Type: Standards Track +Category: Wallet +Created: 25-Oct-2022 +--- + +## Summary + +Introduce `verifyOwner` method to Wallet API. + +## Motivation + +DApp developers want to be able to verify that a user owns the account they are connecting to, without adding keys to their account which results in gas fees. Hence, this feature would make it possible for app authors to verify account ownership without an actual on-chain transaction being submitted i.e no gas fees. + +## Rationale and alternatives + +A possible solution here is to introduce a restricted access key that is only allowed to call a nonexistent function on the owner's account(this will prevent exploitation as anything signed by this key will not execute on-chain). This access key would be used for all requests of this type hence eliminating the need to have a new access key created each time an app author wants the user to verify their ownership of an account or for each app author who does so. + +### The problem with a restricted access key + +Originally, NEAR was designed with the assumption that access keys are only used to sign transactions. Therefore, if those keys are used for any other purpose, it is important to make sure that they can't be abused to sign some undesirable transaction. + +When using the keys for authentication, another issue that needs to be handled is a replay attack: a malicious app might request the user to authenticate only to authenticate itself to some other app as the user. + +If the wallet allows apps to request the user to sign arbitrary data, both attacks would be possible. Thus, some measures are needed to prevent both attacks: + +1. To prevent the malicious signing of transactions, a good solution is to make sure that the data to be signed cannot be parsed as a transaction. This is preferred to having a properly formatted transaction with invalid data (such as an invalid method). The precise format of that data should be made a standard, so that, for example, a future change to transaction format will not make it possible to confuse the data for a transaction. +2. To prevent one app from relaying a signature to a different app, the data to be signed should include the requesting app's identity (e.g. domain name). Again, a well-defined format will facilitate this. + +Using an access key restricted in such a way that it cannot send transactions looks like a good temporary solution. It doesn't solve the second problem, though. + +Sending the signature in a query string to an arbitrary URL (even within the correct domain) is not secure. Most apps will have some pages that will leak this data in the Referrer header or by some other means. Using URL fragment instead of a query string will improve this (the fragment isn't included in the Referrer), and somehow permitting only a fixed callback URL will improve this even more. + +Also, note that NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. + +Hence, the final solution we are settling on is to user a restricted payload as shown below. + +## Specification + +### `verifyOwner` + +Signs the message and verifies the owner. Message is not sent to blockchain. + +```jsx +interface VerifyOwnerParams { + // The message requested sign. Defaults to `verify owner` string. + message?: string; + //Account ID used to sign the message. Defaults to the first account + signerId?: string; + //Public key used to sign the message. Defaults to the public key of the signed in account. + publicKey?: PublicKey; + // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. + callbackUrl?: string; + // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. + meta?: string; +} + +type VerifyOwnerResponse = Promise; + +// Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. + +``` + +## Reference Implementation +You can find a demo implementation of this standard using the project below: +[near/wallet-selector#320](https://github.com/near/wallet-selector/pull/320) + +## Copyright +[copyright]: #copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/specs/Standards/Wallets/BridgeWallets.md b/specs/Standards/Wallets/BridgeWallets.md index 87465e98e..c01befee4 100644 --- a/specs/Standards/Wallets/BridgeWallets.md +++ b/specs/Standards/Wallets/BridgeWallets.md @@ -123,9 +123,9 @@ Signs the message and verifies the owner. Message is not sent to blockchain. interface VerifyOwnerParams { // The message requested sign. Defaults to `verify owner` string. message?: string; - //Account ID used to sign the message. Defaults to the first account + // Account ID used to sign the message. Defaults to the first account signerId?: string; - //Public key used to sign the message. Defaults to the public key of the signed in account. + // Public key used to sign the message. Defaults to the public key of the signed in account. publicKey?: PublicKey; // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. callbackUrl?: string; @@ -134,7 +134,6 @@ interface VerifyOwnerParams { } type VerifyOwnerResponse = Promise; - // Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. ``` From 125ab00552d0ffc541ac6fc886f9d69c5b00587e Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Tue, 25 Oct 2022 20:53:31 +0300 Subject: [PATCH 03/40] cleanup --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 2c9815760..52fe135ec 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -2,7 +2,7 @@ NEP: 413 Title: Near Wallet API - support for verifyOwner method Author: Philip Obosi -DiscussionsTo: https://github.com/near/NEPs/discussions/329 +# DiscussionsTo: Status: Draft Type: Standards Track Category: Wallet From e4ece5824f45e72f1f91902bd50a82bf3d8723d3 Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Wed, 26 Oct 2022 04:48:08 +0300 Subject: [PATCH 04/40] cleanup --- specs/Standards/Wallets/BridgeWallets.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/specs/Standards/Wallets/BridgeWallets.md b/specs/Standards/Wallets/BridgeWallets.md index c01befee4..d8382ba2f 100644 --- a/specs/Standards/Wallets/BridgeWallets.md +++ b/specs/Standards/Wallets/BridgeWallets.md @@ -115,28 +115,6 @@ interface GetAccountsParams {} type GetAccountsResponse = Array; ``` - -#### `verifyOwner` -Signs the message and verifies the owner. Message is not sent to blockchain. - -```ts -interface VerifyOwnerParams { - // The message requested sign. Defaults to `verify owner` string. - message?: string; - // Account ID used to sign the message. Defaults to the first account - signerId?: string; - // Public key used to sign the message. Defaults to the public key of the signed in account. - publicKey?: PublicKey; - // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. - callbackUrl?: string; - // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. - meta?: string; -} - -type VerifyOwnerResponse = Promise; -// Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. -``` - ## Flows **Connect** From 8cc4e6ee3ffe5c26e4e7b62d3f4839856777dc93 Mon Sep 17 00:00:00 2001 From: Philip Obosi Date: Wed, 26 Oct 2022 15:17:29 +0300 Subject: [PATCH 05/40] Update BridgeWallets.md --- specs/Standards/Wallets/BridgeWallets.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/Standards/Wallets/BridgeWallets.md b/specs/Standards/Wallets/BridgeWallets.md index d8382ba2f..34c88764d 100644 --- a/specs/Standards/Wallets/BridgeWallets.md +++ b/specs/Standards/Wallets/BridgeWallets.md @@ -115,6 +115,7 @@ interface GetAccountsParams {} type GetAccountsResponse = Array; ``` + ## Flows **Connect** From 763d0ae448d32297673fe4aa5f750b94939b3641 Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Mon, 31 Oct 2022 18:53:35 +0300 Subject: [PATCH 06/40] chore: some PR review cleanups --- neps/nep-0413.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 52fe135ec..a03971242 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -15,26 +15,27 @@ Introduce `verifyOwner` method to Wallet API. ## Motivation -DApp developers want to be able to verify that a user owns the account they are connecting to, without adding keys to their account which results in gas fees. Hence, this feature would make it possible for app authors to verify account ownership without an actual on-chain transaction being submitted i.e no gas fees. +DApp developers want to be able to verify that a user owns the account they are connecting to,without incurring gas fees by adding keys to their account. This feature would make it possible for app authors to verify account ownership without an actual on-chain transaction being submitted. ## Rationale and alternatives -A possible solution here is to introduce a restricted access key that is only allowed to call a nonexistent function on the owner's account(this will prevent exploitation as anything signed by this key will not execute on-chain). This access key would be used for all requests of this type hence eliminating the need to have a new access key created each time an app author wants the user to verify their ownership of an account or for each app author who does so. +A possible solution here is to introduce a restricted access key that is only allowed to call a nonexistent function on the owner's account (this will prevent exploitation as anything signed by this key will not execute on-chain). This access key would be used for all ownership verification, eliminating the need to create access keys for every verification request. ### The problem with a restricted access key -Originally, NEAR was designed with the assumption that access keys are only used to sign transactions. Therefore, if those keys are used for any other purpose, it is important to make sure that they can't be abused to sign some undesirable transaction. +Originally NEAR was designed with the assumption that access keys would only be used to sign transactions. Using access keys for signing arbitrary payloads carries the risk of the signer inadvertently signing surreptitious transactions which the requester could in turn submit to the network. When using the keys for authentication, another issue that needs to be handled is a replay attack: a malicious app might request the user to authenticate only to authenticate itself to some other app as the user. If the wallet allows apps to request the user to sign arbitrary data, both attacks would be possible. Thus, some measures are needed to prevent both attacks: -1. To prevent the malicious signing of transactions, a good solution is to make sure that the data to be signed cannot be parsed as a transaction. This is preferred to having a properly formatted transaction with invalid data (such as an invalid method). The precise format of that data should be made a standard, so that, for example, a future change to transaction format will not make it possible to confuse the data for a transaction. +1. To prevent the malicious signing of transactions, a good solution is to make sure that the data to be signed cannot be parsed as a transaction. Having a standard format for the verification payload(different from the transaction format) will solve this. + 2. To prevent one app from relaying a signature to a different app, the data to be signed should include the requesting app's identity (e.g. domain name). Again, a well-defined format will facilitate this. Using an access key restricted in such a way that it cannot send transactions looks like a good temporary solution. It doesn't solve the second problem, though. -Sending the signature in a query string to an arbitrary URL (even within the correct domain) is not secure. Most apps will have some pages that will leak this data in the Referrer header or by some other means. Using URL fragment instead of a query string will improve this (the fragment isn't included in the Referrer), and somehow permitting only a fixed callback URL will improve this even more. +Sending the signature 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 of a query string will improve this (the fragment isn't included in the Referrer), and somehow permitting only a fixed callback URL will improve this even more. Also, note that NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. @@ -66,9 +67,14 @@ type VerifyOwnerResponse = Promise; ``` -## Reference Implementation -You can find a demo implementation of this standard using the project below: -[near/wallet-selector#320](https://github.com/near/wallet-selector/pull/320) +## References +This interface is now supported by Wallet Selector as shown below: + +> [near/wallet-selector#391](https://github.com/near/wallet-selector/pull/391) + +More developers within the ecosystem have also begun to add support for this to their Wallets. + +- [https://github.com/near/wallet-selector/pull/436/files](https://github.com/near/wallet-selector/pull/436/files) ## Copyright [copyright]: #copyright From 6a1403320ad6888065401216f4d56cb7503acd5c Mon Sep 17 00:00:00 2001 From: gutsyphilip Date: Tue, 1 Nov 2022 20:04:44 +0300 Subject: [PATCH 07/40] fix: update spec --- neps/nep-0413.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index a03971242..cc18cbc82 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -49,21 +49,23 @@ Signs the message and verifies the owner. Message is not sent to blockchain. ```jsx interface VerifyOwnerParams { - // The message requested sign. Defaults to `verify owner` string. - message?: string; - //Account ID used to sign the message. Defaults to the first account - signerId?: string; - //Public key used to sign the message. Defaults to the public key of the signed in account. - publicKey?: PublicKey; - // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. - callbackUrl?: string; - // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. - meta?: string; + message?: string; // The message requested sign. Defaults to `verify owner` string. + callbackUrl?: string; // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. + meta?: string; // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. } -type VerifyOwnerResponse = Promise; +interface VerifiedOwner { + accountId: string; + message: string; + blockId: string; + publicKey: string; + signature: string; + keyType: utils.key_pair.KeyType; +} + +type VerifyOwnerResponse = Promise -// Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. +// NOTE: Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. ``` From af9373338d6e352739f04965b0c22cbcb62d5b7e Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 11 Nov 2022 16:22:00 +0100 Subject: [PATCH 08/40] Update nep-0413.md --- neps/nep-0413.md | 134 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 32 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index cc18cbc82..38fe11db7 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -1,8 +1,8 @@ --- NEP: 413 Title: Near Wallet API - support for verifyOwner method -Author: Philip Obosi -# DiscussionsTo: +Author: Philip Obosi , Guillermo Gallardo +# DiscussionsTo: Status: Draft Type: Standards Track Category: Wallet @@ -11,72 +11,142 @@ Created: 25-Oct-2022 ## Summary -Introduce `verifyOwner` method to Wallet API. +A standardized Wallet API method, namely `verifyOwner`, that allows users to be authenticated in third-party services using their NEAR account. ## Motivation +NEAR users want to use their accounts as means of authentication in third-party services, in a similar fashion to how mainstream Web2 accounts can be used (e.g. "Login with Facebook"). -DApp developers want to be able to verify that a user owns the account they are connecting to,without incurring gas fees by adding keys to their account. This feature would make it possible for app authors to verify account ownership without an actual on-chain transaction being submitted. +Currently, there is no standardized way for wallets to sign a message for authentication. -## Rationale and alternatives +## Rationale and Alternatives +Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This means that: -A possible solution here is to introduce a restricted access key that is only allowed to call a nonexistent function on the owner's account (this will prevent exploitation as anything signed by this key will not execute on-chain). This access key would be used for all ownership verification, eliminating the need to create access keys for every verification request. +1) The message must be signed off chain, with no transaction being involved. +2) The message being signed must include the service's name. +3) The message being signed cannot represent a valid transaction. +3) The message must be signed using a Full Access Key. +4) The signature should be simple to produce/verify, and transmitted securely. -### The problem with a restricted access key +### Why Off Chain? +So the user would not incur in GAS fees, nor the signed authentication gets broadcasted into a public network. -Originally NEAR was designed with the assumption that access keys would only be used to sign transactions. Using access keys for signing arbitrary payloads carries the risk of the signer inadvertently signing surreptitious transactions which the requester could in turn submit to the network. +### Why The Message Being Signed MUST NOT be a Transaction? +An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it. -When using the keys for authentication, another issue that needs to be handled is a replay attack: a malicious app might request the user to authenticate only to authenticate itself to some other app as the user. +### Why The Message Needs to Include a Service Identifier? +To avoid replay attacks, i.e. a malicious app requesting the user to authenticate in their service, only to use the authentication message in a third-app. -If the wallet allows apps to request the user to sign arbitrary data, both attacks would be possible. Thus, some measures are needed to prevent both attacks: +Including the service's identifier (e.g. domain name) and making sure the user knows about it should mitigate this kind of attacks. -1. To prevent the malicious signing of transactions, a good solution is to make sure that the data to be signed cannot be parsed as a transaction. Having a standard format for the verification payload(different from the transaction format) will solve this. +### Why a FullAccess Key? Why Not Simply Creating an [FunctionCall Key](https://docs.near.org/concepts/basics/accounts/access-keys) for Signing? +The most common flow for [NEAR user authentication into a Web3 frontend](https://docs.near.org/develop/integrate/frontend#user-sign-in--sign-out) involves the creation of a [FunctionCall Key](](https://docs.near.org/concepts/basics/accounts/access-keys)). -2. To prevent one app from relaying a signature to a different app, the data to be signed should include the requesting app's identity (e.g. domain name). Again, a well-defined format will facilitate this. +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 for several reasons: +1. It implies that the user needs to expend gas in creating a new key. +2. Since any third-party can ask the user to create a `FunctionCall Key`, this opens a vector of attack. -Using an access key restricted in such a way that it cannot send transactions looks like a good temporary solution. It doesn't solve the second problem, though. +Using a FullAccess key allows us to be sure that the message 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). -Sending the signature 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 of a query string will improve this (the fragment isn't included in the Referrer), and somehow permitting only a fixed callback URL will improve this even more. +### How to Return the Signed Message in a Safe Way +Sending the signature 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 the fragment isn't included in the Referrer. Further constraining the callback URL to a fixed value will improve security even more. -Also, note that NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. - -Hence, the final solution we are settling on is to user a restricted payload as shown below. +### NEAR Signatures +NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. ## Specification +Given the previous [rationales](#rationale-and-alternatives), this NEP proposes the wallets to implement a `verifyOwner` method following the specifications bellow. + -### `verifyOwner` +### Goal +The `verifyOwner` method must take a plain text `message` and transform it into a signature. -Signs the message and verifies the owner. Message is not sent to blockchain. +### Input Interface +In order to sign a message, `verifyOwner` must implement the following input interface: ```jsx interface VerifyOwnerParams { - message?: string; // The message requested sign. Defaults to `verify owner` string. + message: string; // The message requested to be signed, should contain the app name. callbackUrl?: string; // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. meta?: string; // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. } +``` + +### Structure and Signature +In order to respect all the points layed in [rationales](#rationale-and-alternatives), `verifyOwner` must: + +1. Embed the passed `message` into a predefined structure (see below). +2. Stringify the structure +2. Compute the `SHA256 hash` of the *stringified structure*. +3. Use a full-access key to sign the resulting `SHA256 hash`. -interface VerifiedOwner { - accountId: string; - message: string; - blockId: string; - publicKey: string; - signature: string; - keyType: utils.key_pair.KeyType; +#### Structure +The structure must have the following interface: + +```rust +struct Payload { + accountId: string; // Mandatory: the account name as plain text (e.g. "alice.near") + message: string; // The same message passed in `VerifyOwnerParams.message` + blockId: string; // The hash of a block created close to the time of signature + publicKey: string; // The public counterpart of the key used to sign, expressed as plain text (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y") } +``` + + +#### Signature +In order to create a signature, the `Payload` must be converted into its `string` representation (respecting the attribute's order given above), and then hashed using `SHA256`. + +For example, assuming that: +1. `alice.near` wants to sign the message `"berryclub.io"`. +2. The wallet stores a key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y`. +3. A recent block was produced with the block hash `12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M`. -type VerifyOwnerResponse = Promise +The wallet should sign the **`SHA256` hash** of the following string: -// NOTE: Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. +```jsx +`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}` +``` + +### Output Interface +`verifyOwner` must return a object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. +```jsx +interface AuthenticationToken { + accountId: string; // The account name as plain text (e.g. "alice.near") + message: string; // The same message passed in `VerifyOwnerParams.message` + blockId: string; // The hash of a block created close to the time of signature + publicKey: string; // The public counterpart of the key used to sign, expressed as a base64 string (e.g. base64("ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y")) + signature: string; // The base64 representation of the signature. +} ``` +### Returning the signature +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing each attribute as a URL fragment. For example: `?accountId=&message=...`. + +Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object. + +### Verifying the Signature +To authenticate a user via the `AuthenticationToken` there are 5 steps that you need to perform: +1. Make sure the `AuthenticationToken.message` is exactly the one you asked the user to sign. +2. Verify that the `AuthenticationToken.publicKey` belongs to the `AuthenticationToken.accountId`. +3. Reconstruct the `Payload` locally +4. Use the `AuthenticationToken.publicKey` to verify that the reconstructed `Payload` matches the `AuthenticationToken.signature`. +5. (Optionally) Make sure that the signature is recent by using the `timestamp` of the block with hash `AuthenticationToken.blockId`. + +> See the [reference section](#references---verification) for an example code on how to verify a user. + ## References + +### Signature This interface is now supported by Wallet Selector as shown below: -> [near/wallet-selector#391](https://github.com/near/wallet-selector/pull/391) +1. [Pull Request Implementing `verifyOwner` in near/wallet-selector#391](https://github.com/near/wallet-selector/pull/391). +2. [https://github.com/near/wallet-selector/pull/436/files](https://github.com/near/wallet-selector/pull/436/files). -More developers within the ecosystem have also begun to add support for this to their Wallets. +### Verification +We also include an example on [how to verify the authentication signature](https://github.com/gagdiez/near-login/blob/main/server/authenticate/wallet-authenticate.js). -- [https://github.com/near/wallet-selector/pull/436/files](https://github.com/near/wallet-selector/pull/436/files) +## Drawbacks +Accounts that do not hold a FullAccess Key will not be able to sign authentication messages, however, this is a necessary tradeoff for security. ## Copyright [copyright]: #copyright From eff4cc93731cc57c0f1c54b748aa87c9ca5e2abf Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 11 Nov 2022 16:38:15 +0100 Subject: [PATCH 09/40] Better explained base64 parameters --- neps/nep-0413.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 38fe11db7..1bb7aec11 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -87,23 +87,22 @@ struct Payload { accountId: string; // Mandatory: the account name as plain text (e.g. "alice.near") message: string; // The same message passed in `VerifyOwnerParams.message` blockId: string; // The hash of a block created close to the time of signature - publicKey: string; // The public counterpart of the key used to sign, expressed as plain text (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y") + publicKey: string; // public counterpart of the key used to sign, encoded as a base64 string (see bellow) } ``` - #### Signature In order to create a signature, the `Payload` must be converted into its `string` representation (respecting the attribute's order given above), and then hashed using `SHA256`. For example, assuming that: 1. `alice.near` wants to sign the message `"berryclub.io"`. -2. The wallet stores a key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y`. +2. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y`, which in base64 is: `ZWQyNTUxOTo2VHVweU5yY0hHVHQ1WFJMbUhUYzJLR2FpU2JqaFFpMUtIdENYVGdiY3I0WQ==` 3. A recent block was produced with the block hash `12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M`. The wallet should sign the **`SHA256` hash** of the following string: ```jsx -`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}` +sha256.hash(`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ZWQyNTUxOTo2VHVweU5yY0hHVHQ1WFJMbUhUYzJLR2FpU2JqaFFpMUtIdENYVGdiY3I0WQ=="}`) ``` ### Output Interface @@ -114,7 +113,7 @@ interface AuthenticationToken { accountId: string; // The account name as plain text (e.g. "alice.near") message: string; // The same message passed in `VerifyOwnerParams.message` blockId: string; // The hash of a block created close to the time of signature - publicKey: string; // The public counterpart of the key used to sign, expressed as a base64 string (e.g. base64("ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y")) + publicKey: string; // The public counterpart of the key used to sign, expressed as a base64 string (see previous section) signature: string; // The base64 representation of the signature. } ``` From c2512a47f243ef438ff9e56a5e1db9f140a48f83 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Mon, 14 Nov 2022 17:34:40 +0100 Subject: [PATCH 10/40] Public key format is now ":" --- neps/nep-0413.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 1bb7aec11..46cfe9be9 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -87,7 +87,7 @@ struct Payload { accountId: string; // Mandatory: the account name as plain text (e.g. "alice.near") message: string; // The same message passed in `VerifyOwnerParams.message` blockId: string; // The hash of a block created close to the time of signature - publicKey: string; // public counterpart of the key used to sign, encoded as a base64 string (see bellow) + publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` @@ -96,13 +96,13 @@ In order to create a signature, the `Payload` must be converted into its `string For example, assuming that: 1. `alice.near` wants to sign the message `"berryclub.io"`. -2. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y`, which in base64 is: `ZWQyNTUxOTo2VHVweU5yY0hHVHQ1WFJMbUhUYzJLR2FpU2JqaFFpMUtIdENYVGdiY3I0WQ==` +2. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` 3. A recent block was produced with the block hash `12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M`. The wallet should sign the **`SHA256` hash** of the following string: ```jsx -sha256.hash(`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ZWQyNTUxOTo2VHVweU5yY0hHVHQ1WFJMbUhUYzJLR2FpU2JqaFFpMUtIdENYVGdiY3I0WQ=="}`) +sha256.hash(`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) ``` ### Output Interface @@ -113,7 +113,7 @@ interface AuthenticationToken { accountId: string; // The account name as plain text (e.g. "alice.near") message: string; // The same message passed in `VerifyOwnerParams.message` blockId: string; // The hash of a block created close to the time of signature - publicKey: string; // The public counterpart of the key used to sign, expressed as a base64 string (see previous section) + publicKey: string; // The public counterpart of the key used to sign, expressed as a string with format ":" signature: string; // The base64 representation of the signature. } ``` From 9327ae8af0d1709f8e67ed32d0fcc8eb671ac3a4 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Tue, 15 Nov 2022 12:10:47 +0100 Subject: [PATCH 11/40] Update nep-0413.md --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 46cfe9be9..ea80f45f5 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -119,7 +119,7 @@ interface AuthenticationToken { ``` ### Returning the signature -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing each attribute as a URL fragment. For example: `?accountId=&message=...`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing `accountId`, `message`, `blockId`, `publicKey` as strings, and the signature as an URL fragment. This is: `?accountId=&message=...#signature=`. Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object. From 34fed8cf63534ea3ee2d961e319dc6135705baa5 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Tue, 15 Nov 2022 22:33:24 +0100 Subject: [PATCH 12/40] Tested references & minor change to return types --- neps/nep-0413.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index ea80f45f5..f494c0fb9 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -121,7 +121,9 @@ interface AuthenticationToken { ### Returning the signature Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing `accountId`, `message`, `blockId`, `publicKey` as strings, and the signature as an URL fragment. This is: `?accountId=&message=...#signature=`. -Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object. +If the user cancels the signature, or the signature fails the wallet must return an error message. For Web Wallets this can be done by returning an error string parameter: `?error=`. + +Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object, and raise an error on failure. ### Verifying the Signature To authenticate a user via the `AuthenticationToken` there are 5 steps that you need to perform: @@ -136,13 +138,10 @@ To authenticate a user via the `AuthenticationToken` there are 5 steps that you ## References ### Signature -This interface is now supported by Wallet Selector as shown below: - -1. [Pull Request Implementing `verifyOwner` in near/wallet-selector#391](https://github.com/near/wallet-selector/pull/391). -2. [https://github.com/near/wallet-selector/pull/436/files](https://github.com/near/wallet-selector/pull/436/files). +A full example on how to implement the `verifyOwner` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). ### Verification -We also include an example on [how to verify the authentication signature](https://github.com/gagdiez/near-login/blob/main/server/authenticate/wallet-authenticate.js). +An example on how to verify the authentication signature can be [found here](https://github.com/gagdiez/near-login/blob/main/server/authenticate/wallet-authenticate.js). ## Drawbacks Accounts that do not hold a FullAccess Key will not be able to sign authentication messages, however, this is a necessary tradeoff for security. From ab4b493a117953802ddd406ca019b9d35d356d14 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Wed, 16 Nov 2022 18:56:48 +0100 Subject: [PATCH 13/40] Simplified NEP + Added `domain` input 1. Removed how to verify the signature, so the NEP remains focused on "specifying the input/output of the wallet method". 2. Separated `message` from `domain` in the wallet's input. --- neps/nep-0413.md | 57 +++++++++++++++++------------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index f494c0fb9..fa17ee8ec 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -19,7 +19,7 @@ NEAR users want to use their accounts as means of authentication in third-party Currently, there is no standardized way for wallets to sign a message for authentication. ## Rationale and Alternatives -Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This means that: +Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This means that: 1) The message must be signed off chain, with no transaction being involved. 2) The message being signed must include the service's name. @@ -48,7 +48,7 @@ One might feel tempted to reproduce such process here, for example, by creating Using a FullAccess key allows us to be sure that the message 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). ### How to Return the Signed Message in a Safe Way -Sending the signature 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 the fragment isn't included in the Referrer. Further constraining the callback URL to a fixed value will improve security even more. +Sending the signature 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 the fragment isn't included in the Referrer. ### NEAR Signatures NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. @@ -56,26 +56,25 @@ NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatu ## Specification Given the previous [rationales](#rationale-and-alternatives), this NEP proposes the wallets to implement a `verifyOwner` method following the specifications bellow. - ### Goal -The `verifyOwner` method must take a plain text `message` and transform it into a signature. +The `verifyOwner` method must take as input a `domain` and a `message`, and transform them into a signature. ### Input Interface In order to sign a message, `verifyOwner` must implement the following input interface: ```jsx interface VerifyOwnerParams { - message: string; // The message requested to be signed, should contain the app name. - callbackUrl?: string; // Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. - meta?: string; // Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. + domain: string; // The app in which the user wants to authenticate, e.g. myapp.com. + message: string; // A message to be included in the signature + callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved/cancelled. Defaults to `window.location.href`. } ``` ### Structure and Signature In order to respect all the points layed in [rationales](#rationale-and-alternatives), `verifyOwner` must: -1. Embed the passed `message` into a predefined structure (see below). -2. Stringify the structure +1. Embed the passed `domain` and `message` into a predefined structure (see below). +2. Stringify the structure, respecting the attribute's order (see bellow). 2. Compute the `SHA256 hash` of the *stringified structure*. 3. Use a full-access key to sign the resulting `SHA256 hash`. @@ -84,65 +83,49 @@ The structure must have the following interface: ```rust struct Payload { - accountId: string; // Mandatory: the account name as plain text (e.g. "alice.near") + accountId: string; // The account name as plain text (e.g. "alice.near") + domain: string; // The same domain passed in `VerifyOwnerParams.domain` message: string; // The same message passed in `VerifyOwnerParams.message` - blockId: string; // The hash of a block created close to the time of signature publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` #### Signature -In order to create a signature, the `Payload` must be converted into its `string` representation (respecting the attribute's order given above), and then hashed using `SHA256`. +In order to create a signature, the `Payload` must first be converted into its `string` representation (respecting the attribute's order given above), and then hashed using `SHA256`. The resulting `SHA256` hash is the one to be signed. For example, assuming that: -1. `alice.near` wants to sign the message `"berryclub.io"`. -2. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` -3. A recent block was produced with the block hash `12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M`. +1. `alice.near` wants to login to the domain `"berryclub.io"` by signing the message `"a message"` +3. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` -The wallet should sign the **`SHA256` hash** of the following string: +The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`{"accountId":"alice.near","message":"berryclub.io","blockId":"12FaoFyjYGmuxXnvWGYqxvUqGesSjdFVBNWXkXvoj41M","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) +sha256.hash(`{"accountId":"alice.near","domain":"berryclub.io","message":"a message","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) ``` ### Output Interface -`verifyOwner` must return a object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. +`verifyOwner` must return an object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. ```jsx interface AuthenticationToken { accountId: string; // The account name as plain text (e.g. "alice.near") - message: string; // The same message passed in `VerifyOwnerParams.message` - blockId: string; // The hash of a block created close to the time of signature publicKey: string; // The public counterpart of the key used to sign, expressed as a string with format ":" signature: string; // The base64 representation of the signature. } ``` ### Returning the signature -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing `accountId`, `message`, `blockId`, `publicKey` as strings, and the signature as an URL fragment. This is: `?accountId=&message=...#signature=`. +#### Web Wallets +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing `accountId` and `publicKey` as strings, while the `signature` should be returned as an URL fragment. This is: `?accountId=&publicKey=#signature=`. -If the user cancels the signature, or the signature fails the wallet must return an error message. For Web Wallets this can be done by returning an error string parameter: `?error=`. +If the user cancels the signature, or the signature fails, the wallet must return an error message. For Web Wallets this can be done by returning an error string parameter: `?error=`. +#### Other Wallets Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object, and raise an error on failure. -### Verifying the Signature -To authenticate a user via the `AuthenticationToken` there are 5 steps that you need to perform: -1. Make sure the `AuthenticationToken.message` is exactly the one you asked the user to sign. -2. Verify that the `AuthenticationToken.publicKey` belongs to the `AuthenticationToken.accountId`. -3. Reconstruct the `Payload` locally -4. Use the `AuthenticationToken.publicKey` to verify that the reconstructed `Payload` matches the `AuthenticationToken.signature`. -5. (Optionally) Make sure that the signature is recent by using the `timestamp` of the block with hash `AuthenticationToken.blockId`. - -> See the [reference section](#references---verification) for an example code on how to verify a user. - ## References - -### Signature A full example on how to implement the `verifyOwner` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). -### Verification -An example on how to verify the authentication signature can be [found here](https://github.com/gagdiez/near-login/blob/main/server/authenticate/wallet-authenticate.js). - ## Drawbacks Accounts that do not hold a FullAccess Key will not be able to sign authentication messages, however, this is a necessary tradeoff for security. From cba446e75795429757dc71876e9fd76a9f1d336b Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 18 Nov 2022 15:26:02 +0100 Subject: [PATCH 14/40] Added hash domain Co-authored-by: DavidM-D --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index fa17ee8ec..bc31d3c1e 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -100,7 +100,7 @@ For example, assuming that: The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`{"accountId":"alice.near","domain":"berryclub.io","message":"a message","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) +sha256.hash('NEAR_verifyOwner' + `{"accountId":"alice.near","domain":"berryclub.io","message":"a message","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) ``` ### Output Interface From 23eeda800ed04ee1e446de5b586157aaf510310f Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 18 Nov 2022 15:41:51 +0100 Subject: [PATCH 15/40] Removed accountId from Payload Co-authored-by: DavidM-D --- neps/nep-0413.md | 1 - 1 file changed, 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index bc31d3c1e..19d12b428 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -83,7 +83,6 @@ The structure must have the following interface: ```rust struct Payload { - accountId: string; // The account name as plain text (e.g. "alice.near") domain: string; // The same domain passed in `VerifyOwnerParams.domain` message: string; // The same message passed in `VerifyOwnerParams.message` publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" From 343fe160d21ef45ba59d760a5835d81972724b60 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 18 Nov 2022 15:43:16 +0100 Subject: [PATCH 16/40] Changed message for nonce Co-authored-by: DavidM-D --- neps/nep-0413.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 19d12b428..18a02f7c6 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -65,7 +65,7 @@ In order to sign a message, `verifyOwner` must implement the following input int ```jsx interface VerifyOwnerParams { domain: string; // The app in which the user wants to authenticate, e.g. myapp.com. - message: string; // A message to be included in the signature + nonce: [u8; 256] ; // A nonce to be included in the signature callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved/cancelled. Defaults to `window.location.href`. } ``` @@ -84,7 +84,7 @@ The structure must have the following interface: ```rust struct Payload { domain: string; // The same domain passed in `VerifyOwnerParams.domain` - message: string; // The same message passed in `VerifyOwnerParams.message` + nonce: [u8; 256]; // The same nonce passed in `VerifyOwnerParams.nonce` publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` From dbaeeceb3fcdee9b6a92b1d6d9db273e0bbaf141 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 18 Nov 2022 16:02:25 +0100 Subject: [PATCH 17/40] Reverted nonce change to bring back message --- neps/nep-0413.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 18a02f7c6..7349a7e59 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -65,7 +65,7 @@ In order to sign a message, `verifyOwner` must implement the following input int ```jsx interface VerifyOwnerParams { domain: string; // The app in which the user wants to authenticate, e.g. myapp.com. - nonce: [u8; 256] ; // A nonce to be included in the signature + message: string ; // A message to be included in the signature callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved/cancelled. Defaults to `window.location.href`. } ``` @@ -84,7 +84,7 @@ The structure must have the following interface: ```rust struct Payload { domain: string; // The same domain passed in `VerifyOwnerParams.domain` - nonce: [u8; 256]; // The same nonce passed in `VerifyOwnerParams.nonce` + message: string; // The same message passed in `VerifyOwnerParams.message` publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` From b8d698f2adfbced8b71fb5f7173aa56382eb61cc Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 18 Nov 2022 20:17:46 +0100 Subject: [PATCH 18/40] Changed message for a domain+nonce challenge --- neps/nep-0413.md | 95 +++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 7349a7e59..f4db88729 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -16,90 +16,87 @@ A standardized Wallet API method, namely `verifyOwner`, that allows users to be ## Motivation NEAR users want to use their accounts as means of authentication in third-party services, in a similar fashion to how mainstream Web2 accounts can be used (e.g. "Login with Facebook"). -Currently, there is no standardized way for wallets to sign a message for authentication. +Currently, there is no standardized way for wallets to create an authentication token. ## Rationale and Alternatives -Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This means that: +Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This can be done by signing a challenge, which: -1) The message must be signed off chain, with no transaction being involved. -2) The message being signed must include the service's name. -3) The message being signed cannot represent a valid transaction. -3) The message must be signed using a Full Access Key. -4) The signature should be simple to produce/verify, and transmitted securely. +1) Must be signed off-chain, with no transaction being involved. +2) Must include the service's name. +3) Cannot represent a valid transaction. +3) Must be signed using a Full Access Key. +4) Should be simple to produce/verify, and transmitted securely. -### Why Off Chain? -So the user would not incur in GAS fees, nor the signed authentication gets broadcasted into a public network. +### Why Off-Chain? +So the user would not incur in GAS fees, nor the authentication token gets broadcasted into a public network. -### Why The Message Being Signed MUST NOT be a Transaction? +### Why The Challenge MUST NOT be a Transaction? An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it. -### Why The Message Needs to Include a Service Identifier? -To avoid replay attacks, i.e. a malicious app requesting the user to authenticate in their service, only to use the authentication message in a third-app. +### Why The Challenge Needs to Include a Service Identifier? +To avoid replay attacks, i.e. a malicious app requesting the user to authenticate in their service, only to use the authentication token in a third-app. Including the service's identifier (e.g. domain name) and making sure the user knows about it should mitigate this kind of attacks. -### Why a FullAccess Key? Why Not Simply Creating an [FunctionCall Key](https://docs.near.org/concepts/basics/accounts/access-keys) for Signing? +### Why using a FullAccess Key? Why Not Simply Creating an [FunctionCall Key](https://docs.near.org/concepts/basics/accounts/access-keys) for Signing? The most common flow for [NEAR user authentication into a Web3 frontend](https://docs.near.org/develop/integrate/frontend#user-sign-in--sign-out) involves the creation of a [FunctionCall Key](](https://docs.near.org/concepts/basics/accounts/access-keys)). -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 for several reasons: -1. It implies that the user needs to expend gas in creating a new key. -2. Since any third-party can ask the user to create a `FunctionCall Key`, this opens a vector of attack. +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: +1. The user would need to expend gas in creating a new key. +2. 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 message 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). +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). -### How to Return the Signed Message in a Safe Way -Sending the signature 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 the fragment isn't included in the Referrer. +### How to Return the Auth Token in a Safe Way +Sending the authentication token 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`](https://greenbytes.de/tech/webdav/rfc2616.html#header.referer). ### NEAR Signatures NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. ## Specification -Given the previous [rationales](#rationale-and-alternatives), this NEP proposes the wallets to implement a `verifyOwner` method following the specifications bellow. - -### Goal -The `verifyOwner` method must take as input a `domain` and a `message`, and transform them into a signature. +Wallets must implement a `verifyOwner` method, which takes a challenge in the form of a `domain` and a `nonce`, and transform it into an authentication token. ### Input Interface -In order to sign a message, `verifyOwner` must implement the following input interface: +`verifyOwner` must implement the following input interface: ```jsx interface VerifyOwnerParams { - domain: string; // The app in which the user wants to authenticate, e.g. myapp.com. - message: string ; // A message to be included in the signature - callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved/cancelled. Defaults to `window.location.href`. + domain: string; // The service in which the user wants to authenticate, e.g. myapp.com. + nonce: [u8; 32] ; // A nonce, representing a challenge from the service + callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`. } ``` -### Structure and Signature -In order to respect all the points layed in [rationales](#rationale-and-alternatives), `verifyOwner` must: - -1. Embed the passed `domain` and `message` into a predefined structure (see below). -2. Stringify the structure, respecting the attribute's order (see bellow). -2. Compute the `SHA256 hash` of the *stringified structure*. -3. Use a full-access key to sign the resulting `SHA256 hash`. - -#### Structure -The structure must have the following interface: +### Structure +`verifyOwner` must embed the input `domain` and `nonce` into the following predefined structure: ```rust struct Payload { - domain: string; // The same domain passed in `VerifyOwnerParams.domain` - message: string; // The same message passed in `VerifyOwnerParams.message` + domain: string; // The same domain passed in `VerifyOwnerParams.domain` + nonce: [u8; 32]; // The same nonce passed in `VerifyOwnerParams.nonce` publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` -#### Signature -In order to create a signature, the `Payload` must first be converted into its `string` representation (respecting the attribute's order given above), and then hashed using `SHA256`. The resulting `SHA256` hash is the one to be signed. +### Signature +In order to create a signature, `verifyOwner` must: +1. Convert the `Payload` into its [JSON JCS](https://www.rfc-editor.org/rfc/rfc8785) string representation (i.e. a JSON string, with alphabetically ordered attributes). +2. Prepend the `NEAR_verifyOwner` string to the result from step 1. +3. Compute the `SHA256` hash of the result from step 2. +4. 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. -For example, assuming that: -1. `alice.near` wants to login to the domain `"berryclub.io"` by signing the message `"a message"` -3. The wallet stores a private key which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` +### Example +Assuming that the `verifyOwner` method was invoked, and that: +- The input `domain` is `"berryclub.io"` +- The input `nonce` is `[0,1,...,31]` +- The wallet stores a full-access private key, which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash('NEAR_verifyOwner' + `{"accountId":"alice.near","domain":"berryclub.io","message":"a message","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) +sha256.hash(`NEAR_verifyOwner` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) ``` ### Output Interface @@ -107,7 +104,7 @@ sha256.hash('NEAR_verifyOwner' + `{"accountId":"alice.near","domain":"berryclub. ```jsx interface AuthenticationToken { - accountId: string; // The account name as plain text (e.g. "alice.near") + 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 ":" signature: string; // The base64 representation of the signature. } @@ -115,9 +112,9 @@ interface AuthenticationToken { ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing `accountId` and `publicKey` as strings, while the `signature` should be returned as an URL fragment. This is: `?accountId=&publicKey=#signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. -If the user cancels the signature, or the signature fails, the wallet must return an error message. For Web Wallets this can be done by returning an error string parameter: `?error=`. +If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. #### Other Wallets Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object, and raise an error on failure. @@ -126,7 +123,7 @@ Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return t A full example on how to implement the `verifyOwner` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). ## Drawbacks -Accounts that do not hold a FullAccess Key will not be able to sign authentication messages, however, this is a necessary tradeoff for security. +Accounts that do not hold a FullAccess Key will not be able to sign authentication challenges. This is a necessary tradeoff for security since any third-party can ask the user to create a FunctionAccess key. ## Copyright [copyright]: #copyright From 0084811af228a437133c795cff4781d9d23dff87 Mon Sep 17 00:00:00 2001 From: DavidM-D Date: Sun, 20 Nov 2022 19:34:35 +0100 Subject: [PATCH 19/40] Added colons to the hash namespace --- neps/nep-0413.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index f4db88729..146093da3 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -81,7 +81,7 @@ struct Payload { ### Signature In order to create a signature, `verifyOwner` must: 1. Convert the `Payload` into its [JSON JCS](https://www.rfc-editor.org/rfc/rfc8785) string representation (i.e. a JSON string, with alphabetically ordered attributes). -2. Prepend the `NEAR_verifyOwner` string to the result from step 1. +2. Prepend the `NEAR_verifyOwner:` string to the result from step 1. 3. Compute the `SHA256` hash of the result from step 2. 4. Sign the resulting `SHA256` hash from step 3 using a **full-access** key. @@ -96,7 +96,7 @@ Assuming that the `verifyOwner` method was invoked, and that: The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEAR_verifyOwner` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) +sha256.hash(`NEAR_verifyOwner:` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) ``` ### Output Interface From 129b452bfa55c3514b7508d1dc1579a757b33a69 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Mon, 21 Nov 2022 15:54:33 -0300 Subject: [PATCH 20/40] Update nep-0413.md --- neps/nep-0413.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 146093da3..61f3f3a49 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -74,14 +74,13 @@ interface VerifyOwnerParams { struct Payload { domain: string; // The same domain passed in `VerifyOwnerParams.domain` nonce: [u8; 32]; // The same nonce passed in `VerifyOwnerParams.nonce` - publicKey: string; // public counterpart of the key used to sign, encoded as a string with format ":" } ``` ### Signature In order to create a signature, `verifyOwner` must: 1. Convert the `Payload` into its [JSON JCS](https://www.rfc-editor.org/rfc/rfc8785) string representation (i.e. a JSON string, with alphabetically ordered attributes). -2. Prepend the `NEAR_verifyOwner:` string to the result from step 1. +2. Prepend the `NEP0413:` string to the result from step 1. 3. Compute the `SHA256` hash of the result from step 2. 4. Sign the resulting `SHA256` hash from step 3 using a **full-access** key. @@ -91,12 +90,12 @@ In order to create a signature, `verifyOwner` must: Assuming that the `verifyOwner` method was invoked, and that: - The input `domain` is `"berryclub.io"` - The input `nonce` is `[0,1,...,31]` -- The wallet stores a full-access private key, which public counterpart is: `ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y` +- The wallet stores a full-access private key The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEAR_verifyOwner:` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]","publicKey":"ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y"}`) +sha256.hash(`NEP0413:` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]"}`) ``` ### Output Interface From 36b02bc2a25632047acad3609e688fc92bd71263 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Tue, 22 Nov 2022 17:30:46 -0300 Subject: [PATCH 21/40] Update nep-0413.md --- neps/nep-0413.md | 52 ++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 61f3f3a49..47f688e30 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -11,32 +11,37 @@ Created: 25-Oct-2022 ## Summary -A standardized Wallet API method, namely `verifyOwner`, that allows users to be authenticated in third-party services using their NEAR account. +A standardized Wallet API method, namely `verifyOwner`, that allows users to that allows users to be authenticated in third-party services using their NEAR account. ## Motivation NEAR users want to use their accounts as means of authentication in third-party services, in a similar fashion to how mainstream Web2 accounts can be used (e.g. "Login with Facebook"). -Currently, there is no standardized way for wallets to create an authentication token. +Currently, there is no standardized way for wallets to create an authentication token. ## Rationale and Alternatives -Users want to be authenticated into a service without incurring in GAS fees, nor compromising their account's security. This can be done by signing a challenge, which: +Users want to sign messages for a specific domain without incurring in GAS fees, nor compromising their account's security. This means that the message being signed: -1) Must be signed off-chain, with no transaction being involved. -2) Must include the service's name. +1) Must be signed off-chain, with no transactions being involved. +2) Must include the domain's name and a nonce. 3) Cannot represent a valid transaction. 3) Must be signed using a Full Access Key. 4) Should be simple to produce/verify, and transmitted securely. ### Why Off-Chain? -So the user would not incur in GAS fees, nor the authentication token gets broadcasted into a public network. +So the user would not incur in GAS fees, nor the signed message gets broadcasted into a public network. -### Why The Challenge MUST NOT be a Transaction? +### Why The Message MUST NOT be a Transaction? How To Ensure This? An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it. -### Why The Challenge Needs to Include a Service Identifier? -To avoid replay attacks, i.e. a malicious app requesting the user to authenticate in their service, only to use the authentication token in a third-app. +#### How to Ensure the Message is not a Transaction +In NEAR, transactions are encoded in Borsh before being signed. The first attribute of a Transaction is `signerId: string`, which is encoded as a steam of 32 bytes with the string's length, followed by the N bytes of the string itself. -Including the service's identifier (e.g. domain name) and making sure the user knows about it should mitigate this kind of attacks. +By prepending the string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data), which even if present would represent an invalid account, since accounts have less than 64 chars. + +### Why The Message Needs to Include a Service Identifier and Nonce? +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 domain and making sure the user knows about it should mitigate this 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](https://docs.near.org/concepts/basics/accounts/access-keys) for Signing? The most common flow for [NEAR user authentication into a Web3 frontend](https://docs.near.org/develop/integrate/frontend#user-sign-in--sign-out) involves the creation of a [FunctionCall Key](](https://docs.near.org/concepts/basics/accounts/access-keys)). @@ -47,31 +52,33 @@ One might feel tempted to reproduce such process here, for example, by creating 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). -### How to Return the Auth Token in a Safe Way -Sending the authentication token 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`](https://greenbytes.de/tech/webdav/rfc2616.html#header.referer). +### How to Return the Signed Message in a Safe Way +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`](https://greenbytes.de/tech/webdav/rfc2616.html#header.referer). ### NEAR Signatures NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. ## Specification -Wallets must implement a `verifyOwner` method, which takes a challenge in the form of a `domain` and a `nonce`, and transform it into an authentication token. +Wallets must implement a `verifyOwner` method, which takes a `message` destined to a specific `domain` and transform it into a signed message. ### Input Interface `verifyOwner` must implement the following input interface: ```jsx interface VerifyOwnerParams { - domain: string; // The service in which the user wants to authenticate, e.g. myapp.com. - nonce: [u8; 32] ; // A nonce, representing a challenge from the service + message: string ; // The message that wants to be transmitted. + domain: string; // The domain 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 callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`. } ``` ### Structure -`verifyOwner` must embed the input `domain` and `nonce` into the following predefined structure: +`verifyOwner` must embed the input `message`, `domain` and `nonce` into the following predefined structure: ```rust struct Payload { + message: string; // The same message passed in `VerifyOwnerParams.message` domain: string; // The same domain passed in `VerifyOwnerParams.domain` nonce: [u8; 32]; // The same nonce passed in `VerifyOwnerParams.nonce` } @@ -88,21 +95,22 @@ In order to create a signature, `verifyOwner` must: ### Example Assuming that the `verifyOwner` method was invoked, and that: -- The input `domain` is `"berryclub.io"` -- The input `nonce` is `[0,1,...,31]` +- The input `message` is `"hi"` +- The input `domain` is `"myapp.com"` +- The input `nonce` is `[0,...,31]` - The wallet stores a full-access private key The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEP0413:` + `{"domain":"berryclub.io","nonce":"[0,1,...,31]"}`) +sha256.hash(`NEP0413:` + `{"message":"hi","domain":"myapp.com","nonce":"[0,...,31]"}`) ``` ### Output Interface `verifyOwner` must return an object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. ```jsx -interface AuthenticationToken { +interface VerifyOwnerOut { 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 ":" signature: string; // The base64 representation of the signature. @@ -116,13 +124,13 @@ Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly r If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. #### Other Wallets -Non-web Wallets, such as [Ledger](https://www.ledger.com), can directly return the `AuthenticationToken`, in preference as a JSON object, and raise an error on failure. +Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `VerifyOwnerOut` (in preference as a JSON object) and raise an error on failure. ## References A full example on how to implement the `verifyOwner` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). ## Drawbacks -Accounts that do not hold a FullAccess Key will not be able to sign authentication challenges. This is a necessary tradeoff for security since any third-party can ask the user to create a FunctionAccess key. +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. ## Copyright [copyright]: #copyright From 5b9ddc02e235a3c71d6f983c539cc7e5f58d28cd Mon Sep 17 00:00:00 2001 From: gagdiez Date: Tue, 29 Nov 2022 18:42:33 -0300 Subject: [PATCH 22/40] Renamed to signMessage --- neps/nep-0413.md | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 47f688e30..5832e6f79 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -1,6 +1,6 @@ --- NEP: 413 -Title: Near Wallet API - support for verifyOwner method +Title: Near Wallet API - support for signMessage method Author: Philip Obosi , Guillermo Gallardo # DiscussionsTo: Status: Draft @@ -11,18 +11,18 @@ Created: 25-Oct-2022 ## Summary -A standardized Wallet API method, namely `verifyOwner`, that allows users to that allows users to be authenticated in third-party services using their NEAR account. +A standardized Wallet API method, namely `signMessage`, that allows users to sign a message for a specific receiver using their NEAR account. ## Motivation -NEAR users want to use their accounts as means of authentication in third-party services, in a similar fashion to how mainstream Web2 accounts can be used (e.g. "Login with Facebook"). +NEAR users want to create messages destined to a specific receiver 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 create an authentication token. +Currently, there is no standardized way for wallets to sign a message destined to a specific receiver. ## Rationale and Alternatives -Users want to sign messages for a specific domain without incurring in GAS fees, nor compromising their account's security. This means that the message being signed: +Users want to sign messages for a specific receiver without incurring in GAS fees, nor compromising their account's security. This means that the message being signed: 1) Must be signed off-chain, with no transactions being involved. -2) Must include the domain's name and a nonce. +2) Must include the receiver's name and a nonce. 3) Cannot represent a valid transaction. 3) Must be signed using a Full Access Key. 4) Should be simple to produce/verify, and transmitted securely. @@ -34,12 +34,12 @@ So the user would not incur in GAS fees, nor the signed message gets broadcasted An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it. #### How to Ensure the Message is not a Transaction -In NEAR, transactions are encoded in Borsh before being signed. The first attribute of a Transaction is `signerId: string`, which is encoded as a steam of 32 bytes with the string's length, followed by the N bytes of the string itself. +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) 32 bytes representing the string's length, (2) N bytes representing the string itself. -By prepending the string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data), which even if present would represent an invalid account, since accounts have less than 64 chars. +By prepending the string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data) which, even if present, would represent an invalid account (accounts have less than 64 chars). ### Why The Message Needs to Include a Service Identifier and Nonce? -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 domain and making sure the user knows about it should mitigate this kind of attacks. +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 receiver and making sure the user knows about it should mitigate this kind of attacks. Meanwhile, including a nonce helps to mitigate replay attacks, in which an attacker can delay or re-send a signed message. @@ -59,33 +59,33 @@ Sending the signed message in a query string to an arbitrary URL (even within th NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. ## Specification -Wallets must implement a `verifyOwner` method, which takes a `message` destined to a specific `domain` and transform it into a signed message. +Wallets must implement a `signMessage` method, which takes a `message` destined to a specific `receiver` and transform it into a verifiable signature. ### Input Interface -`verifyOwner` must implement the following input interface: +`signMessage` must implement the following input interface: ```jsx -interface VerifyOwnerParams { +interface SignMessageParams { message: string ; // The message that wants to be transmitted. - domain: string; // The domain to whom the message is destined (e.g. "alice.near" or "myapp.com"). + receiver: string; // The receiver 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 callbackUrl?: string; // Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`. } ``` ### Structure -`verifyOwner` must embed the input `message`, `domain` and `nonce` into the following predefined structure: +`signMessage` must embed the input `message`, `receiver` and `nonce` into the following predefined structure: ```rust struct Payload { - message: string; // The same message passed in `VerifyOwnerParams.message` - domain: string; // The same domain passed in `VerifyOwnerParams.domain` - nonce: [u8; 32]; // The same nonce passed in `VerifyOwnerParams.nonce` + message: string; // The same message passed in `SignMessageParams.message` + receiver: string; // The same receiver passed in `SignMessageParams.receiver` + nonce: [u8; 32]; // The same nonce passed in `SignMessageParams.nonce` } ``` ### Signature -In order to create a signature, `verifyOwner` must: +In order to create a signature, `signMessage` must: 1. Convert the `Payload` into its [JSON JCS](https://www.rfc-editor.org/rfc/rfc8785) string representation (i.e. a JSON string, with alphabetically ordered attributes). 2. Prepend the `NEP0413:` string to the result from step 1. 3. Compute the `SHA256` hash of the result from step 2. @@ -94,23 +94,23 @@ In order to create a signature, `verifyOwner` must: > If the wallet does not hold any `full-access` keys, then it must return an error. ### Example -Assuming that the `verifyOwner` method was invoked, and that: +Assuming that the `signMessage` method was invoked, and that: - The input `message` is `"hi"` -- The input `domain` is `"myapp.com"` +- The input `receiver` is `"myapp.com"` - The input `nonce` is `[0,...,31]` - The wallet stores a full-access private key The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEP0413:` + `{"message":"hi","domain":"myapp.com","nonce":"[0,...,31]"}`) +sha256.hash(`NEP0413:` + `{"message":"hi","receiver":"myapp.com","nonce":"[0,...,31]"}`) ``` ### Output Interface -`verifyOwner` must return an object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. +`signMessage` must return an object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. ```jsx -interface VerifyOwnerOut { +interface SignMessageOut { 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 ":" signature: string; // The base64 representation of the signature. @@ -119,15 +119,15 @@ interface VerifyOwnerOut { ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `VerifyOwnerParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. #### Other Wallets -Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `VerifyOwnerOut` (in preference as a JSON object) and raise an error on failure. +Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignMessageOut` (in preference as a JSON object) and raise an error on failure. ## References -A full example on how to implement the `verifyOwner` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). +A full example on how to implement the `signMessage` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). ## Drawbacks 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. From 111539981ea60cb36a91eebea7e4df0d0e890642 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Tue, 29 Nov 2022 18:47:10 -0300 Subject: [PATCH 23/40] Renamed output structure --- neps/nep-0413.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 5832e6f79..77ebb748b 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -110,7 +110,7 @@ sha256.hash(`NEP0413:` + `{"message":"hi","receiver":"myapp.com","nonce":"[0,... `signMessage` must return an object containing the **base64** representation of the `signature`, and all the data necessary to verify such signature. ```jsx -interface SignMessageOut { +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 ":" signature: string; // The base64 representation of the signature. @@ -119,7 +119,7 @@ interface SignMessageOut { ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `AuthenticationToken` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. From 2e13c7ccf3d52042d08dac4b10a3fbbd720d7423 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 1 Dec 2022 15:03:34 -0300 Subject: [PATCH 24/40] Explained nonce's type --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 77ebb748b..bd3b8aecc 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -68,7 +68,7 @@ Wallets must implement a `signMessage` method, which takes a `message` destined interface SignMessageParams { message: string ; // The message that wants to be transmitted. receiver: string; // The receiver 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 + nonce: [u8; 32] ; // A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `number[]` 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`. } ``` From 9159deceb49f4bb4d393102105a59919c3fe3215 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 2 Dec 2022 16:37:18 -0300 Subject: [PATCH 25/40] Corrected nonce type (`number[]` -> `Buffer`) --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index bd3b8aecc..360a3902e 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -68,7 +68,7 @@ Wallets must implement a `signMessage` method, which takes a `message` destined interface SignMessageParams { message: string ; // The message that wants to be transmitted. receiver: string; // The receiver 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 `number[]` in JS/TS). + 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`. } ``` From dd84fb4d9a83eea128faef1a2cfdad25d96632d0 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Mon, 19 Dec 2022 15:11:21 -0300 Subject: [PATCH 26/40] Fixed wrong naming --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 360a3902e..fe42ddc89 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -124,7 +124,7 @@ Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly r If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. #### Other Wallets -Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignMessageOut` (in preference as a JSON object) and raise an error on failure. +Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignedMessage` (in preference as a JSON object) and raise an error on failure. ## References A full example on how to implement the `signMessage` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). From a18917ae1c637e25450f0bcfa277773f8b78db3e Mon Sep 17 00:00:00 2001 From: gagdiez Date: Mon, 19 Dec 2022 15:17:03 -0300 Subject: [PATCH 27/40] Added Ledger drawback --- neps/nep-0413.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index fe42ddc89..34cb53bd4 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -132,6 +132,10 @@ A full example on how to implement the `signMessage` method can be [found here]( ## Drawbacks 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. + + ## Copyright [copyright]: #copyright From 8721a7a1e92236bf7d08c96ce50e5a48070c1ebd Mon Sep 17 00:00:00 2001 From: gagdiez Date: Wed, 21 Dec 2022 16:12:16 -0300 Subject: [PATCH 28/40] Removed wrong quoting --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 34cb53bd4..a3a4c57ce 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -103,7 +103,7 @@ Assuming that the `signMessage` method was invoked, and that: The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEP0413:` + `{"message":"hi","receiver":"myapp.com","nonce":"[0,...,31]"}`) +sha256.hash(`NEP0413:` + `{"message":"hi","receiver":"myapp.com","nonce":[0,...,31]}`) ``` ### Output Interface From 16eeef17468f60261c5d1204d12ef74344fe2303 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 22 Dec 2022 16:34:58 -0300 Subject: [PATCH 29/40] Ordered attributes in example --- neps/nep-0413.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index a3a4c57ce..faef5d2bd 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -79,8 +79,8 @@ interface SignMessageParams { ```rust struct Payload { message: string; // The same message passed in `SignMessageParams.message` - receiver: string; // The same receiver passed in `SignMessageParams.receiver` nonce: [u8; 32]; // The same nonce passed in `SignMessageParams.nonce` + receiver: string; // The same receiver passed in `SignMessageParams.receiver` } ``` @@ -96,14 +96,14 @@ In order to create a signature, `signMessage` must: ### Example Assuming that the `signMessage` method was invoked, and that: - The input `message` is `"hi"` -- The input `receiver` is `"myapp.com"` - The input `nonce` is `[0,...,31]` +- The input `receiver` is `"myapp.com"` - The wallet stores a full-access private key The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEP0413:` + `{"message":"hi","receiver":"myapp.com","nonce":[0,...,31]}`) +sha256.hash(`NEP0413:` + `{"message":"hi","nonce":[0,...,31],"receiver":"myapp.com"}`) ``` ### Output Interface From 112dd784f96a16854fbb19818f5eaac30a4f6a54 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 20 Jan 2023 16:06:50 -0300 Subject: [PATCH 30/40] Changed receiver for recipient During the NEAR Wallet Standards Work Group Meeting it was recommended to change the name of the `receiver` parameter, since it could be confused with the `receiver` of normal transactions. The community saw this as something favourable and carried a vote. The new name for the parameter is now `recipient`. --- neps/nep-0413.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index faef5d2bd..4abd71271 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -11,18 +11,18 @@ Created: 25-Oct-2022 ## Summary -A standardized Wallet API method, namely `signMessage`, that allows users to sign a message for a specific receiver using their NEAR account. +A standardized Wallet API method, namely `signMessage`, that allows users to sign a message for a specific recipient using their NEAR account. ## Motivation -NEAR users want to create messages destined to a specific receiver using their accounts. This has multiple applications, one of them being authentication in third-party services. +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 receiver. +Currently, there is no standardized way for wallets to sign a message destined to a specific recipient. ## Rationale and Alternatives -Users want to sign messages for a specific receiver without incurring in GAS fees, nor compromising their account's security. This means that the message being signed: +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: 1) Must be signed off-chain, with no transactions being involved. -2) Must include the receiver's name and a nonce. +2) Must include the recipient's name and a nonce. 3) Cannot represent a valid transaction. 3) Must be signed using a Full Access Key. 4) Should be simple to produce/verify, and transmitted securely. @@ -39,7 +39,7 @@ In NEAR, transactions are encoded in Borsh before being signed. The first attrib By prepending the string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data) which, even if present, would represent an invalid account (accounts have less than 64 chars). ### Why The Message Needs to Include a Service Identifier and Nonce? -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 receiver and making sure the user knows about it should mitigate this kind of attacks. +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 this kind of attacks. Meanwhile, including a nonce helps to mitigate replay attacks, in which an attacker can delay or re-send a signed message. @@ -59,7 +59,7 @@ Sending the signed message in a query string to an arbitrary URL (even within th NEAR transaction signatures are not plain Ed25519 signatures but Ed25519 signatures of a SHA-256 hash (see [near/nearcore#2835](https://github.com/near/nearcore/issues/2835)). Any protocol that signs anything with NEAR account keys should use the same signature format. ## Specification -Wallets must implement a `signMessage` method, which takes a `message` destined to a specific `receiver` and transform it into a verifiable signature. +Wallets must implement a `signMessage` method, which takes a `message` destined to a specific `recipient` and transform it into a verifiable signature. ### Input Interface `signMessage` must implement the following input interface: @@ -67,20 +67,20 @@ Wallets must implement a `signMessage` method, which takes a `message` destined ```jsx interface SignMessageParams { message: string ; // The message that wants to be transmitted. - receiver: string; // The receiver to whom the message is destined (e.g. "alice.near" or "myapp.com"). + 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`. } ``` ### Structure -`signMessage` must embed the input `message`, `receiver` and `nonce` into the following predefined structure: +`signMessage` must embed the input `message`, `recipient` and `nonce` into the following predefined structure: ```rust struct Payload { message: string; // The same message passed in `SignMessageParams.message` nonce: [u8; 32]; // The same nonce passed in `SignMessageParams.nonce` - receiver: string; // The same receiver passed in `SignMessageParams.receiver` + recipient: string; // The same recipient passed in `SignMessageParams.recipient` } ``` @@ -97,13 +97,13 @@ In order to create a signature, `signMessage` must: Assuming that the `signMessage` method was invoked, and that: - The input `message` is `"hi"` - The input `nonce` is `[0,...,31]` -- The input `receiver` is `"myapp.com"` +- The input `recipient` is `"myapp.com"` - The wallet stores a full-access private key The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(`NEP0413:` + `{"message":"hi","nonce":[0,...,31],"receiver":"myapp.com"}`) +sha256.hash(`NEP0413:` + `{"message":"hi","nonce":[0,...,31],"recipient":"myapp.com"}`) ``` ### Output Interface From a84a43f3c6618cd64ca2ba1093beb4dbd5807fa6 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 20 Jan 2023 16:11:37 -0300 Subject: [PATCH 31/40] Fixed wrong encoding for returning keys As noticed by @frol, there was a spelling mistake in the comment of the returning key. It should have read base58 instead of base64. --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 4abd71271..e15c43ae1 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -112,7 +112,7 @@ sha256.hash(`NEP0413:` + `{"message":"hi","nonce":[0,...,31],"recipient":"myapp. ```jsx 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 ":" + publicKey: string; // The public counterpart of the key used to sign, expressed as a string with format ":" (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y") signature: string; // The base64 representation of the signature. } ``` From c4065d2e02f615b26278c81112ea633923420590 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 27 Jan 2023 16:18:15 -0300 Subject: [PATCH 32/40] Added Decision Context --- neps/nep-0413.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index e15c43ae1..2978b21fa 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -3,7 +3,7 @@ NEP: 413 Title: Near Wallet API - support for signMessage method Author: Philip Obosi , Guillermo Gallardo # DiscussionsTo: -Status: Draft +Status: Approved Type: Standards Track Category: Wallet Created: 25-Oct-2022 @@ -135,6 +135,26 @@ Accounts that do not hold a FullAccess Key will not be able to sign this kind of 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. +## Decision Context + +### 1.0.0 - Initial Version +The Wallet Standards Working Group members approved this NEP on January 17, 2023 ([meeting recording](https://youtu.be/Y6z7lUJSUuA)). + +### Benefits + +- 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. + +### Concerns + +| # | 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 [copyright]: #copyright From d278e48d4d4cfcb6f90ff199fd79f3e2b8957b0b Mon Sep 17 00:00:00 2001 From: gagdiez Date: Wed, 1 Feb 2023 10:15:40 -0300 Subject: [PATCH 33/40] Fixed minor typo --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 2978b21fa..56f8cd6ec 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -34,7 +34,7 @@ So the user would not incur in GAS fees, nor the signed message gets broadcasted An attacker could make the user inadvertently sign a valid transaction which, once signed, could be submitted into the network to execute it. #### How to Ensure the Message is not a Transaction -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) 32 bytes representing the string's length, (2) N bytes representing the string itself. +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 string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data) which, even if present, would represent an invalid account (accounts have less than 64 chars). From 17c4ec53876b5ab246969efe4bfe7bdef19bac17 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Sat, 11 Feb 2023 10:34:57 -0300 Subject: [PATCH 34/40] Update nep-0413.md --- neps/nep-0413.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 56f8cd6ec..9c5e82268 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -36,10 +36,10 @@ An attacker could make the user inadvertently sign a valid transaction which, on #### How to Ensure the Message is not a Transaction 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 string `"NEP0413:"` we can ensure that the whole message is an invalid transaction. This is because `"NEP0413:"` is `[78, 69, 80, 48]` in bytes, which borsh interprets as `810566990`. When parsing this as a transaction, Borsh will then try to read `810566990` chars (~810Mb of data) which, even if present, would represent an invalid account (accounts have less than 64 chars). +By prepending the prefix tag $2^{31} + 413$ we can both ensure that (1) the whole message is an invalid transaction (since the string would be too long to parse), (2) this NEP is ready for a potential future protocol update, in which non-consensus messages are tagged using $2^{31}$ + NEP-number. -### Why The Message Needs to Include a Service Identifier and Nonce? -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 this kind of attacks. +### Why The Message Needs to Include a Receiver and Nonce? +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. @@ -78,16 +78,18 @@ interface SignMessageParams { ```rust struct Payload { + tag: u32 = 2147484061 // A fixed prefix tag, always 2147484061 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.message` } ``` ### Signature In order to create a signature, `signMessage` must: -1. Convert the `Payload` into its [JSON JCS](https://www.rfc-editor.org/rfc/rfc8785) string representation (i.e. a JSON string, with alphabetically ordered attributes). -2. Prepend the `NEP0413:` string to the result from step 1. +1. Create a `Payload` object, making sure the `tag` is always 2147484061. +2. Convert the `payload` into its [Borsh Representation](https://borsh.io). 3. Compute the `SHA256` hash of the result from step 2. 4. Sign the resulting `SHA256` hash from step 3 using a **full-access** key. @@ -98,12 +100,13 @@ 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: ```jsx -sha256.hash(`NEP0413:` + `{"message":"hi","nonce":[0,...,31],"recipient":"myapp.com"}`) +sha256.hash(Borsh.serialize(Payload{tag: 2147484061, message:"hi", nonce:[0,...,31], recipient:"myapp.com", callbackUrl: "myapp.com/callback"})) ``` ### Output Interface @@ -119,26 +122,34 @@ interface SignedMessage { ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `?accountId=&publicKey=#signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `#accountId=&publicKey=&signature=`. If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. #### Other Wallets Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignedMessage` (in preference as a JSON object) and raise an error on failure. +### Requesting the Signature for an Auth Protocol +While outside of the scope of this NEP, we feel the need to raise awareness on how to use this specification safely on an Authentication protocol. Because of this, we urge readers to learn about (CSRF attacks)[https://auth0.com/docs/secure/attack-protection/state-parameters] and implement ways to mitigate them, e.g. by keeping a `state` variable between the request of the signature and its authentication. + ## References A full example on how to implement the `signMessage` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). ## Drawbacks 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](https://auth0.com/docs/secure/attack-protection/state-parameters), and implement ways to mitigate, e.g. by keeping a local `state` variable between the request of the signature and its authentication. + ## Decision Context ### 1.0.0 - Initial Version -The Wallet Standards Working Group members approved this NEP on January 17, 2023 ([meeting recording](https://youtu.be/Y6z7lUJSUuA)). +The Wallet Standards Working Group members approved this NEP on January 17, 2023 ([meeting recording](https://youtu.be/Y6z7lUJSUuA)). + +### 1.1.0 - First Revison +Important Security concerns were raised by a community member, driving us to change the proposed implementation. + ### Benefits From d68a682490725524ee0a5d1457440b14f372e0c9 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Sat, 11 Feb 2023 14:55:24 -0300 Subject: [PATCH 35/40] More explicit statement on fragments --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 9c5e82268..260fab133 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -122,7 +122,7 @@ interface SignedMessage { ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing both `accountId` and `publicKey` as strings, and the `signature` as an URL fragment. This is: `#accountId=&publicKey=&signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing the `accountId`,`publicKey`, and the `signature` as URL fragments. This is: `#accountId=&publicKey=&signature=`. If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. From 2e43d0e9cdfd786e399cb0c572766a1c1047886d Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 16 Feb 2023 11:21:46 +0100 Subject: [PATCH 36/40] State variable in message for CSRF mitigation --- neps/nep-0413.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 260fab133..8bdde2d9f 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -78,7 +78,7 @@ interface SignMessageParams { ```rust struct Payload { - tag: u32 = 2147484061 // A fixed prefix tag, always 2147484061 + tag: u32 = 2147484061 // A fixed prefix tag (NEP461), always 2147484061 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` @@ -88,7 +88,7 @@ struct Payload { ### Signature In order to create a signature, `signMessage` must: -1. Create a `Payload` object, making sure the `tag` is always 2147484061. +1. Create a `Payload` object, making sure the `tag` is always 2147484061 (NEP461). 2. Convert the `payload` into its [Borsh Representation](https://borsh.io). 3. Compute the `SHA256` hash of the result from step 2. 4. Sign the resulting `SHA256` hash from step 3 using a **full-access** key. @@ -130,7 +130,7 @@ If the signing process fails, then the wallet must return an error message as a Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignedMessage` (in preference as a JSON object) and raise an error on failure. ### Requesting the Signature for an Auth Protocol -While outside of the scope of this NEP, we feel the need to raise awareness on how to use this specification safely on an Authentication protocol. Because of this, we urge readers to learn about (CSRF attacks)[https://auth0.com/docs/secure/attack-protection/state-parameters] and implement ways to mitigate them, e.g. by keeping a `state` variable between the request of the signature and its authentication. +While outside of the scope of this NEP, we feel the need to raise awareness on how to use this specification safely on an Authentication protocol. Because of this, we urge readers to learn about (CSRF attacks)[https://auth0.com/docs/secure/attack-protection/state-parameters] and implement ways to mitigate them, e.g. by keeping a `state` variable, which can be included in the `message` field, so it persists between the request of the signature and its authentication. ## References A full example on how to implement the `signMessage` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). @@ -140,7 +140,7 @@ Accounts that do not hold a FullAccess Key will not be able to sign this kind of 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](https://auth0.com/docs/secure/attack-protection/state-parameters), and implement ways to mitigate, e.g. by keeping a local `state` variable between the request of the signature and its authentication. +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](https://auth0.com/docs/secure/attack-protection/state-parameters), and implement ways to mitigate, e.g. by keeping a local `state` variable between the request of the signature and its authentication, and include it in the `message` field. ## Decision Context From 5d5712a2deeea0dd614988953f80e1089791c73e Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 16 Feb 2023 16:28:07 +0100 Subject: [PATCH 37/40] fixed typo Co-authored-by: Jon Lewis --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 8bdde2d9f..e11a74b8f 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -82,7 +82,7 @@ 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.message` + callbackUrl?: string // The same callbackUrl passed in `SignMessageParams.callbackUrl` } ``` From 23bd3accb86ad81e4f7ff58993c04244de402912 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 17 Feb 2023 12:06:27 +0100 Subject: [PATCH 38/40] Use implicit prefix following NEP461 update --- neps/nep-0413.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index e11a74b8f..5fce94087 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -78,7 +78,6 @@ interface SignMessageParams { ```rust struct Payload { - tag: u32 = 2147484061 // A fixed prefix tag (NEP461), always 2147484061 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` @@ -88,10 +87,11 @@ struct Payload { ### Signature In order to create a signature, `signMessage` must: -1. Create a `Payload` object, making sure the `tag` is always 2147484061 (NEP461). +1. Create a `Payload` object. 2. Convert the `payload` into its [Borsh Representation](https://borsh.io). -3. Compute the `SHA256` hash of the result from step 2. -4. Sign the resulting `SHA256` hash from step 3 using a **full-access** key. +3. Prepend the 4-bytes borsh representation of $2^{31}+413$, as the [prefix tag](https://github.com/near/NEPs/pull/461). +4. Compute the `SHA256` hash of the serialized-prefix + serialized-tag. +5. 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. @@ -106,7 +106,8 @@ Assuming that the `signMessage` method was invoked, and that: The wallet must construct and sign the following `SHA256` hash: ```jsx -sha256.hash(Borsh.serialize(Payload{tag: 2147484061, message:"hi", nonce:[0,...,31], recipient:"myapp.com", callbackUrl: "myapp.com/callback"})) +// 2**31 + 413 == 2147484061 +sha256.hash(Borsh.serialize(2147484061) + Borsh.serialize(Payload{message:"hi", nonce:[0,...,31], recipient:"myapp.com", callbackUrl: "myapp.com/callback"})) ``` ### Output Interface From 3a3375471a36c935f8dd2d9170a0c6f26eaac2a3 Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 23 Feb 2023 10:53:48 +0100 Subject: [PATCH 39/40] Text improvement Co-authored-by: Vlad Frolov --- neps/nep-0413.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 5fce94087..2fdb0cd4b 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -36,7 +36,7 @@ An attacker could make the user inadvertently sign a valid transaction which, on #### How to Ensure the Message is not a Transaction 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 $2^{31} + 413$ we can both ensure that (1) the whole message is an invalid transaction (since the string would be too long to parse), (2) this NEP is ready for a potential future protocol update, in which non-consensus messages are tagged using $2^{31}$ + NEP-number. +By prepending the prefix tag $2^{31} + 413$ we can both ensure that (1) the whole message is an invalid transaction (since the string would be too long to be a valid signer account id), (2) this NEP is ready for a potential future protocol update, in which non-consensus messages are tagged using $2^{31}$ + NEP-number. ### Why The Message Needs to Include a Receiver and Nonce? 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. From 53070e7b6f9ee3a468b822914b070de2ae75427f Mon Sep 17 00:00:00 2001 From: gagdiez Date: Thu, 23 Feb 2023 11:15:26 +0100 Subject: [PATCH 40/40] Implemented optional state parameter --- neps/nep-0413.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/neps/nep-0413.md b/neps/nep-0413.md index 2fdb0cd4b..bbac6797e 100644 --- a/neps/nep-0413.md +++ b/neps/nep-0413.md @@ -52,6 +52,9 @@ One might feel tempted to reproduce such process here, for example, by creating 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). +### Why The Input Needs to Include a State? +Including a state helps to mitigate [CSRF attacks](https://auth0.com/docs/secure/attack-protection/state-parameters). 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. + ### How to Return the Signed Message in a Safe Way 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`](https://greenbytes.de/tech/webdav/rfc2616.html#header.referer). @@ -70,6 +73,7 @@ interface SignMessageParams { 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. } ``` @@ -118,21 +122,19 @@ 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 ":" (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. } ``` ### Returning the signature #### Web Wallets -Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing the `accountId`,`publicKey`, and the `signature` as URL fragments. This is: `#accountId=&publicKey=&signature=`. +Web Wallets, such as [MyNearWallet](https://mynearwallet.com), should directly return the `SignedMessage` to the `SignMessageParams.callbackUrl`, passing the `accountId`,`publicKey`, `signature` and the state as URL fragments. This is: `#accountId=&publicKey=&signature=&state=`. -If the signing process fails, then the wallet must return an error message as a string parameter: `?error=`. +If the signing process fails, then the wallet must return an error message and the state as string fragments: `#error=&state=`. #### Other Wallets Non-web Wallets, such as [Ledger](https://www.ledger.com) can directly return the `SignedMessage` (in preference as a JSON object) and raise an error on failure. -### Requesting the Signature for an Auth Protocol -While outside of the scope of this NEP, we feel the need to raise awareness on how to use this specification safely on an Authentication protocol. Because of this, we urge readers to learn about (CSRF attacks)[https://auth0.com/docs/secure/attack-protection/state-parameters] and implement ways to mitigate them, e.g. by keeping a `state` variable, which can be included in the `message` field, so it persists between the request of the signature and its authentication. - ## References A full example on how to implement the `signMessage` method can be [found here](https://github.com/gagdiez/near-login/blob/main/tests/authentication/auth.ava.ts#L27-#L65). @@ -141,7 +143,7 @@ Accounts that do not hold a FullAccess Key will not be able to sign this kind of 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](https://auth0.com/docs/secure/attack-protection/state-parameters), and implement ways to mitigate, e.g. by keeping a local `state` variable between the request of the signature and its authentication, and include it in the `message` field. +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](https://auth0.com/docs/secure/attack-protection/state-parameters), and make use of the `state` field. ## Decision Context