Add off-chain messages support (#27456)
This commit is contained in:
parent
28a89a1d99
commit
108a02cfd4
|
@ -6256,6 +6256,7 @@ dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"num_enum",
|
||||||
"pbkdf2 0.11.0",
|
"pbkdf2 0.11.0",
|
||||||
"qstring",
|
"qstring",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
|
|
|
@ -25,6 +25,7 @@ use {
|
||||||
decode_error::DecodeError,
|
decode_error::DecodeError,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
instruction::InstructionError,
|
instruction::InstructionError,
|
||||||
|
offchain_message::OffchainMessage,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Signature, Signer, SignerError},
|
signature::{Signature, Signer, SignerError},
|
||||||
stake::{instruction::LockupArgs, state::Lockup},
|
stake::{instruction::LockupArgs, state::Lockup},
|
||||||
|
@ -440,6 +441,14 @@ pub enum CliCommand {
|
||||||
},
|
},
|
||||||
// Address lookup table commands
|
// Address lookup table commands
|
||||||
AddressLookupTable(AddressLookupTableCliCommand),
|
AddressLookupTable(AddressLookupTableCliCommand),
|
||||||
|
SignOffchainMessage {
|
||||||
|
message: OffchainMessage,
|
||||||
|
},
|
||||||
|
VerifyOffchainSignature {
|
||||||
|
signer_pubkey: Option<Pubkey>,
|
||||||
|
signature: Signature,
|
||||||
|
message: OffchainMessage,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
@ -470,6 +479,8 @@ pub enum CliError {
|
||||||
RpcRequestError(String),
|
RpcRequestError(String),
|
||||||
#[error("Keypair file not found: {0}")]
|
#[error("Keypair file not found: {0}")]
|
||||||
KeypairFileNotFound(String),
|
KeypairFileNotFound(String),
|
||||||
|
#[error("Invalid signature")]
|
||||||
|
InvalidSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Box<dyn error::Error>> for CliError {
|
impl From<Box<dyn error::Error>> for CliError {
|
||||||
|
@ -821,6 +832,12 @@ pub fn parse_command(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
("transfer", Some(matches)) => parse_transfer(matches, default_signer, wallet_manager),
|
("transfer", Some(matches)) => parse_transfer(matches, default_signer, wallet_manager),
|
||||||
|
("sign-offchain-message", Some(matches)) => {
|
||||||
|
parse_sign_offchain_message(matches, default_signer, wallet_manager)
|
||||||
|
}
|
||||||
|
("verify-offchain-signature", Some(matches)) => {
|
||||||
|
parse_verify_offchain_signature(matches, default_signer, wallet_manager)
|
||||||
|
}
|
||||||
//
|
//
|
||||||
("", None) => {
|
("", None) => {
|
||||||
eprintln!("{}", matches.usage());
|
eprintln!("{}", matches.usage());
|
||||||
|
@ -1634,11 +1651,18 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||||
derived_address_program_id.as_ref(),
|
derived_address_program_id.as_ref(),
|
||||||
compute_unit_price.as_ref(),
|
compute_unit_price.as_ref(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Address Lookup Table Commands
|
// Address Lookup Table Commands
|
||||||
CliCommand::AddressLookupTable(subcommand) => {
|
CliCommand::AddressLookupTable(subcommand) => {
|
||||||
process_address_lookup_table_subcommand(rpc_client, config, subcommand)
|
process_address_lookup_table_subcommand(rpc_client, config, subcommand)
|
||||||
}
|
}
|
||||||
|
CliCommand::SignOffchainMessage { message } => {
|
||||||
|
process_sign_offchain_message(config, message)
|
||||||
|
}
|
||||||
|
CliCommand::VerifyOffchainSignature {
|
||||||
|
signer_pubkey,
|
||||||
|
signature,
|
||||||
|
message,
|
||||||
|
} => process_verify_offchain_signature(config, signer_pubkey, signature, message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2013,7 +2037,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_command(&test_resolve_signer, &default_signer, &mut None).unwrap(),
|
parse_command(&test_resolve_signer, &default_signer, &mut None).unwrap(),
|
||||||
CliCommandInfo {
|
CliCommandInfo {
|
||||||
command: CliCommand::ResolveSigner(Some(keypair_file)),
|
command: CliCommand::ResolveSigner(Some(keypair_file.clone())),
|
||||||
signers: vec![],
|
signers: vec![],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -2029,6 +2053,43 @@ mod tests {
|
||||||
signers: vec![],
|
signers: vec![],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test SignOffchainMessage
|
||||||
|
let test_sign_offchain = test_commands.clone().get_matches_from(vec![
|
||||||
|
"test",
|
||||||
|
"sign-offchain-message",
|
||||||
|
"Test Message",
|
||||||
|
]);
|
||||||
|
let message = OffchainMessage::new(0, b"Test Message").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
parse_command(&test_sign_offchain, &default_signer, &mut None).unwrap(),
|
||||||
|
CliCommandInfo {
|
||||||
|
command: CliCommand::SignOffchainMessage {
|
||||||
|
message: message.clone()
|
||||||
|
},
|
||||||
|
signers: vec![read_keypair_file(&keypair_file).unwrap().into()],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test VerifyOffchainSignature
|
||||||
|
let signature = keypair.sign_message(&message.serialize().unwrap());
|
||||||
|
let test_verify_offchain = test_commands.clone().get_matches_from(vec![
|
||||||
|
"test",
|
||||||
|
"verify-offchain-signature",
|
||||||
|
"Test Message",
|
||||||
|
&signature.to_string(),
|
||||||
|
]);
|
||||||
|
assert_eq!(
|
||||||
|
parse_command(&test_verify_offchain, &default_signer, &mut None).unwrap(),
|
||||||
|
CliCommandInfo {
|
||||||
|
command: CliCommand::VerifyOffchainSignature {
|
||||||
|
signer_pubkey: None,
|
||||||
|
signature,
|
||||||
|
message
|
||||||
|
},
|
||||||
|
signers: vec![read_keypair_file(&keypair_file).unwrap().into()],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -2375,6 +2436,23 @@ mod tests {
|
||||||
|
|
||||||
config.command = CliCommand::GetTransactionCount;
|
config.command = CliCommand::GetTransactionCount;
|
||||||
assert!(process_command(&config).is_err());
|
assert!(process_command(&config).is_err());
|
||||||
|
|
||||||
|
let message = OffchainMessage::new(0, b"Test Message").unwrap();
|
||||||
|
config.command = CliCommand::SignOffchainMessage {
|
||||||
|
message: message.clone(),
|
||||||
|
};
|
||||||
|
config.signers = vec![&keypair];
|
||||||
|
let result = process_command(&config);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
config.command = CliCommand::VerifyOffchainSignature {
|
||||||
|
signer_pubkey: None,
|
||||||
|
signature: result.unwrap().parse().unwrap(),
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
config.signers = vec![&keypair];
|
||||||
|
let result = process_command(&config);
|
||||||
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -33,6 +33,7 @@ use {
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
commitment_config::CommitmentConfig,
|
commitment_config::CommitmentConfig,
|
||||||
message::Message,
|
message::Message,
|
||||||
|
offchain_message::OffchainMessage,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::Signature,
|
signature::Signature,
|
||||||
stake,
|
stake,
|
||||||
|
@ -274,6 +275,71 @@ impl WalletSubCommands for App<'_, '_> {
|
||||||
.arg(fee_payer_arg())
|
.arg(fee_payer_arg())
|
||||||
.arg(compute_unit_price_arg()),
|
.arg(compute_unit_price_arg()),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("sign-offchain-message")
|
||||||
|
.about("Sign off-chain message")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("message")
|
||||||
|
.index(1)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("STRING")
|
||||||
|
.required(true)
|
||||||
|
.help("The message text to be signed")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("version")
|
||||||
|
.long("version")
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("VERSION")
|
||||||
|
.required(false)
|
||||||
|
.default_value("0")
|
||||||
|
.validator(|p| match p.parse::<u8>() {
|
||||||
|
Err(_) => Err(String::from("Must be unsigned integer")),
|
||||||
|
Ok(_) => { Ok(()) }
|
||||||
|
})
|
||||||
|
.help("The off-chain message version")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("verify-offchain-signature")
|
||||||
|
.about("Verify off-chain message signature")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("message")
|
||||||
|
.index(1)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("STRING")
|
||||||
|
.required(true)
|
||||||
|
.help("The text of the original message")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("signature")
|
||||||
|
.index(2)
|
||||||
|
.value_name("SIGNATURE")
|
||||||
|
.takes_value(true)
|
||||||
|
.required(true)
|
||||||
|
.help("The message signature to verify")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("version")
|
||||||
|
.long("version")
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("VERSION")
|
||||||
|
.required(false)
|
||||||
|
.default_value("0")
|
||||||
|
.validator(|p| match p.parse::<u8>() {
|
||||||
|
Err(_) => Err(String::from("Must be unsigned integer")),
|
||||||
|
Ok(_) => { Ok(()) }
|
||||||
|
})
|
||||||
|
.help("The off-chain message version")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
pubkey!(Arg::with_name("signer")
|
||||||
|
.long("signer")
|
||||||
|
.value_name("PUBKEY")
|
||||||
|
.required(false),
|
||||||
|
"The pubkey of the message signer (if different from config default)")
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,6 +513,54 @@ pub fn parse_transfer(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_sign_offchain_message(
|
||||||
|
matches: &ArgMatches<'_>,
|
||||||
|
default_signer: &DefaultSigner,
|
||||||
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||||
|
) -> Result<CliCommandInfo, CliError> {
|
||||||
|
let version: u8 = value_of(matches, "version").unwrap();
|
||||||
|
let message_text: String = value_of(matches, "message")
|
||||||
|
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
|
||||||
|
let message = OffchainMessage::new(version, message_text.as_bytes())
|
||||||
|
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
|
||||||
|
|
||||||
|
Ok(CliCommandInfo {
|
||||||
|
command: CliCommand::SignOffchainMessage { message },
|
||||||
|
signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_verify_offchain_signature(
|
||||||
|
matches: &ArgMatches<'_>,
|
||||||
|
default_signer: &DefaultSigner,
|
||||||
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
||||||
|
) -> Result<CliCommandInfo, CliError> {
|
||||||
|
let version: u8 = value_of(matches, "version").unwrap();
|
||||||
|
let message_text: String = value_of(matches, "message")
|
||||||
|
.ok_or_else(|| CliError::BadParameter("MESSAGE".to_string()))?;
|
||||||
|
let message = OffchainMessage::new(version, message_text.as_bytes())
|
||||||
|
.map_err(|_| CliError::BadParameter("VERSION or MESSAGE".to_string()))?;
|
||||||
|
|
||||||
|
let signer_pubkey = pubkey_of_signer(matches, "signer", wallet_manager)?;
|
||||||
|
let signers = if signer_pubkey.is_some() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![default_signer.signer_from_path(matches, wallet_manager)?]
|
||||||
|
};
|
||||||
|
|
||||||
|
let signature = value_of(matches, "signature")
|
||||||
|
.ok_or_else(|| CliError::BadParameter("SIGNATURE".to_string()))?;
|
||||||
|
|
||||||
|
Ok(CliCommandInfo {
|
||||||
|
command: CliCommand::VerifyOffchainSignature {
|
||||||
|
signer_pubkey,
|
||||||
|
signature,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
signers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_show_account(
|
pub fn process_show_account(
|
||||||
rpc_client: &RpcClient,
|
rpc_client: &RpcClient,
|
||||||
config: &CliConfig,
|
config: &CliConfig,
|
||||||
|
@ -779,3 +893,29 @@ pub fn process_transfer(
|
||||||
log_instruction_custom_error::<SystemError>(result, config)
|
log_instruction_custom_error::<SystemError>(result, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn process_sign_offchain_message(
|
||||||
|
config: &CliConfig,
|
||||||
|
message: &OffchainMessage,
|
||||||
|
) -> ProcessResult {
|
||||||
|
Ok(message.sign(config.signers[0])?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_verify_offchain_signature(
|
||||||
|
config: &CliConfig,
|
||||||
|
signer_pubkey: &Option<Pubkey>,
|
||||||
|
signature: &Signature,
|
||||||
|
message: &OffchainMessage,
|
||||||
|
) -> ProcessResult {
|
||||||
|
let signer = if let Some(pubkey) = signer_pubkey {
|
||||||
|
*pubkey
|
||||||
|
} else {
|
||||||
|
config.signers[0].pubkey()
|
||||||
|
};
|
||||||
|
|
||||||
|
if message.verify(&signer, signature)? {
|
||||||
|
Ok("Signature is valid".to_string())
|
||||||
|
} else {
|
||||||
|
Err(CliError::InvalidSignature.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -339,6 +339,7 @@ module.exports = {
|
||||||
"offline-signing",
|
"offline-signing",
|
||||||
"offline-signing/durable-nonce",
|
"offline-signing/durable-nonce",
|
||||||
"cli/usage",
|
"cli/usage",
|
||||||
|
"cli/sign-offchain-message",
|
||||||
],
|
],
|
||||||
architectureSidebar: [
|
architectureSidebar: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Sign Off-Chain Message
|
||||||
|
|
||||||
|
To sign an arbitrary off-chain message, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
solana sign-offchain-message -k <KEYPAIR> <MESSAGE>
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 \* | 1212 | Yes |
|
||||||
|
| 1 | UTF-8 | 1212 | Blind sign only |
|
||||||
|
| 2 | UTF-8 | 65515 | No |
|
||||||
|
|
||||||
|
\* 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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Verify Off-Chain Message Signature
|
||||||
|
|
||||||
|
To verify the off-chain message signature, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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:
|
||||||
|
|
||||||
|
```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).
|
|
@ -2,7 +2,7 @@
|
||||||
title: Ledger Nano
|
title: Ledger Nano
|
||||||
---
|
---
|
||||||
|
|
||||||
This page describes how to use a Ledger Nano S or Nano X to interact with Solana
|
This page describes how to use a Ledger Nano S, Nano S Plus, or Nano X to interact with Solana
|
||||||
using the command line tools.
|
using the command line tools.
|
||||||
|
|
||||||
## Before You Begin
|
## Before You Begin
|
||||||
|
|
|
@ -5567,6 +5567,7 @@ dependencies = [
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"num_enum",
|
||||||
"pbkdf2 0.11.0",
|
"pbkdf2 0.11.0",
|
||||||
"qstring",
|
"qstring",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
|
|
|
@ -67,6 +67,7 @@ mod commands {
|
||||||
pub const GET_APP_CONFIGURATION: u8 = 0x04;
|
pub const GET_APP_CONFIGURATION: u8 = 0x04;
|
||||||
pub const GET_PUBKEY: u8 = 0x05;
|
pub const GET_PUBKEY: u8 = 0x05;
|
||||||
pub const SIGN_MESSAGE: u8 = 0x06;
|
pub const SIGN_MESSAGE: u8 = 0x06;
|
||||||
|
pub const SIGN_OFFCHAIN_MESSAGE: u8 = 0x07;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConfigurationVersion {
|
enum ConfigurationVersion {
|
||||||
|
@ -430,6 +431,13 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
|
||||||
derivation_path: &DerivationPath,
|
derivation_path: &DerivationPath,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<Signature, RemoteWalletError> {
|
) -> Result<Signature, RemoteWalletError> {
|
||||||
|
// If the first byte of the data is 0xff then it is an off-chain message
|
||||||
|
// because it starts with the Domain Specifier b"\xffsolana offchain".
|
||||||
|
// On-chain messages, in contrast, start with either 0x80 (MESSAGE_VERSION_PREFIX)
|
||||||
|
// or the number of signatures (0x00 - 0x13).
|
||||||
|
if !data.is_empty() && data[0] == 0xff {
|
||||||
|
return self.sign_offchain_message(derivation_path, data);
|
||||||
|
}
|
||||||
let mut payload = if self.outdated_app() {
|
let mut payload = if self.outdated_app() {
|
||||||
extend_and_serialize(derivation_path)
|
extend_and_serialize(derivation_path)
|
||||||
} else {
|
} else {
|
||||||
|
@ -498,7 +506,16 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
|
||||||
chunks.last_mut().unwrap().0 &= !P2_MORE;
|
chunks.last_mut().unwrap().0 &= !P2_MORE;
|
||||||
|
|
||||||
for (p2, payload) in chunks {
|
for (p2, payload) in chunks {
|
||||||
result = self.send_apdu(commands::SIGN_MESSAGE, p1, p2, &payload)?;
|
result = self.send_apdu(
|
||||||
|
if self.outdated_app() {
|
||||||
|
commands::DEPRECATED_SIGN_MESSAGE
|
||||||
|
} else {
|
||||||
|
commands::SIGN_MESSAGE
|
||||||
|
},
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
&payload,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,6 +526,42 @@ impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
|
||||||
}
|
}
|
||||||
Ok(Signature::new(&result))
|
Ok(Signature::new(&result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_offchain_message(
|
||||||
|
&self,
|
||||||
|
derivation_path: &DerivationPath,
|
||||||
|
message: &[u8],
|
||||||
|
) -> Result<Signature, RemoteWalletError> {
|
||||||
|
if message.len()
|
||||||
|
> solana_sdk::offchain_message::v0::OffchainMessage::MAX_LEN_LEDGER
|
||||||
|
+ solana_sdk::offchain_message::v0::OffchainMessage::HEADER_LEN
|
||||||
|
{
|
||||||
|
return Err(RemoteWalletError::InvalidInput(
|
||||||
|
"Off-chain message to sign is too long".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = extend_and_serialize_multiple(&[derivation_path]);
|
||||||
|
data.extend_from_slice(message);
|
||||||
|
|
||||||
|
let p1 = P1_CONFIRM;
|
||||||
|
let mut p2 = 0;
|
||||||
|
let mut payload = data.as_slice();
|
||||||
|
while payload.len() > MAX_CHUNK_SIZE {
|
||||||
|
let chunk = &payload[..MAX_CHUNK_SIZE];
|
||||||
|
self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2 | P2_MORE, chunk)?;
|
||||||
|
payload = &payload[MAX_CHUNK_SIZE..];
|
||||||
|
p2 |= P2_EXTEND;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2, payload)?;
|
||||||
|
if result.len() != 64 {
|
||||||
|
return Err(RemoteWalletError::Protocol(
|
||||||
|
"Signature packet size mismatch",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Signature::new(&result))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the detected device is a valid `Ledger device` by checking both the product ID and the vendor ID
|
/// Check if the detected device is a valid `Ledger device` by checking both the product ID and the vendor ID
|
||||||
|
|
|
@ -71,6 +71,15 @@ pub enum LedgerError {
|
||||||
#[error("Ledger received invalid Solana message")]
|
#[error("Ledger received invalid Solana message")]
|
||||||
SolanaInvalidMessage = 0x6a80,
|
SolanaInvalidMessage = 0x6a80,
|
||||||
|
|
||||||
|
#[error("Ledger received message with invalid header")]
|
||||||
|
SolanaInvalidMessageHeader = 0x6a81,
|
||||||
|
|
||||||
|
#[error("Ledger received message in invalid format")]
|
||||||
|
SolanaInvalidMessageFormat = 0x6a82,
|
||||||
|
|
||||||
|
#[error("Ledger received message with invalid size")]
|
||||||
|
SolanaInvalidMessageSize = 0x6a83,
|
||||||
|
|
||||||
#[error("Solana summary finalization failed on Ledger device")]
|
#[error("Solana summary finalization failed on Ledger device")]
|
||||||
SolanaSummaryFinalizeFailed = 0x6f00,
|
SolanaSummaryFinalizeFailed = 0x6f00,
|
||||||
|
|
||||||
|
|
|
@ -236,6 +236,15 @@ pub trait RemoteWallet<T> {
|
||||||
) -> Result<Signature, RemoteWalletError> {
|
) -> Result<Signature, RemoteWalletError> {
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sign off-chain message with wallet managing pubkey at derivation path m/44'/501'/<account>'/<change>'.
|
||||||
|
fn sign_offchain_message(
|
||||||
|
&self,
|
||||||
|
derivation_path: &DerivationPath,
|
||||||
|
message: &[u8],
|
||||||
|
) -> Result<Signature, RemoteWalletError> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `RemoteWallet` device
|
/// `RemoteWallet` device
|
||||||
|
|
|
@ -60,6 +60,7 @@ log = "0.4.17"
|
||||||
memmap2 = { version = "0.5.3", optional = true }
|
memmap2 = { version = "0.5.3", optional = true }
|
||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
num_enum = "0.5.7"
|
||||||
pbkdf2 = { version = "0.11.0", default-features = false }
|
pbkdf2 = { version = "0.11.0", default-features = false }
|
||||||
qstring = "0.7.2"
|
qstring = "0.7.2"
|
||||||
rand = { version = "0.7.0", optional = true }
|
rand = { version = "0.7.0", optional = true }
|
||||||
|
|
|
@ -47,6 +47,7 @@ pub mod inflation;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
pub mod native_loader;
|
pub mod native_loader;
|
||||||
pub mod nonce_account;
|
pub mod nonce_account;
|
||||||
|
pub mod offchain_message;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
pub mod poh_config;
|
pub mod poh_config;
|
||||||
pub mod precompiles;
|
pub mod precompiles;
|
||||||
|
|
|
@ -0,0 +1,302 @@
|
||||||
|
//! Off-Chain Message container for storing non-transaction messages.
|
||||||
|
|
||||||
|
#![cfg(feature = "full")]
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if !(0x20..=0x7e).contains(&char) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if given bytes contain valid UTF8 string
|
||||||
|
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, MessageFormat, OffchainMessage as Base},
|
||||||
|
crate::{
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
packet::PACKET_DATA_SIZE,
|
||||||
|
sanitize::SanitizeError,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// OffchainMessage Version 0.
|
||||||
|
/// Struct always contains a non-empty valid message.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct OffchainMessage {
|
||||||
|
format: MessageFormat,
|
||||||
|
message: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OffchainMessage {
|
||||||
|
// Header Length = Message Format (1) + Message Length (2)
|
||||||
|
pub const HEADER_LEN: usize = 3;
|
||||||
|
// 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 = 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) {
|
||||||
|
MessageFormat::RestrictedAscii
|
||||||
|
} else if is_utf8(message) {
|
||||||
|
MessageFormat::LimitedUtf8
|
||||||
|
} else {
|
||||||
|
return Err(SanitizeError::InvalidValue);
|
||||||
|
}
|
||||||
|
} else if message.len() <= OffchainMessage::MAX_LEN {
|
||||||
|
if is_utf8(message) {
|
||||||
|
MessageFormat::ExtendedUtf8
|
||||||
|
} else {
|
||||||
|
return Err(SanitizeError::InvalidValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(SanitizeError::ValueOutOfBounds);
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
format,
|
||||||
|
message: message.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.message.is_empty() && self.message.len() <= Self::MAX_LEN);
|
||||||
|
data.reserve(Self::HEADER_LEN.saturating_add(self.message.len()));
|
||||||
|
// format
|
||||||
|
data.push(self.format.into());
|
||||||
|
// message length
|
||||||
|
data.extend_from_slice(&(self.message.len() as u16).to_le_bytes());
|
||||||
|
// message
|
||||||
|
data.extend_from_slice(&self.message);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize the message from bytes that include a full header
|
||||||
|
pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
|
||||||
|
// validate data length
|
||||||
|
if data.len() <= Self::HEADER_LEN || data.len() > Self::HEADER_LEN + Self::MAX_LEN {
|
||||||
|
return Err(SanitizeError::ValueOutOfBounds);
|
||||||
|
}
|
||||||
|
// decode header
|
||||||
|
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 Self::HEADER_LEN.saturating_add(message_len) != data.len() {
|
||||||
|
return Err(SanitizeError::InvalidValue);
|
||||||
|
}
|
||||||
|
let message = &data[Self::HEADER_LEN..];
|
||||||
|
// check format
|
||||||
|
let is_valid = match format {
|
||||||
|
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 {
|
||||||
|
Ok(Self {
|
||||||
|
format,
|
||||||
|
message: message.to_vec(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(SanitizeError::InvalidValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the SHA256 hash of the serialized off-chain message
|
||||||
|
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) -> MessageFormat {
|
||||||
|
self.format
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_message(&self) -> &Vec<u8> {
|
||||||
|
&self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum OffchainMessage {
|
||||||
|
V0(v0::OffchainMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OffchainMessage {
|
||||||
|
pub const SIGNING_DOMAIN: &'static [u8] = b"\xffsolana offchain";
|
||||||
|
// Header Length = Signing Domain (16) + Header Version (1)
|
||||||
|
pub const HEADER_LEN: usize = Self::SIGNING_DOMAIN.len() + 1;
|
||||||
|
|
||||||
|
/// Construct a new OffchainMessage object from the given version and message
|
||||||
|
pub fn new(version: u8, message: &[u8]) -> Result<Self, SanitizeError> {
|
||||||
|
match version {
|
||||||
|
0 => Ok(Self::V0(v0::OffchainMessage::new(message)?)),
|
||||||
|
_ => Err(SanitizeError::ValueOutOfBounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the off-chain message to bytes including full header
|
||||||
|
pub fn serialize(&self) -> Result<Vec<u8>, SanitizeError> {
|
||||||
|
// serialize signing domain
|
||||||
|
let mut data = Self::SIGNING_DOMAIN.to_vec();
|
||||||
|
|
||||||
|
// serialize version and call version specific serializer
|
||||||
|
match self {
|
||||||
|
Self::V0(msg) => {
|
||||||
|
data.push(0);
|
||||||
|
msg.serialize(&mut data)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize the off-chain message from bytes that include full header
|
||||||
|
pub fn deserialize(data: &[u8]) -> Result<Self, SanitizeError> {
|
||||||
|
if data.len() <= Self::HEADER_LEN {
|
||||||
|
return Err(SanitizeError::ValueOutOfBounds);
|
||||||
|
}
|
||||||
|
let version = data[Self::SIGNING_DOMAIN.len()];
|
||||||
|
let data = &data[Self::SIGNING_DOMAIN.len().saturating_add(1)..];
|
||||||
|
match version {
|
||||||
|
0 => Ok(Self::V0(v0::OffchainMessage::deserialize(data)?)),
|
||||||
|
_ => Err(SanitizeError::ValueOutOfBounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the hash of the off-chain message
|
||||||
|
pub fn hash(&self) -> Result<Hash, SanitizeError> {
|
||||||
|
match self {
|
||||||
|
Self::V0(_) => v0::OffchainMessage::hash(&self.serialize()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::V0(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_format(&self) -> MessageFormat {
|
||||||
|
match self {
|
||||||
|
Self::V0(msg) => msg.get_format(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_message(&self) -> &Vec<u8> {
|
||||||
|
match self {
|
||||||
|
Self::V0(msg) => msg.get_message(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign the message with provided keypair
|
||||||
|
pub fn sign(&self, signer: &dyn Signer) -> Result<Signature, SanitizeError> {
|
||||||
|
Ok(signer.sign_message(&self.serialize()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that the message signature is valid for the given public key
|
||||||
|
pub fn verify(&self, signer: &Pubkey, signature: &Signature) -> Result<bool, SanitizeError> {
|
||||||
|
Ok(signature.verify(signer.as_ref(), &self.serialize()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {super::*, crate::signature::Keypair, std::str::FromStr};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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(), MessageFormat::RestrictedAscii);
|
||||||
|
assert_eq!(message.get_message().as_slice(), b"Test Message");
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
let hash = Hash::from_str("HG5JydBGjtjTfD3sSn21ys5NTWPpXzmqifiGC2BVUjkD").unwrap();
|
||||||
|
assert_eq!(message.serialize().unwrap(), serialized);
|
||||||
|
assert_eq!(message.hash().unwrap(), hash);
|
||||||
|
assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offchain_message_utf8() {
|
||||||
|
let message = OffchainMessage::new(0, "Тестовое сообщение".as_bytes()).unwrap();
|
||||||
|
assert_eq!(message.get_version(), 0);
|
||||||
|
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() == 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,
|
||||||
|
209, 129, 208, 190, 208, 190, 208, 177, 209, 137, 208, 181, 208, 189, 208, 184, 208,
|
||||||
|
181,
|
||||||
|
];
|
||||||
|
let hash = Hash::from_str("6GXTveatZQLexkX4WeTpJ3E7uk1UojRXpKp43c4ArSun").unwrap();
|
||||||
|
assert_eq!(message.serialize().unwrap(), serialized);
|
||||||
|
assert_eq!(message.hash().unwrap(), hash);
|
||||||
|
assert_eq!(message, OffchainMessage::deserialize(&serialized).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_offchain_message_sign_and_verify() {
|
||||||
|
let message = OffchainMessage::new(0, b"Test Message").unwrap();
|
||||||
|
let keypair = Keypair::new();
|
||||||
|
let signature = message.sign(&keypair).unwrap();
|
||||||
|
assert!(message.verify(&keypair.pubkey(), &signature).unwrap());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue