Skip to content

Commit

Permalink
Fixes per PR review
Browse files Browse the repository at this point in the history
  • Loading branch information
askibin committed Oct 13, 2022
1 parent 5ab56c2 commit d29b2e8
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 45 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 35 additions & 14 deletions docs/src/cli/sign-offchain-message.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
title: Off-Chain Message Signing
---

Off-chain message signing is a method of signing non-transaction messages with a Solana wallet. This feature can be used to authenticate users or provide proof of wallet ownership.
Off-chain message signing is a method of signing non-transaction messages with
a Solana wallet. This feature can be used to authenticate users or provide
proof of wallet ownership.

## Sign Off-Chain Message

Expand All @@ -12,22 +14,27 @@ To sign an arbitrary off-chain message, run the following command:
solana sign-offchain-message <MESSAGE>
```

The message will be encoded and signed with CLI's default private key and signature printed to the output. If you want to sign it with another key, just use the `-k/--keypair` option:
The message will be encoded and signed with CLI's default private key and
signature printed to the output. If you want to sign it with another key, just
use the `-k/--keypair` option:

```bash
solana sign-offchain-message -k <KEYPAIR> <MESSAGE>
```

By default, version 0 messages are constructed (and the only supported at this moment). When other versions become available, you can override default value with `--version` option:
By default, the messages constructed are version 0, the only version currently
supported. When other versions become available, you can override the default
value with the `--version` option:

```bash
solana sign-offchain-message -k <KEYPAIR> --version <VERSION> <MESSAGE>
```

The message format is determined automatically based on the version and text of the message.
The message format is determined automatically based on the version and text
of the message.

Version `0` headers specify three message formats allowing for trade-offs between
compatibility and composition of messages:
Version `0` headers specify three message formats allowing for trade-offs
between compatibility and composition of messages:

| ID | Encoding | Maximum Length | Hardware Wallet Support |
| :-: | :-----------------: | :------------: | :---------------------: |
Expand All @@ -38,19 +45,26 @@ compatibility and composition of messages:
\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint)
returns true. That is, `0x20..=0x7e`.

Formats `0` and `1` are motivated by hardware wallet support where both RAM to store the payload and font character support are limited.
Formats `0` and `1` are motivated by hardware wallet support where both RAM to
store the payload and font character support are limited.

To sign an off-chain message with Ledger, ensure your Ledger is running latest firmware and Solana Ledger App version 1.3.0 or later. After Ledger is unlocked and Solana Ledger App is open, run:
To sign an off-chain message with Ledger, ensure your Ledger is running latest
firmware and Solana Ledger App version 1.3.0 or later. After Ledger is
unlocked and Solana Ledger App is open, run:

```bash
solana sign-offchain-message -k usb://ledger <MESSAGE>
```

For more information on how to setup and work with the ledger device see this [link](../wallet-guide/hardware-wallets/ledger.md).
For more information on how to setup and work with the ledger device see this
[link](../wallet-guide/hardware-wallets/ledger.md).

Please note that UTF-8 encoded messages require `Allow blind sign` option enabled in Solana Ledger App. Also, due to the lack of UTF-8 support in Ledger devices, only the hash of the message will be displayed in such cases.
Please note that UTF-8 encoded messages require `Allow blind sign` option
enabled in Solana Ledger App. Also, due to the lack of UTF-8 support in Ledger
devices, only the hash of the message will be displayed in such cases.

If `Display mode` is set to `Expert`, Ledger will display technical information about the message to be signed.
If `Display mode` is set to `Expert`, Ledger will display technical
information about the message to be signed.

## Verify Off-Chain Message Signature

Expand All @@ -60,18 +74,25 @@ To verify the off-chain message signature, run the following command:
solana verify-offchain-signature <MESSAGE> <SIGNATURE>
```

The public key of the default CLI signer will be used. You can specify another key with the `--signer` option:
The public key of the default CLI signer will be used. You can specify another
key with the `--signer` option:

```bash
solana verify-offchain-signature --signer <PUBKEY> <MESSAGE> <SIGNATURE>
```

If the signed message has a version different from the default, you need to specify the matching version explicitly:
If the signed message has a version different from the default, you need to
specify the matching version explicitly:

```bash
solana verify-offchain-signature --version <VERSION> <MESSAGE> <SIGNATURE>
```

## Protocol Specification

