diff --git a/.github/workflows/build_ledger_wallet.yml b/.github/workflows/build_ledger_wallet.yml index 8314a56943..dce5a722e0 100644 --- a/.github/workflows/build_ledger_wallet.yml +++ b/.github/workflows/build_ledger_wallet.yml @@ -15,7 +15,7 @@ name: Build minotari_ledger_wallet env: TS_FILENAME: "minotari_ledger_wallet" SHARUN: "shasum --algorithm 256" - DOCKER_IMAGE: "ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:3.43.0 " + DOCKER_IMAGE: "ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:3.51.0 " concurrency: # https://docs.github.com/en/actions/examples/using-concurrency-expressions-and-a-test-matrix diff --git a/Cargo.lock b/Cargo.lock index 1d926a1e2d..a8c74056fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2793,14 +2793,15 @@ dependencies = [ [[package]] name = "hidapi" -version = "1.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" dependencies = [ "cc", + "cfg-if", "libc", "pkg-config", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -3527,20 +3528,20 @@ dependencies = [ [[package]] name = "ledger-apdu" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +checksum = "c21ffd28d97c9252671ab2ebe7078c9fa860ff3c5a125039e174d25ec6872169" dependencies = [ "arrayref", "no-std-compat", - "snafu", + "snafu 0.8.5", ] [[package]] name = "ledger-transport" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +checksum = "c2f18de77d956a030dbc5869ced47d404bbd641216ef2f9dce7ca90833ca64ff" dependencies = [ "async-trait", "ledger-apdu", @@ -3548,8 +3549,8 @@ dependencies = [ [[package]] name = "ledger-transport" -version = "0.10.0" -source = "git+https://github.com/Zondax/ledger-rs?rev=20e2a20#20e2a2076d799d449ff6f07eb0128548b358d9bc" +version = "0.11.0" +source = "git+https://github.com/Zondax/ledger-rs?rev=4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c#4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c" dependencies = [ "async-trait", "ledger-apdu", @@ -3557,14 +3558,14 @@ dependencies = [ [[package]] name = "ledger-transport-hid" -version = "0.10.0" -source = "git+https://github.com/Zondax/ledger-rs?rev=20e2a20#20e2a2076d799d449ff6f07eb0128548b358d9bc" +version = "0.11.0" +source = "git+https://github.com/Zondax/ledger-rs?rev=4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c#4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c" dependencies = [ "byteorder", "cfg-if", "hex", "hidapi", - "ledger-transport 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ledger-transport 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", "log", "thiserror 1.0.69", @@ -4049,11 +4050,10 @@ version = "1.11.5-pre.0" dependencies = [ "borsh", "dialoguer 0.11.0", - "ledger-transport 0.10.0 (git+https://github.com/Zondax/ledger-rs?rev=20e2a20)", + "ledger-transport 0.11.0 (git+https://github.com/Zondax/ledger-rs?rev=4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c)", "ledger-transport-hid", "log", "minotari_ledger_wallet_common", - "once_cell", "rand", "semver", "serde", @@ -6520,7 +6520,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ "doc-comment", - "snafu-derive", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive 0.8.5", ] [[package]] @@ -6535,6 +6544,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "snow" version = "0.9.6" @@ -7181,7 +7202,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha3", - "snafu", + "snafu 0.7.5", "subtle", "tari_bulletproofs_plus", "tari_utilities", @@ -7461,7 +7482,7 @@ dependencies = [ "newtype-ops", "serde", "serde_json", - "snafu", + "snafu 0.7.5", "subtle", "zeroize", ] diff --git a/applications/minotari_console_wallet/src/automation/error.rs b/applications/minotari_console_wallet/src/automation/error.rs index 50e5581196..4eda54fbf1 100644 --- a/applications/minotari_console_wallet/src/automation/error.rs +++ b/applications/minotari_console_wallet/src/automation/error.rs @@ -95,11 +95,17 @@ pub enum CommandError { #[error("gRPC TLS cert error {0}")] GrpcTlsError(#[from] GrpcTlsError), #[error("Invalid signature: `{0}`")] - FailedSignature(#[from] SchnorrSignatureError), + FailedSignature(String), #[error("Tari script error: {0}")] ScriptError(#[from] ScriptError), } +impl From for CommandError { + fn from(err: SchnorrSignatureError) -> Self { + CommandError::FailedSignature(err.to_string()) + } +} + impl From for CommandError { fn from(err: HexError) -> Self { CommandError::HexError(err.to_string()) diff --git a/applications/minotari_ledger_wallet/comms/Cargo.toml b/applications/minotari_ledger_wallet/comms/Cargo.toml index d11b245f9f..b545bd8149 100644 --- a/applications/minotari_ledger_wallet/comms/Cargo.toml +++ b/applications/minotari_ledger_wallet/comms/Cargo.toml @@ -6,20 +6,19 @@ license = "BSD-3-Clause" edition = "2021" [dependencies] -tari_crypto = { version = "0.22.0", default-features = false } tari_utilities = { version = "0.8" } tari_common = { path = "../../../common" } tari_common_types = { path = "../../../base_layer/common_types" } tari_script = { path = "../../../infrastructure/tari_script" } +tari_crypto = { version = "0.22.0" } minotari_ledger_wallet_common = { path = "../common" } semver = "1.0" borsh = "1.5" dialoguer = { version = "0.11" } -ledger-transport = { git = "https://github.com/Zondax/ledger-rs", rev = "20e2a20" } -ledger-transport-hid = { git = "https://github.com/Zondax/ledger-rs", rev = "20e2a20" } +ledger-transport = { git = "https://github.com/Zondax/ledger-rs", rev = "4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c" } +ledger-transport-hid = { git = "https://github.com/Zondax/ledger-rs", rev = "4ed4cfdef0ae8b40e8997d849a3262bcf00c7d3c" } log = "0.4.20" -once_cell = "1.19.0" rand = "0.8" serde = { version = "1.0.106", features = ["derive"] } thiserror = "1.0.26" diff --git a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs index c597864997..581aad1aa7 100644 --- a/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs +++ b/applications/minotari_ledger_wallet/comms/src/accessor_methods.rs @@ -20,11 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; use log::debug; use minotari_ledger_wallet_common::common_types::{AppSW, Instruction}; -use once_cell::sync::Lazy; use rand::{rngs::OsRng, RngCore}; use semver::Version; use tari_common::configuration::Network; @@ -57,7 +56,7 @@ pub enum ScriptSignatureKey { /// Verify that the ledger application is working properly. pub fn verify_ledger_application() -> Result<(), LedgerDeviceError> { - static VERIFIED: Lazy>>> = Lazy::new(|| Mutex::new(None)); + static VERIFIED: LazyLock>>> = LazyLock::new(|| Mutex::new(None)); if let Ok(mut verified) = VERIFIED.try_lock() { if verified.is_none() { match verify() { diff --git a/applications/minotari_ledger_wallet/comms/src/error.rs b/applications/minotari_ledger_wallet/comms/src/error.rs index 5755356902..c7fdad4091 100644 --- a/applications/minotari_ledger_wallet/comms/src/error.rs +++ b/applications/minotari_ledger_wallet/comms/src/error.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use serde::{Deserialize, Serialize}; -use tari_crypto::tari_utilities::ByteArrayError; +use tari_utilities::ByteArrayError; use thiserror::Error; /// Ledger device errors. diff --git a/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs index 300d1a22d2..9c6102dd7f 100644 --- a/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs +++ b/applications/minotari_ledger_wallet/comms/src/ledger_wallet.rs @@ -20,12 +20,14 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{ops::Deref, sync::Mutex}; +use std::{ + ops::Deref, + sync::{LazyLock, Mutex}, +}; use ledger_transport::{APDUAnswer, APDUCommand}; use ledger_transport_hid::{hidapi::HidApi, TransportNativeHID}; use minotari_ledger_wallet_common::common_types::Instruction; -use once_cell::sync::Lazy; use tari_utilities::ByteArray; use crate::error::LedgerDeviceError; @@ -60,8 +62,8 @@ impl HidManager { } } -static HID_MANAGER: Lazy> = - Lazy::new(|| Mutex::new(HidManager::new().expect("Failed to initialize HidManager"))); +static HID_MANAGER: LazyLock> = + LazyLock::new(|| Mutex::new(HidManager::new().expect("Failed to initialize HidManager"))); pub fn get_transport() -> Result { let mut manager = HID_MANAGER diff --git a/applications/minotari_ledger_wallet/wallet/.cargo/config.toml b/applications/minotari_ledger_wallet/wallet/.cargo/config.toml index d8db64960b..7a04bd1e6c 100644 --- a/applications/minotari_ledger_wallet/wallet/.cargo/config.toml +++ b/applications/minotari_ledger_wallet/wallet/.cargo/config.toml @@ -6,7 +6,7 @@ target = "nanosplus" [unstable] avoid-dev-deps = true -build-std = ["core", "std", "alloc"] +build-std = ["core", "alloc"] build-std-features = ["compiler-builtins-mem"] host-config = true target-applies-to-host = true diff --git a/applications/minotari_ledger_wallet/wallet/Cargo.toml b/applications/minotari_ledger_wallet/wallet/Cargo.toml index 4dcca13ffd..6149a31c5a 100644 --- a/applications/minotari_ledger_wallet/wallet/Cargo.toml +++ b/applications/minotari_ledger_wallet/wallet/Cargo.toml @@ -6,31 +6,22 @@ license = "BSD-3-Clause" edition = "2021" [dependencies] -tari_crypto = { version = "0.22.0", default-features = false, features = [ - "borsh", -] } -tari_hashing = { path = "../../../hashing", version = "1.11.5-pre.0" } minotari_ledger_wallet_common = { path = "../common" } +tari_utilities = { version = "0.8", default-features = false } blake2 = { version = "0.10", default-features = false } borsh = { version = "1.5", default-features = false } digest = { version = "0.10", default-features = false } -include_gif = "1.0.1" -ledger_device_sdk = "1.15" +include_gif = "1.2" +ledger_device_sdk = "1.21" rand_core = { version = "0.6", default_features = false } zeroize = { version = "1", default-features = false } -# We dont directly use or call ledger_secure_sdk_sys, but it is a dependency of ledger_device_sdk, -# we want to force it to a lower version so that it can run on an older rust version -ledger_secure_sdk_sys = {version= "=1.5.3"} - -# once_cell defined here just to lock the version. Other dependencies may try to go to 1.19 which is incompatabile with -# ledger at this time. 1.19 removes "atomic-polyfill" and replaces it with "portable-atomic" which can not build due to -# target mismatches. -once_cell = { version = "=1.18.0", default-features = false } +curve25519-dalek = { version = "4", default-features = false, features = [ "alloc", "rand_core", "precomputed-tables", "zeroize"] } +subtle = { version = "2.5.0", default-features = false } [package.metadata.cargo-machete] -ignored = ["once_cell", "ledger_secure_sdk_sys"] +ignored = [] [profile.release] opt-level = 'z' @@ -61,3 +52,6 @@ icon = "key_32x32.gif" icon = "key_40x40.gif" [workspace] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("stax", "flex", "nanos", "nanox", "nanosplus"))'] } \ No newline at end of file diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_dh_shared_secret.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_dh_shared_secret.rs index 37f430928c..754f1c0d2d 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_dh_shared_secret.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_dh_shared_secret.rs @@ -8,10 +8,11 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::{ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; +use tari_utilities::ByteArray; use zeroize::Zeroizing; use crate::{ + tari_crypto::keys::RistrettoPublicKey, utils::{derive_from_bip32_key, get_key_from_canonical_bytes}, AppSW, KeyType, @@ -25,6 +26,7 @@ pub fn handler_get_dh_shared_secret(comm: &mut Comm) -> Result<(), AppSW> { { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_one_sided_metadata_signature.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_one_sided_metadata_signature.rs index 2f5079de56..ff07a7e54a 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_one_sided_metadata_signature.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_one_sided_metadata_signature.rs @@ -24,25 +24,27 @@ use minotari_ledger_wallet_common::{ tari_dual_address_display, TARI_DUAL_ADDRESS_SIZE, }; -use tari_crypto::{ - commitment::HomomorphicCommitmentFactory, - hashing::DomainSeparatedHasher, - keys::PublicKey, - ristretto::{ - pedersen::{extended_commitment_factory::ExtendedPedersenCommitmentFactory, PedersenCommitment}, - RistrettoComAndPubSig, - RistrettoPublicKey, - RistrettoSecretKey, - }, - tari_utilities::ByteArray, -}; -use tari_hashing::{KeyManagerTransactionsHashDomain, TransactionHashDomain}; +use tari_utilities::ByteArray; use zeroize::Zeroizing; use crate::{ alloc::string::ToString, hashing::DomainSeparatedConsensusHasher, - utils::{derive_from_bip32_key, get_key_from_canonical_bytes, get_key_from_uniform_bytes, get_random_nonce}, + tari_crypto::{ + commitment::PedersenCommitment, + commitment_and_public_key_signature::CommitmentAndPublicKeySignature, + commitment_factory::PedersenCommitmentFactory, + hashing::DomainSeparatedHasher, + keys::{RistrettoPublicKey, RistrettoSecretKey}, + }, + utils::{ + derive_from_bip32_key, + get_key_from_canonical_bytes, + get_key_from_uniform_bytes, + get_random_nonce, + KeyManagerTransactionsHashDomain, + TransactionHashDomain, + }, AppSW, KeyType, RESPONSE_VERSION, @@ -84,6 +86,7 @@ pub fn handler_get_one_sided_metadata_signature(comm: &mut Comm) -> Result<(), A { SingleMessage::new(&format!("Error: {:?}", e.to_string())).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() @@ -107,6 +110,7 @@ pub fn handler_get_one_sided_metadata_signature(comm: &mut Comm) -> Result<(), A value: &format!("{}", receiver_address), }, ]; + #[cfg(not(any(target_os = "stax", target_os = "flex")))] { let review = MultiFieldReview::new( @@ -148,7 +152,7 @@ pub fn handler_get_one_sided_metadata_signature(comm: &mut Comm) -> Result<(), A let r_x = get_random_nonce()?; let ephemeral_private_key = get_random_nonce()?; - let factory = ExtendedPedersenCommitmentFactory::default(); + let factory = PedersenCommitmentFactory::default(); let commitment = factory.commit(&commitment_mask, &value_as_private_key); let ephemeral_commitment = factory.commit(&r_x, &r_a); @@ -186,7 +190,7 @@ pub fn handler_get_one_sided_metadata_signature(comm: &mut Comm) -> Result<(), A &metadata_signature_message, ); - let metadata_signature = match RistrettoComAndPubSig::sign( + let metadata_signature = match CommitmentAndPublicKeySignature::sign( &value_as_private_key, &commitment_mask, &sender_offset_private_key, @@ -197,15 +201,18 @@ pub fn handler_get_one_sided_metadata_signature(comm: &mut Comm) -> Result<(), A &factory, ) { Ok(sig) => sig, - Err(e) => { + Err(_e) => { + let error_string = "Invalid challenge".to_string(); + #[cfg(not(any(target_os = "stax", target_os = "flex")))] { - SingleMessage::new(&format!("Signing error: {:?}", e.to_string())).show_and_wait(); + SingleMessage::new(&format!("Signing error: {}", error_string)).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() - .text(&format!("Signing error: {:?}", e.to_string())) + .text(&format!("Signing error: {}", error_string)) .show(false); } return Err(AppSW::MetadataSignatureFail); diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_key.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_key.rs index 5f4875fd4a..d8ffa57453 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_key.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_key.rs @@ -6,9 +6,9 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; +use tari_utilities::ByteArray; -use crate::{utils::derive_from_bip32_key, AppSW, KeyType, RESPONSE_VERSION}; +use crate::{tari_crypto::keys::RistrettoPublicKey, utils::derive_from_bip32_key, AppSW, KeyType, RESPONSE_VERSION}; pub fn handler_get_public_key(comm: &mut Comm) -> Result<(), AppSW> { let data = comm.get_data().map_err(|_| AppSW::WrongApduLength)?; @@ -17,6 +17,7 @@ pub fn handler_get_public_key(comm: &mut Comm) -> Result<(), AppSW> { { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_spend_key.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_spend_key.rs index 30ca2fffbc..5871a67ca7 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_spend_key.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_public_spend_key.rs @@ -6,9 +6,16 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; +use tari_utilities::ByteArray; -use crate::{utils::derive_from_bip32_key, AppSW, KeyType, RESPONSE_VERSION, STATIC_SPEND_INDEX}; +use crate::{ + tari_crypto::keys::RistrettoPublicKey, + utils::derive_from_bip32_key, + AppSW, + KeyType, + RESPONSE_VERSION, + STATIC_SPEND_INDEX, +}; pub fn handler_get_public_spend_key(comm: &mut Comm) -> Result<(), AppSW> { let data = comm.get_data().map_err(|_| AppSW::WrongApduLength)?; @@ -17,6 +24,7 @@ pub fn handler_get_public_spend_key(comm: &mut Comm) -> Result<(), AppSW> { { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_schnorr_signature.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_schnorr_signature.rs index b1c355ad7a..3e401a15da 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_schnorr_signature.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_schnorr_signature.rs @@ -8,15 +8,12 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::{ - hash_domain, - ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}, - signatures::SchnorrSignature, - tari_utilities::ByteArray, -}; +use tari_utilities::ByteArray; use crate::{ alloc::string::ToString, + hash_domain, + tari_crypto::schnorr::SchnorrSignature, utils::{derive_from_bip32_key, get_random_nonce}, AppSW, KeyType, @@ -24,9 +21,11 @@ use crate::{ }; hash_domain!(CheckSigHashDomain, "com.tari.script.check_sig", 1); +hash_domain!(SchnorrSigChallenge, "com.tari.schnorr_signature", 1); /// The type used for `CheckSig`, `CheckMultiSig`, and related opcodes' signatures -pub type CheckSigSchnorrSignature = SchnorrSignature; +pub type CheckSigSchnorrSignature = SchnorrSignature; +pub type RistrettoSchnorr = SchnorrSignature; pub fn handler_get_raw_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW> { let data = comm.get_data().map_err(|_| AppSW::WrongApduLength)?; @@ -35,6 +34,7 @@ pub fn handler_get_raw_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW> { { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); @@ -71,15 +71,17 @@ pub fn handler_get_raw_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW> { let signature = match RistrettoSchnorr::sign_raw_uniform(&private_key, private_nonce.clone(), &challenge_bytes) { Ok(sig) => sig, - Err(e) => { + Err(_e) => { + let error_string = "Invalid Challange".to_string(); #[cfg(not(any(target_os = "stax", target_os = "flex")))] { - SingleMessage::new(&format!("Signing error: {:?}", e.to_string())).show_and_wait(); + SingleMessage::new(&format!("Signing error: {}", error_string)).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() - .text(&format!("Signing error: {:?}", e.to_string())) + .text(&format!("Signing error: {}", error_string)) .show(false); } return Err(AppSW::RawSchnorrSignatureFail); @@ -101,13 +103,13 @@ pub fn handler_get_script_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); } return Err(AppSW::WrongApduLength); } - let mut account_bytes = [0u8; 8]; account_bytes.clone_from_slice(&data[0..8]); let account = u64::from_le_bytes(account_bytes); @@ -115,14 +117,13 @@ pub fn handler_get_script_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW let mut private_key_index_bytes = [0u8; 8]; private_key_index_bytes.clone_from_slice(&data[8..16]); let private_key_index = u64::from_le_bytes(private_key_index_bytes); - let mut private_key_type_bytes = [0u8; 8]; + private_key_type_bytes.clone_from_slice(&data[16..24]); let key_type = u64::from_le_bytes(private_key_type_bytes); let private_key_type = KeyType::from_branch_key(key_type)?; let private_key = derive_from_bip32_key(account, private_key_index, private_key_type)?; - let mut nonce_bytes = [0u8; 32]; nonce_bytes.clone_from_slice(&data[24..56]); @@ -130,21 +131,22 @@ pub fn handler_get_script_schnorr_signature(comm: &mut Comm) -> Result<(), AppSW let signature = match CheckSigSchnorrSignature::sign_with_nonce_and_message(&private_key, random_nonce, &nonce_bytes) { Ok(sig) => sig, - Err(e) => { + Err(_e) => { + let error_string = "Invalid Challange".to_string(); #[cfg(not(any(target_os = "stax", target_os = "flex")))] { - SingleMessage::new(&format!("Signing error: {:?}", e.to_string())).show_and_wait(); + SingleMessage::new(&format!("Signing error: {}", error_string)).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() - .text(&format!("Signing error: {:?}", e.to_string())) + .text(&format!("Signing error: {}", error_string)) .show(false); } return Err(AppSW::SchnorrSignatureFail); }, }; - comm.append(&[RESPONSE_VERSION]); // version comm.append(&signature.get_public_nonce().to_vec()); comm.append(&signature.get_signature().to_vec()); diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_offset.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_offset.rs index fbcf15881f..7375b4a2b1 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_offset.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_offset.rs @@ -4,9 +4,10 @@ use alloc::vec::Vec; use ledger_device_sdk::io::Comm; -use tari_crypto::{ristretto::RistrettoSecretKey, tari_utilities::ByteArray}; +use tari_utilities::ByteArray; use crate::{ + tari_crypto::keys::RistrettoSecretKey, utils::{alpha_hasher, derive_from_bip32_key, get_key_from_canonical_bytes}, AppSW, KeyType, diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature.rs index 1e3c59f4d7..41247e8cc7 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_script_signature.rs @@ -10,22 +10,23 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::{ - commitment::HomomorphicCommitmentFactory, - keys::PublicKey, - ristretto::{ - pedersen::{extended_commitment_factory::ExtendedPedersenCommitmentFactory, PedersenCommitment}, - RistrettoComAndPubSig, - RistrettoPublicKey, - RistrettoSecretKey, - }, -}; -use tari_hashing::TransactionHashDomain; use crate::{ alloc::string::ToString, hashing::DomainSeparatedConsensusHasher, - utils::{alpha_hasher, derive_from_bip32_key, get_key_from_canonical_bytes, get_random_nonce}, + tari_crypto::{ + commitment::PedersenCommitment, + commitment_and_public_key_signature::CommitmentAndPublicKeySignature, + commitment_factory::PedersenCommitmentFactory, + keys::{RistrettoPublicKey, RistrettoSecretKey}, + }, + utils::{ + alpha_hasher, + derive_from_bip32_key, + get_key_from_canonical_bytes, + get_random_nonce, + TransactionHashDomain, + }, AppSW, KeyType, RESPONSE_VERSION, @@ -39,6 +40,7 @@ pub fn handler_get_script_signature_managed(comm: &mut Comm) -> Result<(), AppSW { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); @@ -83,6 +85,7 @@ pub fn handler_get_script_signature_derived(comm: &mut Comm) -> Result<(), AppSW { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); @@ -175,7 +178,7 @@ fn get_script_signature( script_public_key: RistrettoPublicKey, commitment: PedersenCommitment, script_message: [u8; 32], -) -> Result { +) -> Result { let r_a = get_random_nonce()?; let r_x = get_random_nonce()?; let r_y = get_random_nonce()?; @@ -184,6 +187,7 @@ fn get_script_signature( { SingleMessage::new("Nonces not unique").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Nonces not unique").show(false); @@ -191,7 +195,7 @@ fn get_script_signature( return Err(AppSW::ScriptSignatureFail); } - let factory = ExtendedPedersenCommitmentFactory::default(); + let factory = PedersenCommitmentFactory::default(); let ephemeral_commitment = factory.commit(&r_x, &r_a); let ephemeral_pubkey = RistrettoPublicKey::from_secret_key(&r_y); @@ -206,7 +210,7 @@ fn get_script_signature( &script_message, ); - match RistrettoComAndPubSig::sign( + match CommitmentAndPublicKeySignature::sign( &value, &commitment_private_key, &script_private_key, @@ -217,15 +221,17 @@ fn get_script_signature( &factory, ) { Ok(sig) => Ok(sig), - Err(e) => { + Err(_e) => { + let error_string = "Invalid Challenge".to_string(); #[cfg(not(any(target_os = "stax", target_os = "flex")))] { - SingleMessage::new(&format!("Signing error: {:?}", e.to_string())).show_and_wait(); + SingleMessage::new(&format!("Signing error: {}", error_string)).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() - .text(&format!("Signing error: {:?}", e.to_string())) + .text(&format!("Signing error: {}", error_string)) .show(false); } Err(AppSW::ScriptSignatureFail) diff --git a/applications/minotari_ledger_wallet/wallet/src/handlers/get_view_key.rs b/applications/minotari_ledger_wallet/wallet/src/handlers/get_view_key.rs index ee342e6347..43eee45b60 100644 --- a/applications/minotari_ledger_wallet/wallet/src/handlers/get_view_key.rs +++ b/applications/minotari_ledger_wallet/wallet/src/handlers/get_view_key.rs @@ -6,7 +6,7 @@ use ledger_device_sdk::io::Comm; use ledger_device_sdk::nbgl::NbglStatus; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::SingleMessage; -use tari_crypto::tari_utilities::ByteArray; +use tari_utilities::ByteArray; use crate::{utils::derive_from_bip32_key, AppSW, KeyType, RESPONSE_VERSION, STATIC_VIEW_INDEX}; @@ -17,6 +17,7 @@ pub fn handler_get_view_key(comm: &mut Comm) -> Result<(), AppSW> { { SingleMessage::new("Invalid data length").show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&"Invalid data length").show(false); diff --git a/applications/minotari_ledger_wallet/wallet/src/hashing.rs b/applications/minotari_ledger_wallet/wallet/src/hashing.rs index 384a7932f0..a02944c2ef 100644 --- a/applications/minotari_ledger_wallet/wallet/src/hashing.rs +++ b/applications/minotari_ledger_wallet/wallet/src/hashing.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Tari Project +// Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause use alloc::format; @@ -6,11 +6,12 @@ use core::marker::PhantomData; use borsh::{io, io::Write, BorshSerialize}; use digest::Digest; -use tari_crypto::hashing::DomainSeparation; +use crate::tari_crypto::hashing::DomainSeparation; pub struct DomainSeparatedConsensusHasher { hasher: DomainSeparatedBorshHasher, } +use digest::Output; impl DomainSeparatedConsensusHasher where D: Default @@ -61,8 +62,6 @@ impl DomainSeparatedBorshHasher } } -/// This private struct wraps a Digest and implements the Write trait to satisfy the consensus encoding trait. -/// Do not use the DomainSeparatedHasher with this. #[derive(Clone)] struct WriteHashWrapper(D); @@ -76,3 +75,19 @@ impl Write for WriteHashWrapper { Ok(()) } } + +pub struct DomainSeparatedHash { + pub output: Output, +} + +impl DomainSeparatedHash { + pub fn new(output: Output) -> Self { + Self { output } + } +} + +impl AsRef<[u8]> for DomainSeparatedHash { + fn as_ref(&self) -> &[u8] { + self.output.as_slice() + } +} diff --git a/applications/minotari_ledger_wallet/wallet/src/main.rs b/applications/minotari_ledger_wallet/wallet/src/main.rs index d58d130126..5e63574adb 100644 --- a/applications/minotari_ledger_wallet/wallet/src/main.rs +++ b/applications/minotari_ledger_wallet/wallet/src/main.rs @@ -6,8 +6,8 @@ #![feature(alloc_error_handler)] extern crate alloc; - mod hashing; +mod tari_crypto; pub mod utils; mod app_ui { @@ -207,6 +207,7 @@ extern "C" fn sample_main() { // This is long-lived over the span the ledger app is open, across multiple interactions let mut offset_ctx = ScriptOffsetCtx::new(); + #[cfg(any(target_os = "stax", target_os = "flex"))] let mut home = { // Initialize reference to Comm instance for NBGL diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment.rs new file mode 100644 index 0000000000..eca612158e --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment.rs @@ -0,0 +1,114 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use core::{ + cmp::Ordering, + hash::{Hash, Hasher}, + ops::{Add, Mul, Sub}, +}; + +use tari_utilities::{ByteArray, ByteArrayError}; + +use crate::tari_crypto::keys::{RistrettoPublicKey, RistrettoSecretKey}; + +#[derive(Debug, Clone, Default)] +pub struct PedersenCommitment(pub(crate) RistrettoPublicKey); + +impl PedersenCommitment { + /// Get this commitment as a public key point + pub fn as_public_key(&self) -> &RistrettoPublicKey { + &self.0 + } + + /// Converts a public key into a commitment + pub fn from_public_key(key: &RistrettoPublicKey) -> PedersenCommitment { + PedersenCommitment(key.clone()) + } +} + +impl ByteArray for PedersenCommitment { + fn from_canonical_bytes(bytes: &[u8]) -> Result { + let key = RistrettoPublicKey::from_canonical_bytes(bytes)?; + Ok(Self(key)) + } + + fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl PartialOrd for PedersenCommitment { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PedersenCommitment { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +/// Add two commitments together. Note! There is no check that the bases are equal. +impl<'b> Add for &'b PedersenCommitment { + type Output = PedersenCommitment; + + fn add(self, rhs: &'b PedersenCommitment) -> Self::Output { + PedersenCommitment(&self.0 + &rhs.0) + } +} + +/// Add a public key to a commitment. Note! There is no check that the bases are equal. +impl<'b> Add<&'b RistrettoPublicKey> for &'b PedersenCommitment { + type Output = PedersenCommitment; + + fn add(self, rhs: &'b RistrettoPublicKey) -> Self::Output { + PedersenCommitment(&self.0 + rhs) + } +} + +/// Subtracts the left commitment from the right commitment. Note! There is no check that the bases are equal. +impl<'b> Sub for &'b PedersenCommitment { + type Output = PedersenCommitment; + + fn sub(self, rhs: &'b PedersenCommitment) -> Self::Output { + PedersenCommitment(&self.0 - &rhs.0) + } +} + +/// Multiply the commitment with a private key +impl<'a, 'b> Mul<&'b RistrettoSecretKey> for &'a PedersenCommitment { + type Output = PedersenCommitment; + + fn mul(self, rhs: &'b RistrettoSecretKey) -> PedersenCommitment { + let p = rhs * &self.0; + PedersenCommitment::from_public_key(&p) + } +} + +impl Hash for PedersenCommitment { + fn hash(&self, state: &mut H) { + state.write(self.as_bytes()) + } +} + +impl PartialEq for PedersenCommitment { + fn eq(&self, other: &Self) -> bool { + self.as_public_key() == other.as_public_key() + } +} + +impl Eq for PedersenCommitment {} + +impl borsh::BorshDeserialize for PedersenCommitment { + fn deserialize_reader(reader: &mut R) -> Result + where R: borsh::io::Read { + Ok(Self(RistrettoPublicKey::deserialize_reader(reader)?)) + } +} + +impl borsh::BorshSerialize for PedersenCommitment { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + self.0.serialize(writer) + } +} diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_and_public_key_signature.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_and_public_key_signature.rs new file mode 100644 index 0000000000..d2b7a5419e --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_and_public_key_signature.rs @@ -0,0 +1,289 @@ +// Copyright 2025. The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use alloc::vec::Vec; +use core::{ + cmp::Ordering, + hash::{Hash, Hasher}, + ops::Add, +}; + +use tari_utilities::ByteArray; + +use crate::tari_crypto::{ + commitment::PedersenCommitment, + commitment_factory::PedersenCommitmentFactory, + keys::{RistrettoPublicKey, RistrettoSecretKey}, + schnorr::SchnorrSignature, +}; + +/// An error when creating a commitment signature +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CommitmentAndPublicKeySignatureError { + InvalidChallenge, +} + +#[derive(Debug, Clone)] +pub struct CommitmentAndPublicKeySignature { + pub(crate) ephemeral_commitment: PedersenCommitment, + pub(crate) ephemeral_pubkey: RistrettoPublicKey, + pub(crate) u_a: RistrettoSecretKey, + pub(crate) u_x: RistrettoSecretKey, + pub(crate) u_y: RistrettoSecretKey, +} + +impl CommitmentAndPublicKeySignature { + /// Creates a new [CommitmentSignature] + pub fn new( + ephemeral_commitment: PedersenCommitment, + ephemeral_pubkey: RistrettoPublicKey, + u_a: RistrettoSecretKey, + u_x: RistrettoSecretKey, + u_y: RistrettoSecretKey, + ) -> Self { + CommitmentAndPublicKeySignature { + ephemeral_commitment, + ephemeral_pubkey, + u_a, + u_x, + u_y, + } + } + + #[allow(clippy::too_many_arguments)] + pub fn sign( + a: &RistrettoSecretKey, + x: &RistrettoSecretKey, + y: &RistrettoSecretKey, + r_a: &RistrettoSecretKey, + r_x: &RistrettoSecretKey, + r_y: &RistrettoSecretKey, + challenge: &[u8], + factory: &PedersenCommitmentFactory, + ) -> Result { + // The challenge is computed by wide reduction + let e = match RistrettoSecretKey::from_uniform_bytes(challenge) { + Ok(e) => e, + Err(_) => return Err(CommitmentAndPublicKeySignatureError::InvalidChallenge), + }; + + // The challenge cannot be zero + if e == RistrettoSecretKey::default() { + return Err(CommitmentAndPublicKeySignatureError::InvalidChallenge); + } + + // Compute the response values + let ea = &e * a; + let ex = &e * x; + let ey = &e * y; + + let u_a = r_a + &ea; + let u_x = r_x + &ex; + let u_y = r_y + &ey; + + // Compute the initial values + let ephemeral_commitment = factory.commit(r_x, r_a); + let ephemeral_pubkey = RistrettoPublicKey::from_secret_key(r_y); + + Ok(Self::new(ephemeral_commitment, ephemeral_pubkey, u_a, u_x, u_y)) + } + + /// Get the signature tuple `(ephemeral_commitment, ephemeral_pubkey, u_a, u_x, u_y)` + pub fn complete_signature_tuple( + &self, + ) -> ( + &PedersenCommitment, + &RistrettoPublicKey, + &RistrettoSecretKey, + &RistrettoSecretKey, + &RistrettoSecretKey, + ) { + ( + &self.ephemeral_commitment, + &self.ephemeral_pubkey, + &self.u_a, + &self.u_x, + &self.u_y, + ) + } + + /// Get the response value `u_a` + pub fn u_a(&self) -> &RistrettoSecretKey { + &self.u_a + } + + /// Get the response value `u_x` + pub fn u_x(&self) -> &RistrettoSecretKey { + &self.u_x + } + + /// Get the response value `u_y` + pub fn u_y(&self) -> &RistrettoSecretKey { + &self.u_y + } + + /// Get the ephemeral commitment `ephemeral_commitment` + pub fn ephemeral_commitment(&self) -> &PedersenCommitment { + &self.ephemeral_commitment + } + + /// Get the ephemeral public key `ephemeral_pubkey` + pub fn ephemeral_pubkey(&self) -> &RistrettoPublicKey { + &self.ephemeral_pubkey + } + + /// Produce a canonical byte representation of the commitment signature + pub fn to_vec(&self) -> Vec { + let mut buf = Vec::with_capacity(2 * RistrettoPublicKey::key_length() + 3 * RistrettoSecretKey::key_length()); + buf.extend_from_slice(self.ephemeral_commitment().as_bytes()); + buf.extend_from_slice(self.ephemeral_pubkey().as_bytes()); + buf.extend_from_slice(self.u_a().as_bytes()); + buf.extend_from_slice(self.u_x().as_bytes()); + buf.extend_from_slice(self.u_y().as_bytes()); + buf + } +} + +impl<'a, 'b> Add<&'b CommitmentAndPublicKeySignature> for &'a CommitmentAndPublicKeySignature { + type Output = CommitmentAndPublicKeySignature; + + fn add(self, rhs: &'b CommitmentAndPublicKeySignature) -> CommitmentAndPublicKeySignature { + let ephemeral_commitment_sum = self.ephemeral_commitment() + rhs.ephemeral_commitment(); + let ephemeral_pubkey_sum_sum = self.ephemeral_pubkey() + rhs.ephemeral_pubkey(); + let u_a_sum = self.u_a() + rhs.u_a(); + let u_x_sum = self.u_x() + rhs.u_x(); + let u_y_sum = self.u_y() + rhs.u_y(); + + CommitmentAndPublicKeySignature::new( + ephemeral_commitment_sum, + ephemeral_pubkey_sum_sum, + u_a_sum, + u_x_sum, + u_y_sum, + ) + } +} + +impl<'a> Add for &'a CommitmentAndPublicKeySignature { + type Output = CommitmentAndPublicKeySignature; + + fn add(self, rhs: CommitmentAndPublicKeySignature) -> CommitmentAndPublicKeySignature { + let ephemeral_commitment_sum = self.ephemeral_commitment() + rhs.ephemeral_commitment(); + let ephemeral_pubkey_sum_sum = self.ephemeral_pubkey() + rhs.ephemeral_pubkey(); + let u_a_sum = self.u_a() + rhs.u_a(); + let u_x_sum = self.u_x() + rhs.u_x(); + let u_y_sum = self.u_y() + rhs.u_y(); + + CommitmentAndPublicKeySignature::new( + ephemeral_commitment_sum, + ephemeral_pubkey_sum_sum, + u_a_sum, + u_x_sum, + u_y_sum, + ) + } +} + +impl<'a, 'b> Add<&'b SchnorrSignature> for &'a CommitmentAndPublicKeySignature { + type Output = CommitmentAndPublicKeySignature; + + fn add(self, rhs: &'b SchnorrSignature) -> CommitmentAndPublicKeySignature { + let ephemeral_commitment_sum = self.ephemeral_commitment().clone(); + let ephemeral_pubkey_sum_sum = self.ephemeral_pubkey() + rhs.get_public_nonce(); + let u_a_sum = self.u_a().clone(); + let u_x_sum = self.u_x().clone(); + let u_y_sum = self.u_y() + rhs.get_signature(); + + CommitmentAndPublicKeySignature::new( + ephemeral_commitment_sum, + ephemeral_pubkey_sum_sum, + u_a_sum, + u_x_sum, + u_y_sum, + ) + } +} + +impl<'a> Add for &'a CommitmentAndPublicKeySignature { + type Output = CommitmentAndPublicKeySignature; + + fn add(self, rhs: SchnorrSignature) -> CommitmentAndPublicKeySignature { + let ephemeral_commitment_sum = self.ephemeral_commitment().clone(); + let ephemeral_pubkey_sum_sum = self.ephemeral_pubkey() + rhs.get_public_nonce(); + let u_a_sum = self.u_a().clone(); + let u_x_sum = self.u_x().clone(); + let u_y_sum = self.u_y() + rhs.get_signature(); + + CommitmentAndPublicKeySignature::new( + ephemeral_commitment_sum, + ephemeral_pubkey_sum_sum, + u_a_sum, + u_x_sum, + u_y_sum, + ) + } +} + +impl Default for CommitmentAndPublicKeySignature { + fn default() -> Self { + CommitmentAndPublicKeySignature::new( + PedersenCommitment::default(), + RistrettoPublicKey::default(), + RistrettoSecretKey::default(), + RistrettoSecretKey::default(), + RistrettoSecretKey::default(), + ) + } +} + +/// Provide a canonical ordering for commitment signatures. We use byte representations of all values in this order: +/// `ephemeral_commitment, ephemeral_pubkey, u_a, u_x, u_y` +impl Ord for CommitmentAndPublicKeySignature { + fn cmp(&self, other: &Self) -> Ordering { + let mut compare = self.ephemeral_commitment().cmp(other.ephemeral_commitment()); + if compare != Ordering::Equal { + return compare; + } + + compare = self.ephemeral_pubkey().cmp(other.ephemeral_pubkey()); + if compare != Ordering::Equal { + return compare; + } + + compare = self.u_a().as_bytes().cmp(other.u_a().as_bytes()); + if compare != Ordering::Equal { + return compare; + } + + compare = self.u_x().as_bytes().cmp(other.u_x().as_bytes()); + if compare != Ordering::Equal { + return compare; + } + + self.u_y().as_bytes().cmp(other.u_y().as_bytes()) + } +} + +impl PartialOrd for CommitmentAndPublicKeySignature { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for CommitmentAndPublicKeySignature { + fn eq(&self, other: &Self) -> bool { + self.ephemeral_commitment().eq(other.ephemeral_commitment()) && + self.ephemeral_pubkey().eq(other.ephemeral_pubkey()) && + self.u_a().eq(other.u_a()) && + self.u_x().eq(other.u_x()) && + self.u_y().eq(other.u_y()) + } +} + +impl Eq for CommitmentAndPublicKeySignature {} + +impl Hash for CommitmentAndPublicKeySignature { + fn hash(&self, state: &mut H) { + state.write(&self.to_vec()) + } +} diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_factory.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_factory.rs new file mode 100644 index 0000000000..9515bb6542 --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/commitment_factory.rs @@ -0,0 +1,76 @@ +// Copyright 2025. The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +//! Pedersen commitment types and factories for Ristretto + +use curve25519_dalek::{ + constants::RISTRETTO_BASEPOINT_POINT, + ristretto::{CompressedRistretto, RistrettoPoint}, + traits::{Identity, MultiscalarMul}, +}; + +use crate::tari_crypto::{ + commitment::PedersenCommitment, + keys::{RistrettoPublicKey, RistrettoSecretKey}, +}; + +pub const TARI_H: CompressedRistretto = CompressedRistretto([ + 206, 56, 152, 65, 192, 200, 105, 138, 185, 91, 112, 36, 42, 238, 166, 72, 64, 177, 234, 197, 246, 68, 183, 208, 8, + 172, 5, 135, 207, 71, 29, 112, +]); +pub const RISTRETTO_PEDERSEN_G: RistrettoPoint = RISTRETTO_BASEPOINT_POINT; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[allow(non_snake_case)] +pub struct PedersenCommitmentFactory { + pub(crate) G: RistrettoPoint, + pub(crate) H: RistrettoPoint, +} + +impl PedersenCommitmentFactory { + #[allow(non_snake_case)] + pub fn new(G: RistrettoPoint, H: RistrettoPoint) -> PedersenCommitmentFactory { + PedersenCommitmentFactory { G, H } + } +} + +impl Default for PedersenCommitmentFactory { + fn default() -> Self { + PedersenCommitmentFactory::new(RISTRETTO_PEDERSEN_G, ristretto_pedersen_h()) + } +} + +impl PedersenCommitmentFactory { + #[allow(non_snake_case)] + pub fn commit(&self, k: &RistrettoSecretKey, v: &RistrettoSecretKey) -> PedersenCommitment { + let c = if (self.G, self.H) == (RISTRETTO_PEDERSEN_G, ristretto_pedersen_h()) { + RistrettoPoint::multiscalar_mul(&[v.0, k.0], &[self.H, self.G]) + } else { + RistrettoPoint::multiscalar_mul(&[v.0, k.0], &[self.H, self.G]) + }; + PedersenCommitment(RistrettoPublicKey::new_from_pk(c)) + } + + pub fn zero(&self) -> PedersenCommitment { + PedersenCommitment(RistrettoPublicKey::new_from_pk(RistrettoPoint::identity())) + } + + pub fn open(&self, k: &RistrettoSecretKey, v: &RistrettoSecretKey, commitment: &PedersenCommitment) -> bool { + let c_test = self.commit(k, v); + commitment.0 == c_test.0 + } + + pub fn commit_value(&self, k: &RistrettoSecretKey, value: u64) -> PedersenCommitment { + let v = RistrettoSecretKey::from(value); + self.commit(k, &v) + } + + pub fn open_value(&self, k: &RistrettoSecretKey, v: u64, commitment: &PedersenCommitment) -> bool { + let kv = RistrettoSecretKey::from(v); + self.open(k, &kv, commitment) + } +} + +fn ristretto_pedersen_h() -> RistrettoPoint { + TARI_H.decompress().expect("Failed to decompress TARI_H") +} diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/hashing.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/hashing.rs new file mode 100644 index 0000000000..39c73e743c --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/hashing.rs @@ -0,0 +1,240 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause +use alloc::{format, string::String}; + +use core::marker::PhantomData; + +use digest::{Digest, FixedOutput, FixedOutputReset, Output, OutputSizeUser, Update}; + +use crate::hashing::DomainSeparatedHash; + +// +pub trait DomainSeparation { + /// Returns the version number for the metadata tag + fn version() -> u8; + + /// Returns the category label for the metadata tag. For example, `tari_hmac` + fn domain() -> &'static str; + + /// The domain separation tag is defined as `{domain}.v{version}.{label}`, where the version and tag are + /// typically hard-coded into the implementing type, and the label is provided per specific application of the + /// domain + fn domain_separation_tag>(label: S) -> String { + if !label.as_ref().is_empty() { + return format!("{}.v{}.{}", Self::domain(), Self::version(), label.as_ref()); + } + format!("{}.v{}", Self::domain(), Self::version()) + } + + /// Adds the domain separation tag to the given digest. The domain separation tag is defined as + /// `{domain}.v{version}.{label}`, where the version and tag are typically hard-coded into the implementing + /// type, and the label is provided per specific application of the domain. + fn add_domain_separation_tag, D: Digest>(digest: &mut D, label: S) { + let label = if label.as_ref().is_empty() { &[] } else { label.as_ref() }; + let domain = Self::domain(); + let (version_offset, version) = byte_to_decimal_ascii_bytes(Self::version()); + let len = if label.is_empty() { + // 2 additional bytes are 1 x '.' delimiters and 'v' tag for version + domain.len() + (3 - version_offset) + 2 + } else { + // 3 additional bytes are 2 x '.' delimiters and 'v' tag for version + domain.len() + (3 - version_offset) + label.len() + 3 + }; + let len = (len as u64).to_le_bytes(); + digest.update(len); + digest.update(domain); + digest.update(b".v"); + digest.update(&version[version_offset..]); + if !label.is_empty() { + digest.update(b"."); + digest.update(label); + } + } +} + +fn byte_to_decimal_ascii_bytes(mut byte: u8) -> (usize, [u8; 3]) { + const ZERO_ASCII_CHAR: u8 = 48; + // A u8 can only ever be a 3 char number. + let mut bytes = [0u8, 0u8, ZERO_ASCII_CHAR]; + let mut pos = 3usize; + if byte == 0 { + return (2, bytes); + } + while byte > 0 { + let rem = byte % 10; + byte /= 10; + bytes[pos - 1] = ZERO_ASCII_CHAR + rem; + pos -= 1; + } + (pos, bytes) +} +#[derive(Debug, Clone, Default)] +pub struct DomainSeparatedHasher { + inner: D, + label: &'static str, + _dst: PhantomData, +} + +impl DomainSeparatedHasher { + /// Create a new instance of [`DomainSeparatedHasher`] without an additional label (to correspond to 'D::new()'). + pub fn new() -> Self { + Self::new_with_label("") + } + + /// Create a new instance of [`DomainSeparatedHasher`] for the given label. + pub fn new_with_label(label: &'static str) -> Self { + let mut inner = D::new(); + M::add_domain_separation_tag(&mut inner, label); + Self { + inner, + label, + _dst: PhantomData, + } + } + + /// Adds the data to the digest function by first appending the length of the data in the byte array, and then + /// supplying the data itself. + pub fn update(&mut self, data: impl AsRef<[u8]>) { + let len = (data.as_ref().len() as u64).to_le_bytes(); + self.inner.update(len); + self.inner.update(data); + } + + /// Does the same thing as [`Self::update`], but returns the hasher instance to support fluent syntax. + #[must_use] + pub fn chain(mut self, data: impl AsRef<[u8]>) -> Self { + self.update(data); + self + } + + /// Finalize the hasher and return the hash result. + pub fn finalize(self) -> DomainSeparatedHash { + let output = self.inner.finalize(); + DomainSeparatedHash::new(output) + } +} + +impl PartialEq for DomainSeparatedHasher { + fn eq(&self, other: &Self) -> bool { + self.label == other.label + } +} + +impl Eq for DomainSeparatedHasher {} + +impl OutputSizeUser + for DomainSeparatedHasher +{ + type OutputSize = TInnerDigest::OutputSize; +} +// +impl Update for DomainSeparatedHasher { + fn update(&mut self, data: &[u8]) { + self.inner.update(data); + } +} + +// impl AsFixedBytes for DomainSeparatedHash {} + +impl FixedOutput + for DomainSeparatedHasher +{ + fn finalize_into(self, out: &mut Output) { + self.inner.finalize_into(out); + } +} + +// Implements Digest so that it can be used for other crates +impl Digest + for DomainSeparatedHasher +{ + fn new() -> Self { + DomainSeparatedHasher::::new() + } + + // Create new hasher instance which has processed the provided data. + fn new_with_prefix(data: impl AsRef<[u8]>) -> Self { + let hasher = DomainSeparatedHasher::::new(); + hasher.chain_update(data) + } + + fn update(&mut self, data: impl AsRef<[u8]>) { + self.update(data); + } + + fn chain_update(self, data: impl AsRef<[u8]>) -> Self + where Self: Sized { + self.chain(data) + } + + fn finalize(self) -> Output { + self.finalize().output + } + + fn finalize_reset(&mut self) -> Output { + let value = self.inner.finalize_reset(); + TDomain::add_domain_separation_tag(&mut self.inner, self.label); + value + } + + fn finalize_into_reset(&mut self, out: &mut Output) { + Digest::finalize_into_reset(&mut self.inner, out); + } + + // Write result into provided array and consume the hasher instance. + fn finalize_into(self, out: &mut Output) { + Digest::finalize_into(self.inner, out); + } + + fn reset(&mut self) { + Digest::reset(&mut self.inner); + TDomain::add_domain_separation_tag(&mut self.inner, self.label); + } + + fn output_size() -> usize { + ::output_size() + } + + fn digest(data: impl AsRef<[u8]>) -> Output { + let mut hasher = Self::new(); + hasher.update(data); + hasher.finalize().output + } +} + +//------------------------------------------------ HMAC ------------------------------------------------------------ +/// A domain separation tag for use in MAC derivation algorithms. +pub struct MacDomain; + +impl DomainSeparation for MacDomain { + fn version() -> u8 { + 1 + } + + fn domain() -> &'static str { + "com.tari.mac" + } +} + +/// Creates a DomainSeparation struct for a given domain. +#[macro_export] +macro_rules! hash_domain { + ($name:ident, $domain:expr, $version: expr) => { + /// A hashing domain instance + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name; + + impl $crate::tari_crypto::hashing::DomainSeparation for $name { + fn version() -> u8 { + $version + } + + fn domain() -> &'static str { + $domain + } + } + }; + ($name:ident, $domain:expr) => { + hash_domain!($name, $domain, 1); + }; +} diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/keys.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/keys.rs new file mode 100644 index 0000000000..264b25e399 --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/keys.rs @@ -0,0 +1,556 @@ +// Copyright 2025. The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +//! The Tari-compatible implementation of Ristretto based on the curve25519-dalek implementation +use alloc::{format, string::ToString, vec::Vec}; +use core::{ + borrow::Borrow, + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + ops::{Add, Mul, Sub}, +}; + +use blake2::Blake2b; +use curve25519_dalek::{ + constants::RISTRETTO_BASEPOINT_TABLE, + ristretto::{CompressedRistretto, RistrettoPoint}, + scalar::Scalar, +}; +use digest::{consts::U64, Digest}; +use rand_core::{CryptoRng, RngCore}; +use subtle::ConstantTimeEq; +use tari_utilities::{hex::Hex, ByteArray, ByteArrayError, Hashable}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +macro_rules! define_add_variants { + (LHS = $lhs:ty, RHS = $rhs:ty, Output = $out:ty) => { + impl<'b> Add<&'b $rhs> for $lhs { + type Output = $out; + + fn add(self, rhs: &'b $rhs) -> $out { + &self + rhs + } + } + + impl<'a> Add<$rhs> for &'a $lhs { + type Output = $out; + + fn add(self, rhs: $rhs) -> $out { + self + &rhs + } + } + + impl Add<$rhs> for $lhs { + type Output = $out; + + fn add(self, rhs: $rhs) -> $out { + &self + &rhs + } + } + }; +} + +/// Add variations for `Sub` definitions, similar to those for `Add` +macro_rules! define_sub_variants { + (LHS = $lhs:ty, RHS = $rhs:ty, Output = $out:ty) => { + impl<'b> Sub<&'b $rhs> for $lhs { + type Output = $out; + + fn sub(self, rhs: &'b $rhs) -> $out { + &self - rhs + } + } + + impl<'a> Sub<$rhs> for &'a $lhs { + type Output = $out; + + fn sub(self, rhs: $rhs) -> $out { + self - &rhs + } + } + + impl Sub<$rhs> for $lhs { + type Output = $out; + + fn sub(self, rhs: $rhs) -> $out { + &self - &rhs + } + } + }; +} + +/// Add variations for `Mul` definitions, similar to those for `Add` +macro_rules! define_mul_variants { + (LHS = $lhs:ty, RHS = $rhs:ty, Output = $out:ty) => { + impl<'b> Mul<&'b $rhs> for $lhs { + type Output = $out; + + fn mul(self, rhs: &'b $rhs) -> $out { + &self * rhs + } + } + + impl<'a> Mul<$rhs> for &'a $lhs { + type Output = $out; + + fn mul(self, rhs: $rhs) -> $out { + self * &rhs + } + } + + impl Mul<$rhs> for $lhs { + type Output = $out; + + fn mul(self, rhs: $rhs) -> $out { + &self * &rhs + } + } + }; +} + +#[derive(Clone, Default, Zeroize, ZeroizeOnDrop)] +pub struct RistrettoSecretKey(pub(crate) Scalar); + +impl RistrettoSecretKey {} + +impl borsh::BorshSerialize for RistrettoSecretKey { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + borsh::BorshSerialize::serialize(&self.as_bytes(), writer) + } +} + +impl borsh::BorshDeserialize for RistrettoSecretKey { + fn deserialize_reader(reader: &mut R) -> Result + where R: borsh::io::Read { + let bytes: Zeroizing> = Zeroizing::new(borsh::BorshDeserialize::deserialize_reader(reader)?); + Self::from_canonical_bytes(bytes.as_slice()) + .map_err(|e| borsh::io::Error::new(borsh::io::ErrorKind::InvalidInput, e.to_string())) + } +} + +//------------------------------------- Ristretto Secret Key ByteArray ---------------------------------------------// + +impl ByteArray for RistrettoSecretKey { + /// Return a secret key computed from a canonical byte array + /// If the byte array is not exactly 32 bytes, returns an error + /// If the byte array does not represent a canonical encoding, returns an error + fn from_canonical_bytes(bytes: &[u8]) -> Result + where Self: Sized { + if bytes.len() != Self::KEY_LEN { + return Err(ByteArrayError::IncorrectLength {}); + } + + let mut bytes_copied = [0u8; 32]; + bytes_copied.copy_from_slice(bytes); + let scalar = Option::::from(Scalar::from_canonical_bytes(bytes_copied)).ok_or( + ByteArrayError::ConversionError { + reason: ("Invalid canonical scalar byte array".to_string()), + }, + )?; + bytes_copied.zeroize(); + + Ok(RistrettoSecretKey(scalar)) + } + + /// Return the byte array for the secret key in little-endian order + fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Hash for RistrettoSecretKey { + /// Require the implementation of the Hash trait for Hashmaps + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +impl PartialEq for RistrettoSecretKey { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for RistrettoSecretKey {} + +impl ConstantTimeEq for RistrettoSecretKey { + fn ct_eq(&self, other: &Self) -> subtle::Choice { + self.0.ct_eq(&other.0) + } +} + +//---------------------------------- RistrettoSecretKey Debug --------------------------------------------// +impl fmt::Debug for RistrettoSecretKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RistrettoSecretKey(***)") + } +} + +impl RistrettoSecretKey { + const KEY_LEN: usize = 32; + const WIDE_REDUCTION_LEN: usize = 64; + + /// Get the multiplicative inverse of a nonzero secret key + /// If zero is passed, returns `None`; annoying, but a useful guardrail + pub fn invert(&self) -> Option { + if self.0 == Scalar::ZERO { + None + } else { + Some(RistrettoSecretKey(self.0.invert())) + } + } + + /// Return a random secret key on the `ristretto255` curve using the supplied CSPRNG. + pub fn random(rng: &mut R) -> Self { + RistrettoSecretKey(Scalar::random(rng)) + } + + /// Return a secret key computed from a uniform byte slice using wide reduction + /// If the byte array is not exactly 64 bytes, returns an error + pub fn from_uniform_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::WIDE_REDUCTION_LEN { + return Err(ByteArrayError::IncorrectLength {}); + } + + let mut bytes_copied = Zeroizing::new([0u8; Self::WIDE_REDUCTION_LEN]); + bytes_copied.copy_from_slice(bytes); + + Ok(RistrettoSecretKey(Scalar::from_bytes_mod_order_wide(&bytes_copied))) + } + + pub fn key_length() -> usize { + Self::KEY_LEN + } +} + +impl ConstantTimeEq for RistrettoPublicKey { + fn ct_eq(&self, other: &Self) -> subtle::Choice { + self.point.ct_eq(&other.point) + } +} + +//---------------------------------- RistrettoSecretKey Mul / Add / Sub --------------------------------------------// + +impl<'b> Mul<&'b RistrettoPublicKey> for &RistrettoSecretKey { + type Output = RistrettoPublicKey; + + fn mul(self, rhs: &'b RistrettoPublicKey) -> RistrettoPublicKey { + let p = self.0 * rhs.point; + RistrettoPublicKey::new_from_pk(p) + } +} + +impl<'b> Add<&'b RistrettoSecretKey> for &RistrettoSecretKey { + type Output = RistrettoSecretKey; + + fn add(self, rhs: &'b RistrettoSecretKey) -> RistrettoSecretKey { + let k = self.0 + rhs.0; + RistrettoSecretKey(k) + } +} + +impl<'b> Sub<&'b RistrettoSecretKey> for &RistrettoSecretKey { + type Output = RistrettoSecretKey; + + fn sub(self, rhs: &'b RistrettoSecretKey) -> RistrettoSecretKey { + RistrettoSecretKey(self.0 - rhs.0) + } +} + +define_add_variants!( + LHS = RistrettoSecretKey, + RHS = RistrettoSecretKey, + Output = RistrettoSecretKey +); +define_sub_variants!( + LHS = RistrettoSecretKey, + RHS = RistrettoSecretKey, + Output = RistrettoSecretKey +); +define_mul_variants!( + LHS = RistrettoSecretKey, + RHS = RistrettoPublicKey, + Output = RistrettoPublicKey +); + +//--------------------------------------------- Conversions -------------------------------------------------// + +impl From for RistrettoSecretKey { + fn from(v: u64) -> Self { + let s = Scalar::from(v); + RistrettoSecretKey(s) + } +} + +//--------------------------------------------- Borrow impl -------------------------------------------------// + +impl Borrow for &RistrettoSecretKey { + fn borrow(&self) -> &Scalar { + &self.0 + } +} + +//--------------------------------------------- Ristretto Public Key -------------------------------------------------// + +#[derive(Clone)] +pub struct RistrettoPublicKey { + point: RistrettoPoint, + compressed: CompressedRistretto, +} + +impl RistrettoPublicKey { + const KEY_LEN: usize = 32; + + // Private constructor + pub(super) fn new_from_pk(pk: RistrettoPoint) -> Self { + let compressed = pk.compress(); + Self { point: pk, compressed } + } + + fn new_from_compressed(compressed: CompressedRistretto) -> Option { + compressed.decompress().map(|point| Self { + compressed: compressed.into(), + point, + }) + } + + /// Return the embedded RistrettoPoint representation + pub fn point(&self) -> RistrettoPoint { + self.point + } + + pub(super) fn compressed(&self) -> &CompressedRistretto { + &self.compressed + } + + /// Generates a new Public key from the given secret key + pub fn from_secret_key(k: &RistrettoSecretKey) -> RistrettoPublicKey { + let pk = &k.0 * RISTRETTO_BASEPOINT_TABLE; + RistrettoPublicKey::new_from_pk(pk) + } + + pub fn key_length() -> usize { + Self::KEY_LEN + } +} + +impl borsh::BorshSerialize for RistrettoPublicKey { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + borsh::BorshSerialize::serialize(&self.as_bytes(), writer) + } +} + +impl borsh::BorshDeserialize for RistrettoPublicKey { + fn deserialize_reader(reader: &mut R) -> Result + where R: borsh::io::Read { + let bytes: Vec = borsh::BorshDeserialize::deserialize_reader(reader)?; + Self::from_canonical_bytes(bytes.as_slice()) + .map_err(|e| borsh::io::Error::new(borsh::io::ErrorKind::InvalidInput, e.to_string())) + } +} + +impl Zeroize for RistrettoPublicKey { + /// Zeroizes both the point and (if it exists) the compressed point + fn zeroize(&mut self) { + self.point.zeroize(); + self.compressed.zeroize(); + } +} + +// Requires custom Hashable implementation for RistrettoPublicKey as CompressedRistretto doesnt implement this trait +impl Hashable for RistrettoPublicKey { + fn hash(&self) -> Vec { + Blake2b::::digest(self.as_bytes()).to_vec() + } +} + +impl Hash for RistrettoPublicKey { + /// Require the implementation of the Hash trait for Hashmaps + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +//---------------------------------- Ristretto Public Key Default -----------------------------------------------// + +impl Default for RistrettoPublicKey { + fn default() -> Self { + RistrettoPublicKey::new_from_pk(RistrettoPoint::default()) + } +} + +//------------------------------------ PublicKey Display impl ---------------------------------------------// + +impl fmt::Display for RistrettoPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_case(f, false) + } +} + +impl RistrettoPublicKey { + fn fmt_case(&self, f: &mut fmt::Formatter, uppercase: bool) -> fmt::Result { + let mut hex = self.to_hex(); + if uppercase { + hex = hex.to_uppercase(); + } + if f.alternate() { + hex = format!("0x{hex}"); + } + match f.width() { + None => f.write_str(hex.as_str()), + Some(w @ 1..=6) => f.write_str(&hex[..w]), + Some(w @ 7..=63) => { + let left = (w - 3) / 2; + let right = hex.len() - (w - left - 3); + f.write_str(format!("{}...{}", &hex[..left], &hex[right..]).as_str()) + }, + _ => core::fmt::Display::fmt(&hex, f), + } + } +} + +impl fmt::LowerHex for RistrettoPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_case(f, false) + } +} + +impl fmt::UpperHex for RistrettoPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_case(f, true) + } +} + +impl fmt::Debug for RistrettoPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +//------------------------------------ PublicKey PartialEq, Eq, Ord impl ---------------------------------------------// + +impl PartialEq for RistrettoPublicKey { + fn eq(&self, other: &RistrettoPublicKey) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for RistrettoPublicKey {} + +impl PartialOrd for RistrettoPublicKey { + fn partial_cmp(&self, other: &RistrettoPublicKey) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for RistrettoPublicKey { + fn cmp(&self, other: &Self) -> Ordering { + self.compressed().as_bytes().cmp(other.compressed().as_bytes()) + } +} + +//---------------------------------- PublicKey ByteArray implementation ---------------------------------------------// + +impl ByteArray for RistrettoPublicKey { + fn from_canonical_bytes(bytes: &[u8]) -> Result + where Self: Sized { + // Check the length here, because The Ristretto constructor panics rather than returning an error + if bytes.len() != 32 { + return Err(ByteArrayError::IncorrectLength {}); + } + let compressed = CompressedRistretto::from_slice(bytes).map_err(|_| ByteArrayError::ConversionError { + reason: "Invalid Public key".to_string(), + })?; + match RistrettoPublicKey::new_from_compressed(compressed) { + Some(p) => Ok(p), + None => Err(ByteArrayError::ConversionError { + reason: "Invalid compressed Ristretto point".to_string(), + }), + } + } + + /// Return the little-endian byte array representation of the compressed public key + fn as_bytes(&self) -> &[u8] { + self.compressed().as_bytes() + } +} + +//---------------------------------- PublicKey Add / Sub / Mul ---------------------------------------------// + +impl<'a> Add<&'a RistrettoPublicKey> for &RistrettoPublicKey { + type Output = RistrettoPublicKey; + + fn add(self, rhs: &'a RistrettoPublicKey) -> RistrettoPublicKey { + let p_sum = self.point + rhs.point; + RistrettoPublicKey::new_from_pk(p_sum) + } +} + +impl Sub<&RistrettoPublicKey> for &RistrettoPublicKey { + type Output = RistrettoPublicKey; + + fn sub(self, rhs: &RistrettoPublicKey) -> RistrettoPublicKey { + let p_sum = self.point - rhs.point; + RistrettoPublicKey::new_from_pk(p_sum) + } +} + +impl<'a> Mul<&'a RistrettoSecretKey> for &RistrettoPublicKey { + type Output = RistrettoPublicKey; + + fn mul(self, rhs: &'a RistrettoSecretKey) -> RistrettoPublicKey { + let p = rhs.0 * self.point; + RistrettoPublicKey::new_from_pk(p) + } +} + +impl<'a> Mul<&'a RistrettoSecretKey> for &RistrettoSecretKey { + type Output = RistrettoSecretKey; + + fn mul(self, rhs: &'a RistrettoSecretKey) -> RistrettoSecretKey { + let p = &rhs.0 * &self.0; + RistrettoSecretKey(p) + } +} + +define_add_variants!( + LHS = RistrettoPublicKey, + RHS = RistrettoPublicKey, + Output = RistrettoPublicKey +); +define_sub_variants!( + LHS = RistrettoPublicKey, + RHS = RistrettoPublicKey, + Output = RistrettoPublicKey +); +define_mul_variants!( + LHS = RistrettoPublicKey, + RHS = RistrettoSecretKey, + Output = RistrettoPublicKey +); +define_mul_variants!( + LHS = RistrettoSecretKey, + RHS = RistrettoSecretKey, + Output = RistrettoSecretKey +); + +//---------------------------------- PublicKey From implementations -------------------------------------// + +impl From for Scalar { + fn from(k: RistrettoSecretKey) -> Self { + k.0 + } +} + +impl From for RistrettoPoint { + fn from(pk: RistrettoPublicKey) -> Self { + pk.point + } +} + +impl From<&RistrettoPublicKey> for RistrettoPoint { + fn from(pk: &RistrettoPublicKey) -> Self { + pk.point + } +} diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/mod.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/mod.rs new file mode 100644 index 0000000000..4715173e1d --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2025. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod commitment; +pub mod commitment_and_public_key_signature; +pub mod commitment_factory; +pub mod hashing; +pub mod keys; +pub mod schnorr; diff --git a/applications/minotari_ledger_wallet/wallet/src/tari_crypto/schnorr.rs b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/schnorr.rs new file mode 100644 index 0000000000..98ba943812 --- /dev/null +++ b/applications/minotari_ledger_wallet/wallet/src/tari_crypto/schnorr.rs @@ -0,0 +1,242 @@ +// Copyright 2025. The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use core::{ + cmp::Ordering, + hash::{Hash, Hasher}, + marker::PhantomData, + ops::Add, +}; + +use blake2::Blake2b; +use digest::{consts::U64, Digest}; +use rand_core::{CryptoRng, RngCore}; +use tari_utilities::ByteArray; + +use crate::{ + hash_domain, + hashing::DomainSeparatedHash, + tari_crypto::{ + hashing::{DomainSeparatedHasher, DomainSeparation}, + keys::{RistrettoPublicKey, RistrettoSecretKey}, + }, +}; + +hash_domain!(SchnorrSigChallenge, "com.tari.schnorr_signature", 1); + +/// An error occurred during construction of a SchnorrSignature +#[derive(Clone, Debug, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum SchnorrSignatureError { + InvalidChallenge, +} + +#[allow(non_snake_case)] +#[derive(Debug, Clone)] +pub struct SchnorrSignature { + pub(crate) public_nonce: RistrettoPublicKey, + pub(crate) signature: RistrettoSecretKey, + _phantom: PhantomData, +} + +impl SchnorrSignature +where H: DomainSeparation +{ + /// Create a new `SchnorrSignature`. + pub fn new(public_nonce: RistrettoPublicKey, signature: RistrettoSecretKey) -> Self { + SchnorrSignature { + public_nonce, + signature, + _phantom: PhantomData, + } + } + + /// Calculates the signature verifier `s.G`. This must be equal to `R + eK`. + fn calc_signature_verifier(&self) -> RistrettoPublicKey { + RistrettoPublicKey::from_secret_key(&self.signature) + } + + pub fn sign_raw_uniform<'a>( + secret: &'a RistrettoSecretKey, + nonce: RistrettoSecretKey, + challenge: &[u8], + ) -> Result { + // s = r + e.k + let e = match RistrettoSecretKey::from_uniform_bytes(challenge) { + Ok(e) => e, + Err(_) => return Err(SchnorrSignatureError::InvalidChallenge), + }; + let public_nonce = RistrettoPublicKey::from_secret_key(&nonce); + let ek = e * secret; + let s = ek + nonce; + Ok(Self::new(public_nonce, s)) + } + + pub fn sign_raw_canonical<'a>( + secret: &'a RistrettoSecretKey, + nonce: RistrettoSecretKey, + challenge: &[u8], + ) -> Result { + // s = r + e.k + let e = match RistrettoSecretKey::from_canonical_bytes(challenge) { + Ok(e) => e, + Err(_) => return Err(SchnorrSignatureError::InvalidChallenge), + }; + let public_nonce = RistrettoPublicKey::from_secret_key(&nonce); + let ek = e * secret; + let s = ek + nonce; + Ok(Self::new(public_nonce, s)) + } + + pub fn sign<'a, B, R: RngCore + CryptoRng>( + secret: &'a RistrettoSecretKey, + message: B, + rng: &mut R, + ) -> Result + where + B: AsRef<[u8]>, + { + let nonce = RistrettoSecretKey::random(rng); + Self::sign_with_nonce_and_message(secret, nonce, message) + } + + pub fn sign_with_nonce_and_message<'a, B>( + secret: &'a RistrettoSecretKey, + nonce: RistrettoSecretKey, + message: B, + ) -> Result + where + B: AsRef<[u8]>, + { + let public_nonce = RistrettoPublicKey::from_secret_key(&nonce); + let public_key = RistrettoPublicKey::from_secret_key(secret); + let challenge = + Self::construct_domain_separated_challenge::<_, Blake2b>(&public_nonce, &public_key, message); + Self::sign_raw_uniform(secret, nonce, challenge.as_ref()) + } + + pub fn construct_domain_separated_challenge( + public_nonce: &RistrettoPublicKey, + public_key: &RistrettoPublicKey, + message: B, + ) -> DomainSeparatedHash + where + B: AsRef<[u8]>, + D: Digest, + { + DomainSeparatedHasher::::new_with_label("challenge") + .chain(public_nonce.as_bytes()) + .chain(public_key.as_bytes()) + .chain(message.as_ref()) + .finalize() + } + + pub fn verify<'a, B>(&self, public_key: &'a RistrettoPublicKey, message: B) -> bool + where B: AsRef<[u8]> { + let challenge = + Self::construct_domain_separated_challenge::<_, Blake2b>(&self.public_nonce, public_key, message); + self.verify_raw_uniform(public_key, challenge.as_ref()) + } + + pub fn verify_raw_uniform<'a>(&self, public_key: &'a RistrettoPublicKey, challenge: &[u8]) -> bool { + let e = match RistrettoSecretKey::from_uniform_bytes(challenge) { + Ok(e) => e, + Err(_) => return false, + }; + self.verify_challenge_scalar(public_key, &e) + } + + pub fn verify_raw_canonical<'a>(&self, public_key: &'a RistrettoPublicKey, challenge: &[u8]) -> bool { + let e = match RistrettoSecretKey::from_canonical_bytes(challenge) { + Ok(e) => e, + Err(_) => return false, + }; + self.verify_challenge_scalar(public_key, &e) + } + + pub fn verify_challenge_scalar<'a>( + &self, + public_key: &'a RistrettoPublicKey, + challenge: &RistrettoSecretKey, + ) -> bool { + // Reject a zero key + if public_key == &RistrettoPublicKey::default() { + return false; + } + + let lhs = self.calc_signature_verifier(); + let rhs = &self.public_nonce + challenge * public_key; + // Implementors should make this a constant time comparison + lhs == rhs + } + + pub fn get_signature(&self) -> &RistrettoSecretKey { + &self.signature + } + + pub fn get_public_nonce(&self) -> &RistrettoPublicKey { + &self.public_nonce + } +} + +impl<'a, 'b, H> Add<&'b SchnorrSignature> for &'a SchnorrSignature +where H: DomainSeparation +{ + type Output = SchnorrSignature; + + fn add(self, rhs: &'b SchnorrSignature) -> SchnorrSignature { + let r_sum = self.get_public_nonce() + rhs.get_public_nonce(); + let s_sum = self.get_signature() + rhs.get_signature(); + SchnorrSignature::new(r_sum, s_sum) + } +} + +impl<'a, H> Add> for &'a SchnorrSignature +where H: DomainSeparation +{ + type Output = SchnorrSignature; + + fn add(self, rhs: SchnorrSignature) -> SchnorrSignature { + let r_sum = self.get_public_nonce() + rhs.get_public_nonce(); + let s_sum = self.get_signature() + rhs.get_signature(); + SchnorrSignature::new(r_sum, s_sum) + } +} + +impl Default for SchnorrSignature +where H: DomainSeparation +{ + fn default() -> Self { + SchnorrSignature::new(RistrettoPublicKey::default(), RistrettoSecretKey::default()) + } +} + +impl Ord for SchnorrSignature { + fn cmp(&self, other: &Self) -> Ordering { + match self.public_nonce.cmp(&other.public_nonce) { + Ordering::Equal => self.signature.as_bytes().cmp(other.signature.as_bytes()), + v => v, + } + } +} + +impl PartialOrd for SchnorrSignature { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for SchnorrSignature {} + +impl PartialEq for SchnorrSignature { + fn eq(&self, other: &Self) -> bool { + self.public_nonce.eq(&other.public_nonce) && self.signature.eq(&other.signature) + } +} + +impl Hash for SchnorrSignature { + fn hash(&self, state: &mut T) { + self.public_nonce.hash(state); + self.signature.hash(state); + } +} diff --git a/applications/minotari_ledger_wallet/wallet/src/utils.rs b/applications/minotari_ledger_wallet/wallet/src/utils.rs index a577365fe2..6390db832a 100644 --- a/applications/minotari_ledger_wallet/wallet/src/utils.rs +++ b/applications/minotari_ledger_wallet/wallet/src/utils.rs @@ -15,22 +15,25 @@ use ledger_device_sdk::{ random::LedgerRng, }; use rand_core::RngCore; -use tari_crypto::{ - hashing::DomainSeparatedHasher, - keys::SecretKey, - ristretto::RistrettoSecretKey, - tari_utilities::ByteArray, -}; -use tari_hashing::{KeyManagerTransactionsHashDomain, LedgerHashDomain}; +use tari_utilities::ByteArray; use zeroize::Zeroizing; use crate::{ alloc::string::{String, ToString}, + hash_domain, + tari_crypto::{hashing::DomainSeparatedHasher, keys::RistrettoSecretKey}, AppSW, KeyType, BIP32_COIN_TYPE, }; +hash_domain!(LedgerHashDomain, "com.tari.minotari_ledger_wallet", 0); +hash_domain!( + KeyManagerTransactionsHashDomain, + "com.tari.base_layer.core.transactions.key_manager", + 1 +); + /// BIP32 path stored as an array of [`u32`]. /// /// # Generic arguments @@ -153,7 +156,7 @@ fn cx_error_to_string(e: CxError) -> String { // ever used in a subsequent key derivation function. fn get_raw_bip32_key(path: &[u32]) -> Result, String> { let mut key_buffer = Zeroizing::new([0u8; 64]); - match bip32_derive(CurvesId::Secp256k1, path, key_buffer.as_mut(), Some(&mut [])) { + match bip32_derive(CurvesId::Secp256k1, path, key_buffer.as_mut(), None) { Ok(_) => { if key_buffer.deref() == &[0u8; 64] { return Err(cx_error_to_string(CxError::InternalError)); @@ -161,7 +164,9 @@ fn get_raw_bip32_key(path: &[u32]) -> Result, String> { Ok(key_buffer) } }, - Err(e) => return Err(cx_error_to_string(e)), + Err(e) => { + return Err(cx_error_to_string(e)) + }, } } @@ -174,7 +179,6 @@ fn get_raw_key_hash(path: &[u32]) -> Result, String> { DomainSeparatedHasher::, LedgerHashDomain>::new_with_label("raw_key") .chain(&raw_key_64.as_ref()) .finalize_into(raw_key_hashed.as_mut().into()); - Ok(raw_key_hashed) } @@ -203,11 +207,13 @@ pub fn derive_from_bip32_key( Err(e) => { let mut msg = "".to_string(); msg.push_str("Err: raw key >>..."); + #[cfg(not(any(target_os = "stax", target_os = "flex")))] { SingleMessage::new(&msg).show_and_wait(); SingleMessage::new(&e).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new().text(&msg).show(false); @@ -233,6 +239,7 @@ pub fn get_key_from_uniform_bytes(bytes: &Zeroizing<[u8; 64]>) -> Result(bytes: &[u8]) -> Result Result { MessageScroller::new(&format!("Err: nonce conversion {:?}", e.to_string())).event_loop(); SingleMessage::new(&e.to_string()).show_and_wait(); } + #[cfg(any(target_os = "stax", target_os = "flex"))] { NbglStatus::new() @@ -319,3 +328,5 @@ pub fn get_random_nonce() -> Result { }, } } + +hash_domain!(TransactionHashDomain, "com.tari.base_layer.core.transactions", 0);