From 2744ce86e8b510e9fd1fc66bdb45bb2d909f7dda Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Wed, 28 Jan 2026 16:43:04 +0300 Subject: [PATCH 1/3] feat: use signature domain instead of signature id for signatures --- nekoton-abi/src/abi_helpers.rs | 1 + src/core/keystore/mod.rs | 6 +-- src/core/ton_wallet/wallet_v5r1.rs | 22 +++++--- src/crypto/derived_key/mod.rs | 17 +++--- src/crypto/encrypted_key/mod.rs | 27 +++++----- src/crypto/ledger_key/mod.rs | 9 ++-- src/crypto/mod.rs | 28 +++++----- src/crypto/signature_domain/mod.rs | 84 ++++++++++++++++++++++++++++++ src/external/mod.rs | 6 ++- 9 files changed, 148 insertions(+), 52 deletions(-) create mode 100644 src/crypto/signature_domain/mod.rs diff --git a/nekoton-abi/src/abi_helpers.rs b/nekoton-abi/src/abi_helpers.rs index d84316448..5be15ba69 100644 --- a/nekoton-abi/src/abi_helpers.rs +++ b/nekoton-abi/src/abi_helpers.rs @@ -4,6 +4,7 @@ use ton_types::UInt256; use super::{BuildTokenValue, KnownParamType, UnpackerError, UnpackerResult}; +#[derive(Clone, Debug)] pub struct BigUint128(pub BigUint); impl BuildTokenValue for BigUint128 { diff --git a/src/core/keystore/mod.rs b/src/core/keystore/mod.rs index be7c1562c..6d13dd363 100644 --- a/src/core/keystore/mod.rs +++ b/src/core/keystore/mod.rs @@ -14,7 +14,7 @@ use tokio::sync::RwLock; use nekoton_utils::*; use crate::crypto::{ - EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, SignatureId, + EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, SignatureDomain, Signer, SignerContext, SignerEntry, SignerStorage, }; use crate::external::Storage; @@ -290,7 +290,7 @@ impl KeyStore { pub async fn sign( &self, data: &[u8], - signature_id: Option, + signature_domain: SignatureDomain, input: T::SignInput, ) -> Result where @@ -303,7 +303,7 @@ impl KeyStore { }; state .get_signer_ref::()? - .sign(ctx, data, signature_id, input) + .sign(ctx, data, signature_domain, input) .await } diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index 0ea1444b0..76d4f5e6d 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -385,7 +385,7 @@ mod tests { use crate::core::ton_wallet::wallet_v5r1::{ compute_contract_address, is_wallet_v5r1, InitData, WALLET_ID, }; - use crate::crypto::extend_with_signature_id; + use crate::crypto::{SignatureDomain, ToSign}; use ed25519_dalek::{PublicKey, Signature, Verifier}; use nekoton_contracts::wallets; use ton_block::AccountState; @@ -438,7 +438,12 @@ mod tests { let public_key = PublicKey::from_bytes(public_key_bytes.as_slice())?; - let result = check_signature(in_msg_body_slice, public_key, Some(2000))?; + let result = check_signature( + in_msg_body_slice, + public_key, + SignatureDomain::L2 { global_id: 2000 }, + false, + )?; assert!(result); Ok(()) } @@ -446,7 +451,8 @@ mod tests { fn check_signature( mut in_msg_body: SliceData, public_key: PublicKey, - signature_id: Option, + signature_domain: SignatureDomain, + enable_signature_domains: bool, ) -> anyhow::Result { let signature_binding = in_msg_body .get_slice(in_msg_body.remaining_bits() - 512, 512)? @@ -458,11 +464,15 @@ mod tests { .into_cell(); let hash = payload.repr_hash(); - - let data = extend_with_signature_id(hash.as_ref(), signature_id); + let to_sign = ToSign { + enable_signature_domains, + signature_domain, + data: hash.into_vec(), + }; + let data = to_sign.write_to_bytes(); Ok(public_key - .verify(&*data, &Signature::from_bytes(sig)?) + .verify(&data, &Signature::from_bytes(sig)?) .is_ok()) } } diff --git a/src/crypto/derived_key/mod.rs b/src/crypto/derived_key/mod.rs index 05e25ec15..3289852f4 100644 --- a/src/crypto/derived_key/mod.rs +++ b/src/crypto/derived_key/mod.rs @@ -2,18 +2,17 @@ use std::collections::hash_map::{self, HashMap}; use anyhow::Result; use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce}; -use ed25519_dalek::{Keypair, PublicKey, Signer}; +use ed25519_dalek::{Keypair, PublicKey}; use secstr::SecUtf8; use serde::{Deserialize, Serialize, Serializer}; -use nekoton_utils::*; - use super::mnemonic::*; use super::{ - default_key_name, extend_with_signature_id, Password, PasswordCache, PasswordCacheTransaction, - PubKey, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, Password, PasswordCache, PasswordCacheTransaction, PubKey, SharedSecret, + Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; +use crate::crypto::signature_domain::SignatureDomain; +use nekoton_utils::*; #[derive(Default, Clone, Debug, Eq, PartialEq)] pub struct DerivedKeySigner { @@ -355,12 +354,12 @@ impl StoreSigner for DerivedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_domain: SignatureDomain, input: Self::SignInput, ) -> Result<[u8; 64]> { let keypair = self.use_sign_input(ctx.password_cache, input)?; - let data = extend_with_signature_id(data, signature_id); - Ok(keypair.sign(&data).to_bytes()) + let signature = signature_domain.sign(&keypair, data); + Ok(signature.to_bytes()) } } diff --git a/src/crypto/encrypted_key/mod.rs b/src/crypto/encrypted_key/mod.rs index 2631aee97..58170ec84 100644 --- a/src/crypto/encrypted_key/mod.rs +++ b/src/crypto/encrypted_key/mod.rs @@ -4,19 +4,18 @@ use std::io::Read; use anyhow::Result; use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; -use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signer}; +use ed25519_dalek::{Keypair, PublicKey, SecretKey}; use rand::Rng; use secstr::SecUtf8; use serde::{Deserialize, Serialize}; -use nekoton_utils::*; - use super::mnemonic::*; use super::{ - default_key_name, extend_with_signature_id, Password, PasswordCache, PasswordCacheTransaction, - PubKey, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, signature_domain::SignatureDomain, Password, PasswordCache, + PasswordCacheTransaction, PubKey, SharedSecret, Signer as StoreSigner, SignerContext, + SignerEntry, SignerStorage, }; +use nekoton_utils::*; #[derive(Default, Clone, Debug, Eq, PartialEq)] pub struct EncryptedKeySigner { @@ -194,7 +193,7 @@ impl StoreSigner for EncryptedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_domain: SignatureDomain, input: Self::SignInput, ) -> Result<[u8; 64]> { let key = self.get_key(&input.public_key)?; @@ -203,7 +202,7 @@ impl StoreSigner for EncryptedKeySigner { .password_cache .process_password(input.public_key.to_bytes(), input.password)?; - let signature = key.sign(data, signature_id, password.as_ref())?; + let signature = key.sign(signature_domain, data, password.as_ref())?; password.proceed(); Ok(signature) @@ -470,11 +469,11 @@ impl EncryptedKey { pub fn sign( &self, + signature_domain: SignatureDomain, data: &[u8], - signature_id: Option, password: &str, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { - self.inner.sign(data, signature_id, password) + self.inner.sign(signature_domain, data, password) } pub fn compute_shared_keys( @@ -531,8 +530,8 @@ struct CryptoData { impl CryptoData { pub fn sign( &self, + signature_domain: SignatureDomain, data: &[u8], - signature_id: Option, password: &str, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let secret = self.decrypt_secret(password)?; @@ -540,8 +539,8 @@ impl CryptoData { secret, public: self.pubkey, }; - let data = extend_with_signature_id(data, signature_id); - Ok(pair.sign(&data).to_bytes()) + let signature = signature_domain.sign(&pair, data); + Ok(signature.to_bytes()) } pub fn compute_shared_keys( @@ -705,7 +704,7 @@ mod tests { .unwrap(); assert!(!signer.as_json().is_empty()); - let result = signer.sign(b"lol", None, "lol"); + let result = signer.sign(SignatureDomain::Empty, b"lol", "lol"); assert!(result.is_err()); } diff --git a/src/crypto/ledger_key/mod.rs b/src/crypto/ledger_key/mod.rs index 91aca3cdb..b78a81814 100644 --- a/src/crypto/ledger_key/mod.rs +++ b/src/crypto/ledger_key/mod.rs @@ -10,10 +10,11 @@ use serde::{Deserialize, Serialize}; use nekoton_utils::*; use super::{ - default_key_name, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, + default_key_name, SharedSecret, Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; use crate::core::ton_wallet::WalletType; +use crate::crypto::signature_domain::SignatureDomain; use crate::external::{LedgerConnection, LedgerSignatureContext}; #[derive(Clone)] @@ -150,14 +151,14 @@ impl StoreSigner for LedgerKeySigner { &self, _: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_domain: SignatureDomain, input: Self::SignInput, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let key = self.get_key(&input.public_key)?; let signature = match input.context { None => { self.connection - .sign(key.account_id, signature_id, data) + .sign(key.account_id, signature_domain, data) .await? } Some(context) => { @@ -165,7 +166,7 @@ impl StoreSigner for LedgerKeySigner { .sign_transaction( key.account_id, input.wallet.try_into()?, - signature_id, + signature_domain, data, &context, ) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 7deef6f40..a29c10853 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use anyhow::Result; use downcast_rs::{impl_downcast, Downcast}; use dyn_clone::DynClone; @@ -15,12 +13,14 @@ pub use encrypted_key::*; pub use ledger_key::*; pub use mnemonic::*; pub use password_cache::*; +pub use signature_domain::*; mod derived_key; mod encrypted_key; mod ledger_key; mod mnemonic; mod password_cache; +mod signature_domain; pub type Signature = [u8; ed25519_dalek::SIGNATURE_LENGTH]; pub type PubKey = [u8; ed25519_dalek::PUBLIC_KEY_LENGTH]; @@ -166,7 +166,7 @@ pub trait Signer: SignerStorage { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_domain: SignatureDomain, input: Self::SignInput, ) -> Result; } @@ -240,17 +240,17 @@ pub fn default_key_name(public_key: &PubKey) -> String { ) } -pub fn extend_with_signature_id(data: &[u8], signature_id: Option) -> Cow<'_, [u8]> { - match signature_id { - Some(signature_id) => { - let mut extended_data = Vec::with_capacity(4 + data.len()); - extended_data.extend_from_slice(&signature_id.to_be_bytes()); - extended_data.extend_from_slice(data); - Cow::Owned(extended_data) - } - None => Cow::Borrowed(data), - } -} +// pub fn extend_with_signature_id(data: &[u8], signature_id: Option) -> Cow<'_, [u8]> { +// match signature_id { +// Some(signature_id) => { +// let mut extended_data = Vec::with_capacity(4 + data.len()); +// extended_data.extend_from_slice(&signature_id.to_be_bytes()); +// extended_data.extend_from_slice(data); +// Cow::Owned(extended_data) +// } +// None => Cow::Borrowed(data), +// } +// } pub mod x25519 { use curve25519_dalek_ng::scalar::Scalar; diff --git a/src/crypto/signature_domain/mod.rs b/src/crypto/signature_domain/mod.rs new file mode 100644 index 000000000..bffd8a611 --- /dev/null +++ b/src/crypto/signature_domain/mod.rs @@ -0,0 +1,84 @@ +use ed25519_dalek::Signer; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +pub struct ToSign { + pub enable_signature_domains: bool, + pub signature_domain: SignatureDomain, + pub data: Vec, +} + +impl ToSign { + pub fn write_to_bytes(&self) -> Vec { + let mut output = Vec::new(); + + match self.signature_domain { + // Empty signature domain always doesn't have any prefix. + SignatureDomain::Empty => {} + // All other signature domains are prefixed as hash. + _ if self.enable_signature_domains => { + output.extend_from_slice(&self.signature_domain.get_tl_hash()); + } + // Fallback for the original `SignatureWithId` implementation + // if domains are disabled. + SignatureDomain::L2 { global_id } => output.extend_from_slice(&global_id.to_be_bytes()), + } + + output.extend_from_slice(&self.data); + + output + } +} + +/// Signature domain variants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignatureDomain { + /// Special variant to NOT add any prefix for the verified data. + /// Can be used to verify mainnet signatures from L2 networks. + Empty, + /// Non-empty variant. Hash of its TL representation + /// is used as a prefix for the verified data. + L2 { + /// Global id of the network. + global_id: i32, + }, +} + +impl SignatureDomain { + /// Signs arbitrary data using the key and optional signature id. + pub fn sign(&self, key: &ed25519_dalek::Keypair, data: &[u8]) -> ed25519_dalek::Signature { + let data = self.apply(data); + key.sign(&data) + } + /// Prepares arbitrary data for signing. + pub fn apply<'a>(&self, data: &'a [u8]) -> Cow<'a, [u8]> { + if let Self::Empty = self { + Cow::Borrowed(data) + } else { + let hash = self.get_tl_hash(); + let mut result = Vec::with_capacity(32 + data.len()); + result.extend_from_slice(&hash); + result.extend_from_slice(data); + Cow::Owned(result) + } + } + + fn get_tl_hash(&self) -> Vec { + Sha256::digest(self.write_to_bytes()).to_vec() + } + + fn write_to_bytes(&self) -> Vec { + let mut data = Vec::new(); + match self { + Self::Empty => { + data.extend_from_slice(&0xe1d571bu32.to_le_bytes()); //Empty variant tl tag + } + Self::L2 { global_id } => { + data.extend_from_slice(&0x71b34ee1u32.to_le_bytes()); // L2 variant tl tag + data.extend_from_slice(&global_id.to_le_bytes()); + } + } + + data + } +} diff --git a/src/external/mod.rs b/src/external/mod.rs index 6264ca719..e7763b4e2 100644 --- a/src/external/mod.rs +++ b/src/external/mod.rs @@ -2,6 +2,8 @@ use anyhow::Result; use nekoton_utils::serde_optional_hex_array; use serde::{Deserialize, Serialize}; +use super::crypto::SignatureDomain; + #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] #[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] pub trait Storage: Sync + Send { @@ -87,7 +89,7 @@ pub trait LedgerConnection: Send + Sync { async fn sign( &self, account: u16, - signature_id: Option, + signature_domain: SignatureDomain, message: &[u8], ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; @@ -95,7 +97,7 @@ pub trait LedgerConnection: Send + Sync { &self, account: u16, wallet: u16, - signature_id: Option, + signature_id: SignatureDomain, message: &[u8], context: &LedgerSignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; From 63d593f6d319c57ec7a2535ab9aae68a3a10d21d Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Thu, 29 Jan 2026 16:41:25 +0300 Subject: [PATCH 2/3] feat: use signature context to determine which signature scheme should be used --- src/core/keystore/mod.rs | 6 +-- src/core/ton_wallet/wallet_v5r1.rs | 14 ++--- src/crypto/derived_key/mod.rs | 7 ++- src/crypto/encrypted_key/mod.rs | 25 +++++---- src/crypto/ledger_key/mod.rs | 11 ++-- src/crypto/mod.rs | 14 +---- src/crypto/signature_domain/mod.rs | 87 +++++++++++++++++++++++------- src/external/mod.rs | 6 +-- 8 files changed, 104 insertions(+), 66 deletions(-) diff --git a/src/core/keystore/mod.rs b/src/core/keystore/mod.rs index 6d13dd363..a8afad69b 100644 --- a/src/core/keystore/mod.rs +++ b/src/core/keystore/mod.rs @@ -14,7 +14,7 @@ use tokio::sync::RwLock; use nekoton_utils::*; use crate::crypto::{ - EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, SignatureDomain, + EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, SignatureContext, Signer, SignerContext, SignerEntry, SignerStorage, }; use crate::external::Storage; @@ -290,7 +290,7 @@ impl KeyStore { pub async fn sign( &self, data: &[u8], - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, input: T::SignInput, ) -> Result where @@ -303,7 +303,7 @@ impl KeyStore { }; state .get_signer_ref::()? - .sign(ctx, data, signature_domain, input) + .sign(ctx, data, signature_ctx, input) .await } diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index 76d4f5e6d..c02badcc2 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -385,7 +385,7 @@ mod tests { use crate::core::ton_wallet::wallet_v5r1::{ compute_contract_address, is_wallet_v5r1, InitData, WALLET_ID, }; - use crate::crypto::{SignatureDomain, ToSign}; + use crate::crypto::{SignatureContext, SignatureType, ToSign}; use ed25519_dalek::{PublicKey, Signature, Verifier}; use nekoton_contracts::wallets; use ton_block::AccountState; @@ -441,8 +441,10 @@ mod tests { let result = check_signature( in_msg_body_slice, public_key, - SignatureDomain::L2 { global_id: 2000 }, - false, + SignatureContext { + global_id: Some(2000), + signature_type: SignatureType::SignatureId, + }, )?; assert!(result); Ok(()) @@ -451,8 +453,7 @@ mod tests { fn check_signature( mut in_msg_body: SliceData, public_key: PublicKey, - signature_domain: SignatureDomain, - enable_signature_domains: bool, + ctx: SignatureContext, ) -> anyhow::Result { let signature_binding = in_msg_body .get_slice(in_msg_body.remaining_bits() - 512, 512)? @@ -465,8 +466,7 @@ mod tests { let hash = payload.repr_hash(); let to_sign = ToSign { - enable_signature_domains, - signature_domain, + ctx, data: hash.into_vec(), }; let data = to_sign.write_to_bytes(); diff --git a/src/crypto/derived_key/mod.rs b/src/crypto/derived_key/mod.rs index 3289852f4..17ec21d0d 100644 --- a/src/crypto/derived_key/mod.rs +++ b/src/crypto/derived_key/mod.rs @@ -9,9 +9,8 @@ use serde::{Deserialize, Serialize, Serializer}; use super::mnemonic::*; use super::{ default_key_name, Password, PasswordCache, PasswordCacheTransaction, PubKey, SharedSecret, - Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, + SignatureContext, Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; -use crate::crypto::signature_domain::SignatureDomain; use nekoton_utils::*; #[derive(Default, Clone, Debug, Eq, PartialEq)] @@ -354,11 +353,11 @@ impl StoreSigner for DerivedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; 64]> { let keypair = self.use_sign_input(ctx.password_cache, input)?; - let signature = signature_domain.sign(&keypair, data); + let signature = signature_ctx.sign(&keypair, data); Ok(signature.to_bytes()) } } diff --git a/src/crypto/encrypted_key/mod.rs b/src/crypto/encrypted_key/mod.rs index 58170ec84..ce951d1ec 100644 --- a/src/crypto/encrypted_key/mod.rs +++ b/src/crypto/encrypted_key/mod.rs @@ -11,9 +11,8 @@ use serde::{Deserialize, Serialize}; use super::mnemonic::*; use super::{ - default_key_name, signature_domain::SignatureDomain, Password, PasswordCache, - PasswordCacheTransaction, PubKey, SharedSecret, Signer as StoreSigner, SignerContext, - SignerEntry, SignerStorage, + default_key_name, Password, PasswordCache, PasswordCacheTransaction, PubKey, SharedSecret, + SignatureContext, Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; use nekoton_utils::*; @@ -193,7 +192,7 @@ impl StoreSigner for EncryptedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; 64]> { let key = self.get_key(&input.public_key)?; @@ -202,7 +201,7 @@ impl StoreSigner for EncryptedKeySigner { .password_cache .process_password(input.public_key.to_bytes(), input.password)?; - let signature = key.sign(signature_domain, data, password.as_ref())?; + let signature = key.sign(data, password.as_ref(), signature_ctx)?; password.proceed(); Ok(signature) @@ -469,11 +468,11 @@ impl EncryptedKey { pub fn sign( &self, - signature_domain: SignatureDomain, data: &[u8], password: &str, + signature_ctx: SignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { - self.inner.sign(signature_domain, data, password) + self.inner.sign(data, password, signature_ctx) } pub fn compute_shared_keys( @@ -530,16 +529,16 @@ struct CryptoData { impl CryptoData { pub fn sign( &self, - signature_domain: SignatureDomain, data: &[u8], password: &str, + signature_ctx: SignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let secret = self.decrypt_secret(password)?; let pair = Keypair { secret, public: self.pubkey, }; - let signature = signature_domain.sign(&pair, data); + let signature = signature_ctx.sign(&pair, data); Ok(signature.to_bytes()) } @@ -663,7 +662,7 @@ mod tests { use std::time::Duration; use super::*; - use crate::crypto::PasswordCacheBehavior; + use crate::crypto::{PasswordCacheBehavior, SignatureType}; const TEST_PASSWORD: &str = "123"; const TEST_MNEMONIC: &str = "canyon stage apple useful bench lazy grass enact canvas like figure help pave reopen betray exotic nose fetch wagon senior acid across salon alley"; @@ -704,7 +703,11 @@ mod tests { .unwrap(); assert!(!signer.as_json().is_empty()); - let result = signer.sign(SignatureDomain::Empty, b"lol", "lol"); + let sig_ctx = SignatureContext { + global_id: None, + signature_type: SignatureType::Empty, + }; + let result = signer.sign(b"lol", "lol", sig_ctx); assert!(result.is_err()); } diff --git a/src/crypto/ledger_key/mod.rs b/src/crypto/ledger_key/mod.rs index b78a81814..158ea3f68 100644 --- a/src/crypto/ledger_key/mod.rs +++ b/src/crypto/ledger_key/mod.rs @@ -10,11 +10,10 @@ use serde::{Deserialize, Serialize}; use nekoton_utils::*; use super::{ - default_key_name, SharedSecret, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, SharedSecret, SignatureContext, Signer as StoreSigner, SignerContext, + SignerEntry, SignerStorage, }; use crate::core::ton_wallet::WalletType; -use crate::crypto::signature_domain::SignatureDomain; use crate::external::{LedgerConnection, LedgerSignatureContext}; #[derive(Clone)] @@ -151,14 +150,14 @@ impl StoreSigner for LedgerKeySigner { &self, _: SignerContext<'_>, data: &[u8], - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let key = self.get_key(&input.public_key)?; let signature = match input.context { None => { self.connection - .sign(key.account_id, signature_domain, data) + .sign(key.account_id, signature_ctx, data) .await? } Some(context) => { @@ -166,7 +165,7 @@ impl StoreSigner for LedgerKeySigner { .sign_transaction( key.account_id, input.wallet.try_into()?, - signature_domain, + signature_ctx, data, &context, ) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index a29c10853..c67a07758 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -166,7 +166,7 @@ pub trait Signer: SignerStorage { &self, ctx: SignerContext<'_>, data: &[u8], - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result; } @@ -240,18 +240,6 @@ pub fn default_key_name(public_key: &PubKey) -> String { ) } -// pub fn extend_with_signature_id(data: &[u8], signature_id: Option) -> Cow<'_, [u8]> { -// match signature_id { -// Some(signature_id) => { -// let mut extended_data = Vec::with_capacity(4 + data.len()); -// extended_data.extend_from_slice(&signature_id.to_be_bytes()); -// extended_data.extend_from_slice(data); -// Cow::Owned(extended_data) -// } -// None => Cow::Borrowed(data), -// } -// } - pub mod x25519 { use curve25519_dalek_ng::scalar::Scalar; use zeroize::Zeroizing; diff --git a/src/crypto/signature_domain/mod.rs b/src/crypto/signature_domain/mod.rs index bffd8a611..ce5229e40 100644 --- a/src/crypto/signature_domain/mod.rs +++ b/src/crypto/signature_domain/mod.rs @@ -2,9 +2,9 @@ use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; use std::borrow::Cow; +#[derive(Debug, Clone)] pub struct ToSign { - pub enable_signature_domains: bool, - pub signature_domain: SignatureDomain, + pub ctx: SignatureContext, pub data: Vec, } @@ -12,16 +12,20 @@ impl ToSign { pub fn write_to_bytes(&self) -> Vec { let mut output = Vec::new(); - match self.signature_domain { - // Empty signature domain always doesn't have any prefix. - SignatureDomain::Empty => {} - // All other signature domains are prefixed as hash. - _ if self.enable_signature_domains => { - output.extend_from_slice(&self.signature_domain.get_tl_hash()); + match (self.ctx.signature_type, self.ctx.global_id) { + (SignatureType::Empty, _) => {} + (SignatureType::SignatureId, None) => {} + (SignatureType::SignatureDomain, None) => { + let sd = SignatureDomain::Empty; + output.extend_from_slice(&sd.hash()) + } + (SignatureType::SignatureDomain, Some(global_id)) => { + let sd = SignatureDomain::L2 { global_id }; + output.extend_from_slice(&sd.hash()) + } + (SignatureType::SignatureId, Some(global_id)) => { + output.extend_from_slice(&global_id.to_be_bytes()) } - // Fallback for the original `SignatureWithId` implementation - // if domains are disabled. - SignatureDomain::L2 { global_id } => output.extend_from_slice(&global_id.to_be_bytes()), } output.extend_from_slice(&self.data); @@ -30,6 +34,36 @@ impl ToSign { } } +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct SignatureContext { + pub global_id: Option, + pub signature_type: SignatureType, +} + +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub enum SignatureType { + Empty, + SignatureId, + #[default] + SignatureDomain, +} + +impl SignatureContext { + pub fn sign<'a>( + &self, + key: &ed25519_dalek::Keypair, + data: &'a [u8], + ) -> ed25519_dalek::Signature { + let data = match self.signature_type { + SignatureType::Empty => Cow::Borrowed(data), + SignatureType::SignatureId => extend_with_signature_id(data, self.global_id), + SignatureType::SignatureDomain => extend_with_signature_domain(data, self.global_id), + }; + + key.sign(&data) + } +} + /// Signature domain variants. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SignatureDomain { @@ -45,17 +79,12 @@ pub enum SignatureDomain { } impl SignatureDomain { - /// Signs arbitrary data using the key and optional signature id. - pub fn sign(&self, key: &ed25519_dalek::Keypair, data: &[u8]) -> ed25519_dalek::Signature { - let data = self.apply(data); - key.sign(&data) - } /// Prepares arbitrary data for signing. - pub fn apply<'a>(&self, data: &'a [u8]) -> Cow<'a, [u8]> { + fn apply<'a>(&self, data: &'a [u8]) -> Cow<'a, [u8]> { if let Self::Empty = self { Cow::Borrowed(data) } else { - let hash = self.get_tl_hash(); + let hash = self.hash(); let mut result = Vec::with_capacity(32 + data.len()); result.extend_from_slice(&hash); result.extend_from_slice(data); @@ -63,7 +92,7 @@ impl SignatureDomain { } } - fn get_tl_hash(&self) -> Vec { + fn hash(&self) -> Vec { Sha256::digest(self.write_to_bytes()).to_vec() } @@ -82,3 +111,23 @@ impl SignatureDomain { data } } + +fn extend_with_signature_id(data: &[u8], global_id: Option) -> Cow<'_, [u8]> { + match global_id { + Some(signature_id) => { + let mut extended_data = Vec::with_capacity(4 + data.len()); + extended_data.extend_from_slice(&signature_id.to_be_bytes()); + extended_data.extend_from_slice(data); + Cow::Owned(extended_data) + } + None => Cow::Borrowed(data), + } +} + +fn extend_with_signature_domain(data: &[u8], global_id: Option) -> Cow<'_, [u8]> { + let sd = match global_id { + None => SignatureDomain::Empty, + Some(global_id) => SignatureDomain::L2 { global_id }, + }; + sd.apply(data) +} diff --git a/src/external/mod.rs b/src/external/mod.rs index e7763b4e2..53eda4173 100644 --- a/src/external/mod.rs +++ b/src/external/mod.rs @@ -2,7 +2,7 @@ use anyhow::Result; use nekoton_utils::serde_optional_hex_array; use serde::{Deserialize, Serialize}; -use super::crypto::SignatureDomain; +use super::crypto::SignatureContext; #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] #[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] @@ -89,7 +89,7 @@ pub trait LedgerConnection: Send + Sync { async fn sign( &self, account: u16, - signature_domain: SignatureDomain, + signature_ctx: SignatureContext, message: &[u8], ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; @@ -97,7 +97,7 @@ pub trait LedgerConnection: Send + Sync { &self, account: u16, wallet: u16, - signature_id: SignatureDomain, + signature_ctx: SignatureContext, message: &[u8], context: &LedgerSignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; From 240a91668f6b4c2ae8f41771efccd84f3f9a1f69 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Mon, 9 Feb 2026 16:57:51 +0100 Subject: [PATCH 3/3] Add signature_context to NetworkCapabilities --- src/models.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/models.rs b/src/models.rs index aa6048bc1..698803d13 100644 --- a/src/models.rs +++ b/src/models.rs @@ -10,6 +10,7 @@ use nekoton_abi::*; use nekoton_utils::*; // TODO: (-_-) +use crate::crypto::{SignatureContext, SignatureDomain, SignatureType}; pub use nekoton_contracts::tip3_any::{ RootTokenContractDetails, TokenWalletDetails, TokenWalletVersion, }; @@ -411,11 +412,44 @@ pub struct NetworkCapabilities { impl NetworkCapabilities { const CAP_SIGNATURE_WITH_ID: u64 = 0x4000000; + const CAP_SIGNATURE_DOMAIN: u64 = 0x800000000; /// Returns the signature id if `CapSignatureWithId` capability is enabled. pub fn signature_id(&self) -> Option { (self.raw & Self::CAP_SIGNATURE_WITH_ID != 0).then_some(self.global_id) } + + /// Returns the signature domain type from `CapSignatureDomain` value (enabled or not) + pub fn signature_domain(&self) -> SignatureDomain { + if self.raw & Self::CAP_SIGNATURE_DOMAIN != 0 { + SignatureDomain::L2 { + global_id: self.global_id, + } + } else { + SignatureDomain::Empty + } + } + + /// Returns the signature context type which depends on enabled signature capability + pub fn signature_context(&self) -> SignatureContext { + let signature_id_enabled = self.raw & Self::CAP_SIGNATURE_WITH_ID != 0; + let signature_domain_enabled = self.raw & Self::CAP_SIGNATURE_DOMAIN != 0; + + match (signature_domain_enabled, signature_id_enabled) { + (true, _) => SignatureContext { + global_id: Some(self.global_id), + signature_type: SignatureType::SignatureDomain, + }, + (false, true) => SignatureContext { + global_id: Some(self.global_id), + signature_type: SignatureType::SignatureId, + }, + _ => SignatureContext { + global_id: None, + signature_type: SignatureType::Empty, + }, + } + } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]