Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEP-483: Prefix Tag for Signed Messages #483

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions neps/nep-0483.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
NEP: 0461
Title: Prefix Tag for Signed Messages
Author: Evgeny Kapun <eatmore@near.org>, Gallardo Guillermo <guillermo@near.org>, Firat Sertgoz <firat@near.org>
DiscussionsTo: https://github.com/nearprotocol/neps/pull/461
Status: Review
Type: Protocol
Category: Runtime
Version: 1.0.0
Created: 14-Feb-2023
Updated: 14-Feb-2023
---

## Summary

A proposal to include a standardized prefix in all standardized signable messages, to distinguish between (1) actionable messages (i.e. those that are validated on chain), (2) non-actionable messages (i.e. those that are used off-chain), and (3) [classic transactions](https://nomicon.io/RuntimeSpec/Transactions).

Particularly, we propose to prepend an explicit prefix to the structure being signed using the following rules:

| Prefix | Message |
| - | - |
| $< 2^{30}$ | [Classic Transactions](https://nomicon.io/RuntimeSpec/Transactions) |
| $2^{30}$ + NEP-number | On-chain Actionable Message, e.g. [`SignedDelegateAction`](https://github.com/near/NEPs/blob/master/neps/nep-0366.md) |
| $2^{31}$ + NEP-number | Off-chain Message, e.g. [`signMessage`](https://github.com/near/NEPs/pull/413) |
gagdiez marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

As the ecosystem grows, more standards will appear, enabling users to sign messages that are not transactions (e.g. [NEP413](https://github.com/near/NEPs/pull/413/), and [NEP366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md)). If such messages collide with transactions, a malicious app could ask users to sign them, only to later relay them as valid transactions to the network.

## Rationale and alternatives

In NEAR, users sign transactions that are later relayed to the network. At the moment of being relayed, the transactions are simple streams of bytes that a node then parses into a [valid transaction](https://nomicon.io/RuntimeSpec/Transactions).

As the protocol expands, more messages will be added for users to sign. We want users to be able to use the protocol safely, making sure that all those signed messages cannot be interpreted as a valid transaction.

In practice, what users sign is the `sha256` hash of a [borsh-serialized](https://borsh.io) structure. This is:

```ts
sha256.hash(Borsh.serialize(MessageStructure))
```

Including a large `u32` prefix allows to easily distinguish transactions from messages, ensuring that they never overlap.

```ts
sha256.hash(Borsh.serialize<u32>(large-number) + Borsh.serialize(MessageStructure))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original comment by @mfornet

Before reading the suggestion, I thought about the user signing a message of this type:

Suggested change
sha256.hash(Borsh.serialize<u32>(large-number) + Borsh.serialize(MessageStructure))
Borsh.serialize<u32>(large-number) + sha256.hash(Borsh.serialize(MessageStructure))

It is 4 bytes longer, but the advantage is that the wallet (signing device) can tell apart the target (i.e transaction/on-chain/off-chain) by just looking at this payload, rather than requiring the whole message. It is probably not safe anyway to sign a payload without seeing the message, but it might make sense for low-end devices (like some hardware wallets) where having the whole message is expensive or not possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mfornet wouldn't this have the problem that sha256(transaction) could collide with borsh(prefix) + sha256(MessageStruture)? If all transactions are of the form sha256(prefix + transaction) then by definition there cannot be a collision.

```

This is because the first field of a [valid transaction](https://nomicon.io/RuntimeSpec/Transactions) is an `AccountId`, which is encoded as `string length <u32>` + `string chars <u32[]>`. Prepending a prefix in the range of $(2^{30}, 2^{32})$ will automatically make the message deserialize into an invalid transaction, since no `AccountId` has more than 64 chars.


## Specification

### On-Chain Messages

All actionable messages, i.e. those that ARE validated on-chain, should prepend an explicit `prefix: u32` with fixed value $2^{30}$ + NEP-number.

For example, if we implement a new actionable message `ActionableMessage` destined to be used on-chain, and the proposal has the number `NEP400`, then the message must have the following form:

```ts
// 2**30 + 400 = 1073742224
sha256.hash(Borsh.serialize<u32>(1073742224) + Borsh.serialize(ActionableMessage))
```

### Off-Chain Messages

All non-actionable messages, i.e. those that are NOT validated on-chain, should prepend an explicit `prefix: u32` with fixed value $2^{31}$ + NEP-number.

For example, if we implement a new non-actionable message `NonActionableMessage`, destined to be used off-chain, and the proposal has the number `NEP400`, then the message to be signed is:

```ts
// 2**31 + 400 = 2147484048
sha256.hash(Borsh.serialize<u32>(2147484048) + Borsh.serialize(NonActionableMessage))
```

## Security Implications

By implementing this NEP, we can be sure that all new messages do not collide with classic transactions.

## Drawbacks

We will need to update [NEP366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md) to follow this standard, and make sure all future NEPs follow it, which might be hard to track.

If at any point there is an update on how transactions are encoded, this NEP could stop being valid.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original comment by @mfornet

If any protocol change updates the tx encoding, we should enforce that the first 4 bytes are used following this NEP. (i.e., they must be decoded as a number in the range [0, 2**30).

What do you think the best way is to be alert of this? (Putting some comments in the client source code?)

Copy link
Contributor Author

@gagdiez gagdiez May 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

original comment by @mfornet

I propose introducing a "protocol update" that enforces the first four bytes of every "classic tx" to be in the proper range. This is the case today (implicitly), but we should add it to the spec, and an extra assertion to the client (no real protocol change required) so we don't break this in the future.