diff --git a/docs/src/proposals/off-chain-message-signing.md b/docs/src/proposals/off-chain-message-signing.md new file mode 100644 index 0000000000..d0918ad6ad --- /dev/null +++ b/docs/src/proposals/off-chain-message-signing.md @@ -0,0 +1,138 @@ +# Off-chain message signing +## Motivation + +There is ecosystem demand for a method of signing non-transaction messages with +a Solana wallet. Typically this is some kind of "proof of wallet ownership" for +entry into a whitelisted system. + +Some inspiration can be gleaned from relevant portions of Ethereum's +[EIP-712](https://eips.ethereum.org/EIPS/eip-712) + +## Considerations + +* Security + * Off-chain message signatures should not be valid transaction message signatures +* Future-proofing + * Versioning + * Co-exist with versioned transaction messages and extended length transactions +* Compatibility + * Hardware wallet signing + * Localization + +## Message Preamble + +| Field | Start offset | Length (bytes) | Description +| :---- | :----------: | :------------: | :---------- +| Signing Domain | 0x00 | 16 | \[[1](#signing-domain-specifier)\] +| Header version | 0x10 | 1 | \[[2](#header-version)\] +| Application domain | 0x11 | 32 | \[[3](#application-domain)\] +| Message format | 0x31 | 1 | \[[4](#message-format)\] +| Signer count | 0x32 | 1 | Number of signers committing to the message. An 8-bit, unsigned integer. **MUST NOT** be zero. +| Signers | 0x33 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys of signers +| Message length | 0x33 + `SIGNER_CNT` * 32 | 2 | Length of message in bytes. A 16-bit, unsigned, little-endian integer. **MUST NOT** be zero. + +### Signing Domain Specifier + +The signing domain specifier is a prefix byte string used to give similar structure +to all off-chain message signatures. We assign its value to be: +``` +b"\xffsolana offchain" +``` +The first byte, `\xff`, was chosen for the following reasons +1. It corresponds to a value that is illegal as the first byte in a transaction +`MessageHeader`. +1. It avoids unintentional misuse in languages with C-like, null-terminated strings. + +The remaining bytes, `b"solana offchain"`, were chosen to be descriptive and +reasonably long, but are otherwise arbitrary. + +This field **SHOULD NOT** be displayed to users + +### Header + +#### Header version + +The header version is represented as an 8-bit unsigned integer. Only the version +0 header format is specfied in this document + +This field **SHOULD NOT** be displayed to users + +#### Application domain + +A 32-byte array identifying the application requesting off-chain message signing. +This may be any arbitrary bytes. For instance the on-chain address of a program, +DAO instance, Candy Machine, etc. + +This field **SHOULD** be displayed to users as a base58-encoded ASCII string rather +than interpretted otherwise. + +#### Message Format + +Version `0` headers specify three message formats allowing for trade-offs between +compatibility and composition of messages. + +| ID | Encoding | Maximum Length \* | Hardware Wallet Support | +| :-: | :-------------------: | :---------------: | :---------------------: | +| 0 | Restricted ASCII \*\* | 1232 | Yes | +| 1 | UTF-8 | 1232 | Blind sign only | +| 2 | UTF-8 | 65535 | No | + +\* Combined length of the [message preamble](#message-preamble) and message body
+\*\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) +returns true. That is, `0x20..=0x7e`. + +Both the message encoding and maximum message length **MUST** be enforced by +signer and verifier. + +Formats `0` and `1` are motivated by hardware wallet support where both RAM +to store the payload and font character support are limited. + +This field **SHOULD NOT** be displayed to users + +## Signing + +Solana off-chain messages **MUST** only be signed using the ed25519 digital +signature scheme. Before signing, the message **MUST** be strictly checked to +conform to the associated preamble. The message body is then appended to the +[message preamble](#message-preamble). Finally the result is ed25519 signed. + +## Verification +Upon successful ed25519 verification of _all_ attached signatures, the message +**MUST** be strictly checked to conform to the [message preamble](#message-preamble). +A message that does not conform to its preamble is invalid, regardless of the +validity of any signatures. + +## Envelope + +When passing around signed off-chain messages a common format is helpful. The +recommended binary representation is as follows: + +| Field | Start offset | Length (bytes) | Description +| :---- | :----------: | :------------: | :---------- +| Signature Count \* | 0x00 | 1 | Number of signatures. An 8-bit, unsigned integer +| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures +| Message Preamble | 0x01 + `SIG_COUNT` * 64 | `PREAMBLE_LEN` | The [message preamble](#message-preamble) +| Message Body | 0x01 + `SIG_COUNT * 64 + `PREAMBLE_LEN` | `MESSAGE_LEN` | The message content + +The signature count **MUST** match the value of signers count from the message +preamble. + +Signatures **MUST** be ordered to match their corresponding public keys as +specified in the message preamble. + +## Runtime Considerations + +To prevent social attacks by which the signer is tricked into signing a transaction, +the runtime **MUST NOT** accept signed off-chain messages as transactions under any +circumstances. The first byte of the [signing domain specifier](#signing-domain-specifier) +is chosen such that it corresponds to a value (`0xff`) which is implicitly illegal +as the first byte in a transaction `MessageHeader` today. The property is implicit +because the top bit in the first byte of a `MessageHeader` being set signals a +versioned transaction, but only a value of +[zero is supported](https://github.com/solana-labs/solana/blob/b6ae6c1fe17e4b64c5051c651ca2585e4f55468c/sdk/program/src/message/versions/mod.rs#L269-L281) +at this time. The runtime will need to be modified to reserve 127 as an illegal +version number, making this property explicit. + +### Implementation + +The runtime changes described above have been implemented in PR [#29807](https://github.com/solana-labs/solana/pull/29807)