To ensure that off-chain messages are not valid transactions, they are encoded with a fixed prefix: `\xffsolana offchain`, where first byte is chosen such that it is implicitly illegal as the first byte in a transaction `MessageHeader` today. More details about the payload format and other considerations are available in the [proposal](https://github.com/solana-labs/solana/blob/e80f67dd58b7fa3901168055211f346164efa43a/docs/src/proposals/off-chain-message-signing.md).
To ensure that off-chain messages are not valid transactions, they are encoded
with a fixed prefix: `\xffsolana offchain`, where first byte is chosen such
that it is implicitly illegal as the first byte in a transaction
`MessageHeader` today. More details about the payload format and other
considerations are available in the
[proposal](https://github.com/solana-labs/solana/blob/e80f67dd58b7fa3901168055211f346164efa43a/docs/src/proposals/off-chain-message-signing.md).
1 change: 1 addition & 0 deletions programs/bpf/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ libsecp256k1 = { version = "0.6.0", optional = true }
log = "0.4.17"
memmap2 = { version = "0.5.3", optional = true }
num-derive = "0.3"
num_enum = "0.5.7"
num-traits = "0.2"
pbkdf2 = { version = "0.11.0", default-features = false }
qstring = "0.7.2"
Expand Down
87 changes: 56 additions & 31 deletions sdk/src/offchain_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@
#![cfg(feature = "full")]

use crate::{
hash::Hash,
pubkey::Pubkey,
sanitize::SanitizeError,
signature::{Signature, Signer},
use {
crate::{
hash::Hash,
pubkey::Pubkey,
sanitize::SanitizeError,
signature::{Signature, Signer},
},
num_enum::{IntoPrimitive, TryFromPrimitive},
};

#[cfg(test)]
static_assertions::const_assert_eq!(OffchainMessage::HEADER_LEN, 17);
#[cfg(test)]
static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN, 65515);
#[cfg(test)]
static_assertions::const_assert_eq!(v0::OffchainMessage::MAX_LEN_LEDGER, 1212);

/// Check if given bytes contain only printable ASCII characters
pub fn is_printable_ascii(data: &[u8]) -> bool {
for &char in data {
Expand All @@ -24,12 +34,21 @@ pub fn is_utf8(data: &[u8]) -> bool {
std::str::from_utf8(data).is_ok()
}

#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, TryFromPrimitive, IntoPrimitive)]
pub enum MessageFormat {
RestrictedAscii,
LimitedUtf8,
ExtendedUtf8,
}

#[allow(clippy::integer_arithmetic)]
pub mod v0 {
use {
super::{is_printable_ascii, is_utf8, OffchainMessage as Base},
super::{is_printable_ascii, is_utf8, MessageFormat, OffchainMessage as Base},
crate::{
hash::{Hash, Hasher},
packet::PACKET_DATA_SIZE,
sanitize::SanitizeError,
},
};
Expand All @@ -38,7 +57,7 @@ pub mod v0 {
/// Struct always contains a non-empty valid message.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OffchainMessage {
format: u8,
format: MessageFormat,
message: Vec<u8>,
}

Expand All @@ -48,23 +67,23 @@ pub mod v0 {
// Max length of the OffchainMessage
pub const MAX_LEN: usize = u16::MAX as usize - Base::HEADER_LEN - Self::HEADER_LEN;
// Max Length of the OffchainMessage supported by the Ledger
pub const MAX_LEN_LEDGER: usize = 1232 - Base::HEADER_LEN - Self::HEADER_LEN;
pub const MAX_LEN_LEDGER: usize = PACKET_DATA_SIZE - Base::HEADER_LEN - Self::HEADER_LEN;

/// Construct a new OffchainMessage object from the given message
pub fn new(message: &[u8]) -> Result<Self, SanitizeError> {
let format = if message.is_empty() {
return Err(SanitizeError::InvalidValue);
} else if message.len() <= OffchainMessage::MAX_LEN_LEDGER {
if is_printable_ascii(message) {
0
MessageFormat::RestrictedAscii
} else if is_utf8(message) {
1
MessageFormat::LimitedUtf8
} else {
return Err(SanitizeError::InvalidValue);
}
} else if message.len() <= OffchainMessage::MAX_LEN {
if is_utf8(message) {
2
MessageFormat::ExtendedUtf8
} else {
return Err(SanitizeError::InvalidValue);
}
Expand All @@ -80,12 +99,10 @@ pub mod v0 {
/// Serialize the message to bytes, including the full header
pub fn serialize(&self, data: &mut Vec<u8>) -> Result<(), SanitizeError> {
// invalid messages shouldn't be possible, but a quick sanity check never hurts
assert!(
self.format <= 2 && !self.message.is_empty() && self.message.len() <= Self::MAX_LEN
);
assert!(!self.message.is_empty() && self.message.len() <= Self::MAX_LEN);
data.reserve(Self::HEADER_LEN.saturating_add(self.message.len()));
// format
data.push(self.format);
data.push(self.format.into());
// message length
data.extend_from_slice(&(self.message.len() as u16).to_le_bytes());
// message
Expand All @@ -100,19 +117,23 @@ pub mod v0 {
return Err(SanitizeError::ValueOutOfBounds);
}
// decode header
let format = data[0];
let format =
MessageFormat::try_from(data[0]).map_err(|_| SanitizeError::InvalidValue)?;
let message_len = u16::from_le_bytes([data[1], data[2]]) as usize;
// check header
if format > 2 || Self::HEADER_LEN.saturating_add(message_len) != data.len() {
if Self::HEADER_LEN.saturating_add(message_len) != data.len() {
return Err(SanitizeError::InvalidValue);
}
let message = &data[3..];
let message = &data[Self::HEADER_LEN..];
// check format
let is_valid = match format {
0 => message.len() <= Self::MAX_LEN_LEDGER && is_printable_ascii(message),
1 => message.len() <= Self::MAX_LEN_LEDGER && is_utf8(message),
2 => message.len() <= Self::MAX_LEN && is_utf8(message),
_ => false,
MessageFormat::RestrictedAscii => {
(message.len() <= Self::MAX_LEN_LEDGER) && is_printable_ascii(message)
}
MessageFormat::LimitedUtf8 => {
(message.len() <= Self::MAX_LEN_LEDGER) && is_utf8(message)
}
MessageFormat::ExtendedUtf8 => (message.len() <= Self::MAX_LEN) && is_utf8(message),
};

if is_valid {
Expand All @@ -126,13 +147,13 @@ pub mod v0 {
}

/// Compute the SHA256 hash of the serialized off-chain message
pub fn hash(&self, serialized_message: &[u8]) -> Result<Hash, SanitizeError> {
pub fn hash(serialized_message: &[u8]) -> Result<Hash, SanitizeError> {
let mut hasher = Hasher::default();
hasher.hash(serialized_message);
Ok(hasher.result())
}

pub fn get_format(&self) -> u8 {
pub fn get_format(&self) -> MessageFormat {
self.format
}

Expand Down Expand Up @@ -191,7 +212,7 @@ impl OffchainMessage {
/// Compute the hash of the off-chain message
pub fn hash(&self) -> Result<Hash, SanitizeError> {
match self {
Self::V0(msg) => msg.hash(&self.serialize()?),
Self::V0(_) => v0::OffchainMessage::hash(&self.serialize()?),
}
}

Expand All @@ -201,9 +222,9 @@ impl OffchainMessage {
}
}

pub fn get_format(&self) -> u32 {
pub fn get_format(&self) -> MessageFormat {
match self {
Self::V0(msg) => msg.get_format() as u32,
Self::V0(msg) => msg.get_format(),
}
}

Expand Down Expand Up @@ -232,9 +253,11 @@ mod tests {
fn test_offchain_message_ascii() {
let message = OffchainMessage::new(0, b"Test Message").unwrap();
assert_eq!(message.get_version(), 0);
assert_eq!(message.get_format(), 0);
assert_eq!(message.get_format(), MessageFormat::RestrictedAscii);
assert_eq!(message.get_message().as_slice(), b"Test Message");
assert!(matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == 0));
assert!(
matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::RestrictedAscii)
);
let serialized = [
255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 0, 12, 0,
84, 101, 115, 116, 32, 77, 101, 115, 115, 97, 103, 101,
Expand All @@ -249,12 +272,14 @@ mod tests {
fn test_offchain_message_utf8() {
let message = OffchainMessage::new(0, "Тестовое сообщение".as_bytes()).unwrap();
assert_eq!(message.get_version(), 0);
assert_eq!(message.get_format(), 1);
assert_eq!(message.get_format(), MessageFormat::LimitedUtf8);
assert_eq!(
message.get_message().as_slice(),
"Тестовое сообщение".as_bytes()
);
assert!(matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == 1));
assert!(
matches!(message, OffchainMessage::V0(ref msg) if msg.get_format() == MessageFormat::LimitedUtf8)
);
let serialized = [
255, 115, 111, 108, 97, 110, 97, 32, 111, 102, 102, 99, 104, 97, 105, 110, 0, 1, 35, 0,
208, 162, 208, 181, 209, 129, 209, 130, 208, 190, 208, 178, 208, 190, 208, 181, 32,
Expand Down

0 comments on commit d29b2e8

Please sign in to comment.