|
| 1 | +//! # Order memo signature format |
| 2 | +//! |
| 3 | +//! When providing a Tari address in a Shopify order, we cannot let users provide just any wallet address there, |
| 4 | +//! because this would let folks put other wallet addresses in there and hope that one day someone makes a payment |
| 5 | +//! from that wallet and their order will then be fulfilled. |
| 6 | +//! |
| 7 | +//! Users need to _prove_ that they own the wallet address they provide in the order. This is done by signing a message |
| 8 | +//! with the wallet's private key. The message is constructed from the wallet address and the order ID (preventing |
| 9 | +//! naughty people from using the same signature for their own orders, and again, trying to get free stuff). |
| 10 | +//! |
| 11 | +//! The signature is then stored in the order memo field, and the payment server can verify the signature by checking |
| 12 | +//! the wallet's public key against the signature. |
| 13 | +//! |
| 14 | +//! ## Message format |
| 15 | +//! |
| 16 | +//! The message is constructed by concatenating the wallet address and the order ID, separated by a colon. |
| 17 | +//! The challenge is a domain-separated Schnorr signature. The full format is: |
| 18 | +//! |
| 19 | +//! ```text |
| 20 | +//! {aaaaaaaa}MemoSignature.v1.challenge{bbbbbbbb}{address}:{order_id} |
| 21 | +//! ``` |
| 22 | +//! |
| 23 | +//! where |
| 24 | +//! * `aaaaaaaa` is the length of `MemoSignature.v1.challenge`, i.e. 25 in little-endian format. |
| 25 | +//! * `bbbbbbbb` is the length of `address`(64) + `:`(1) + `order_id.len()` in little-endian format. |
| 26 | +//! * `address` is the Tari address of the wallet owner, in hexadecimal |
| 27 | +//! * `order_id` is the order ID, a string |
| 28 | +//! |
| 29 | +//! The message is then hashed with `Blake2b<U64>` to get the challenge. |
| 30 | +
|
| 31 | +use serde::{Deserialize, Serialize}; |
| 32 | +use tari_common_types::tari_address::TariAddress; |
| 33 | +use tari_jwt::tari_crypto::{ |
| 34 | + hash_domain, |
| 35 | + ristretto::{RistrettoPublicKey, RistrettoSchnorrWithDomain, RistrettoSecretKey}, |
| 36 | + signatures::SchnorrSignatureError, |
| 37 | + tari_utilities::{hex::Hex, message_format::MessageFormat}, |
| 38 | +}; |
| 39 | +use tari_payment_engine::db_types::SerializedTariAddress; |
| 40 | +use thiserror::Error; |
| 41 | + |
| 42 | +hash_domain!(MemoSignatureDomain, "MemoSignature"); |
| 43 | + |
| 44 | +pub type MemoSchnorr = RistrettoSchnorrWithDomain<MemoSignatureDomain>; |
| 45 | + |
| 46 | +#[derive(Debug, Clone, Error)] |
| 47 | +#[error("Invalid memo signature: {0}")] |
| 48 | +pub struct MemoSignatureError(String); |
| 49 | + |
| 50 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 51 | +pub struct MemoSignature { |
| 52 | + pub address: SerializedTariAddress, |
| 53 | + pub order_id: String, |
| 54 | + #[serde(serialize_with = "ser_sig", deserialize_with = "de_sig")] |
| 55 | + pub signature: MemoSchnorr, |
| 56 | +} |
| 57 | + |
| 58 | +impl MemoSignature { |
| 59 | + pub fn create( |
| 60 | + address: TariAddress, |
| 61 | + order_id: String, |
| 62 | + secret_key: &RistrettoSecretKey, |
| 63 | + ) -> Result<Self, MemoSignatureError> { |
| 64 | + let address = SerializedTariAddress::from(address); |
| 65 | + let message = signature_message(&address, &order_id); |
| 66 | + let signature = sign_message(&message, secret_key).map_err(|e| MemoSignatureError(e.to_string()))?; |
| 67 | + Ok(Self { address, order_id, signature }) |
| 68 | + } |
| 69 | + |
| 70 | + pub fn new(address: &str, order_id: &str, signature: &str) -> Result<Self, MemoSignatureError> { |
| 71 | + let address = address.parse::<SerializedTariAddress>().map_err(|e| MemoSignatureError(e.to_string()))?; |
| 72 | + let signature = hex_to_memo_schnorr(signature).map_err(|e| MemoSignatureError(e.to_string()))?; |
| 73 | + let order_id = order_id.to_string(); |
| 74 | + Ok(Self { address, order_id, signature }) |
| 75 | + } |
| 76 | + |
| 77 | + pub fn message(&self) -> String { |
| 78 | + signature_message(&self.address, &self.order_id) |
| 79 | + } |
| 80 | + |
| 81 | + pub fn is_valid(&self) -> bool { |
| 82 | + let message = self.message(); |
| 83 | + let pubkey = self.address.as_address().public_key(); |
| 84 | + println!( |
| 85 | + "Verifying. pubkey: {:x}. nonce: {:x}, sig:{}", |
| 86 | + pubkey, |
| 87 | + self.signature.get_public_nonce(), |
| 88 | + self.signature.get_signature().reveal().to_string() |
| 89 | + ); |
| 90 | + self.signature.verify(pubkey, message) |
| 91 | + } |
| 92 | + |
| 93 | + pub fn as_json(&self) -> String { |
| 94 | + serde_json::to_string(self).unwrap() |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +pub fn signature_message(address: &SerializedTariAddress, order_id: &str) -> String { |
| 99 | + let addr = address.as_address().to_hex(); |
| 100 | + format!("{addr}:{order_id}") |
| 101 | +} |
| 102 | + |
| 103 | +pub fn sign_message(message: &str, secret_key: &RistrettoSecretKey) -> Result<MemoSchnorr, SchnorrSignatureError> { |
| 104 | + let mut rng = rand::thread_rng(); |
| 105 | + MemoSchnorr::sign(secret_key, message.as_bytes(), &mut rng) |
| 106 | +} |
| 107 | + |
| 108 | +pub fn ser_sig<S>(sig: &MemoSchnorr, s: S) -> Result<S::Ok, S::Error> |
| 109 | +where S: serde::Serializer { |
| 110 | + let nonce = sig.get_public_nonce().to_hex(); |
| 111 | + let sig = sig.get_signature().to_hex(); |
| 112 | + s.serialize_str(&format!("{nonce}{sig}")) |
| 113 | +} |
| 114 | + |
| 115 | +pub fn de_sig<'de, D>(d: D) -> Result<MemoSchnorr, D::Error> |
| 116 | +where D: serde::Deserializer<'de> { |
| 117 | + let s = String::deserialize(d)?; |
| 118 | + hex_to_memo_schnorr(&s).map_err(serde::de::Error::custom) |
| 119 | +} |
| 120 | + |
| 121 | +pub fn hex_to_memo_schnorr(s: &str) -> Result<MemoSchnorr, MemoSignatureError> { |
| 122 | + if s.len() != 128 { |
| 123 | + return Err(MemoSignatureError("Invalid signature length".into())); |
| 124 | + } |
| 125 | + let nonce = RistrettoPublicKey::from_hex(&s[..64]) |
| 126 | + .map_err(|e| MemoSignatureError(format!("Signature contains an invalid public nonce. {e}")))?; |
| 127 | + let sig = RistrettoSecretKey::from_hex(&s[64..]) |
| 128 | + .map_err(|e| MemoSignatureError(format!("Signature contains an invalid signature key. {e}")))?; |
| 129 | + Ok(MemoSchnorr::new(nonce, sig)) |
| 130 | +} |
| 131 | + |
| 132 | +#[cfg(test)] |
| 133 | +mod test { |
| 134 | + use log::info; |
| 135 | + |
| 136 | + use super::*; |
| 137 | + |
| 138 | + // These tests use this address |
| 139 | + // ----------------------------- Tari Address ----------------------------- |
| 140 | + // Network: mainnet |
| 141 | + // Secret key: 1dbbce83de2b0233c404b96b9234233bb3cec51503e2124d8c728a2d9b4fb00c |
| 142 | + // Public key: a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e547 |
| 143 | + // Address: a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733 |
| 144 | + // Emoji ID: 👽🔥🍓🐗🎼😉🍊👘🍁🔮🐎👘👣👙🎮💨🍆🐑🏉🐬🎷👒🍪🚜💦🚌👽💼🐼🐬😍🎡🍰 |
| 145 | + // ------------------------------------------------------------------------ |
| 146 | + |
| 147 | + fn secret_key() -> RistrettoSecretKey { |
| 148 | + RistrettoSecretKey::from_hex("1dbbce83de2b0233c404b96b9234233bb3cec51503e2124d8c728a2d9b4fb00c").unwrap() |
| 149 | + } |
| 150 | + |
| 151 | + #[test] |
| 152 | + fn create_memo_signature() { |
| 153 | + let address = "a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733" |
| 154 | + .parse() |
| 155 | + .expect("Failed to parse TariAddress"); |
| 156 | + let sig = |
| 157 | + MemoSignature::create(address, "oid554432".into(), &secret_key()).expect("Failed to create memo signature"); |
| 158 | + println!("{}", sig.as_json()); |
| 159 | + let msg = signature_message(&sig.address, &sig.order_id); |
| 160 | + assert_eq!(msg, "a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733:oid554432"); |
| 161 | + assert_eq!( |
| 162 | + sig.address.as_address().to_hex(), |
| 163 | + "a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733" |
| 164 | + ); |
| 165 | + assert_eq!(sig.order_id, "oid554432"); |
| 166 | + assert!(sig.is_valid()); |
| 167 | + } |
| 168 | + |
| 169 | + #[test] |
| 170 | + fn verify_from_json() { |
| 171 | + let json = r#"{ |
| 172 | + "address": "a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733", |
| 173 | + "order_id": "oid554432", |
| 174 | + "signature": "2421e3c98522d7c5518f55ddb39f759ee9051dde8060679d48f257994372fb214e9024917a5befacb132fc9979527ff92daa2c5d42062b8a507dc4e3b6954c05" |
| 175 | + }"#; |
| 176 | + let sig = serde_json::from_str::<MemoSignature>(json).expect("Failed to deserialize memo signature"); |
| 177 | + assert_eq!( |
| 178 | + sig.address.as_address().to_hex(), |
| 179 | + "a8d523755de41b9c14de709ca59d52bc1772658258962ef5bbefa8c59082e54733" |
| 180 | + ); |
| 181 | + assert_eq!(sig.order_id, "oid554432"); |
| 182 | + assert!(sig.is_valid()); |
| 183 | + } |
| 184 | +} |
0 commit comments