From: Jeffrey Czyz Date: Mon, 27 Feb 2023 20:23:05 +0000 (-0600) Subject: TaggedHash for BOLT 12 signing function X-Git-Tag: v0.0.117-alpha1~49^2~13 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=1811ebff32e5f32aaf80e9f14bfeef12a47a35c6;p=rust-lightning TaggedHash for BOLT 12 signing function The function used to sign BOLT 12 messages only takes a message digest. This doesn't allow signers to independently verify the message before signing nor does it allow them to derive the necessary signing keys, if needed. Introduce a TaggedHash wrapper for a message digest, which each unsigned BOLT 12 message type constructs upon initialization. Change the signing function to take AsRef, which each unsigned type implements. This allows the signing function to take any unsigned message and obtain its tagged hash. --- diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index ca9d06ab..22a2258f 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -38,7 +38,7 @@ pub fn do_test(data: &[u8], _out: Out) { if signing_pubkey == odd_pubkey || signing_pubkey == even_pubkey { unsigned_invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -46,7 +46,7 @@ pub fn do_test(data: &[u8], _out: Out) { } else { unsigned_invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap_err(); } @@ -69,9 +69,9 @@ fn privkey(byte: u8) -> SecretKey { SecretKey::from_slice(&[byte; 32]).unwrap() } -fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( - invoice_request: &'a InvoiceRequest, secp_ctx: &Secp256k1 -) -> Result, Bolt12SemanticError> { +fn build_response( + invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1 +) -> Result { let entropy_source = Randomness {}; let paths = vec![ BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs index 53f67a33..e16c3b41 100644 --- a/fuzz/src/offer_deser.rs +++ b/fuzz/src/offer_deser.rs @@ -30,7 +30,7 @@ pub fn do_test(data: &[u8], _out: Out) { if let Ok(invoice_request) = build_response(&offer, pubkey) { invoice_request .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -39,9 +39,9 @@ pub fn do_test(data: &[u8], _out: Out) { } } -fn build_response<'a>( - offer: &'a Offer, pubkey: PublicKey -) -> Result, Bolt12SemanticError> { +fn build_response( + offer: &Offer, pubkey: PublicKey +) -> Result { let mut builder = offer.request_invoice(vec![42; 64], pubkey)?; builder = match offer.amount() { diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs index 81b614d6..fd273d7e 100644 --- a/fuzz/src/refund_deser.rs +++ b/fuzz/src/refund_deser.rs @@ -34,7 +34,7 @@ pub fn do_test(data: &[u8], _out: Out) { if let Ok(invoice) = build_response(&refund, pubkey, &secp_ctx) { invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -58,9 +58,9 @@ fn privkey(byte: u8) -> SecretKey { SecretKey::from_slice(&[byte; 32]).unwrap() } -fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( - refund: &'a Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 -) -> Result, Bolt12SemanticError> { +fn build_response( + refund: &Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 +) -> Result { let entropy_source = Randomness {}; let paths = vec![ BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index c3d4500a..773e1ead 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -55,7 +55,9 @@ //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -84,7 +86,9 @@ //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -97,11 +101,11 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; -use core::convert::{Infallible, TryFrom}; +use core::convert::{AsRef, Infallible, TryFrom}; use core::time::Duration; use crate::io; use crate::blinded_path::BlindedPath; @@ -110,7 +114,7 @@ use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; @@ -126,7 +130,8 @@ use std::time::SystemTime; const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); -pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); +/// Tag for the hash function used when signing a [`Bolt12Invoice`]'s merkle root. +pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); /// Builds a [`Bolt12Invoice`] from either: /// - an [`InvoiceRequest`] for the "offer to be paid" flow or @@ -331,7 +336,7 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { /// Builds an unsigned [`Bolt12Invoice`] after checking for valid semantics. It can be signed by /// [`UnsignedBolt12Invoice::sign`]. - pub fn build(self) -> Result, Bolt12SemanticError> { + pub fn build(self) -> Result { #[cfg(feature = "std")] { if self.invoice.is_offer_or_refund_expired() { return Err(Bolt12SemanticError::AlreadyExpired); @@ -339,7 +344,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, .. } = self; - Ok(UnsignedBolt12Invoice { invreq_bytes, invoice }) + Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice)) } } @@ -355,23 +360,42 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; - let unsigned_invoice = UnsignedBolt12Invoice { invreq_bytes, invoice }; + let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); let keys = keys.unwrap(); let invoice = unsigned_invoice - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); Ok(invoice) } } /// A semantically valid [`Bolt12Invoice`] that hasn't been signed. -pub struct UnsignedBolt12Invoice<'a> { - invreq_bytes: &'a Vec, +pub struct UnsignedBolt12Invoice { + bytes: Vec, invoice: InvoiceContents, + tagged_hash: TaggedHash, } -impl<'a> UnsignedBolt12Invoice<'a> { +impl UnsignedBolt12Invoice { + fn new(invreq_bytes: &[u8], invoice: InvoiceContents) -> Self { + // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may + // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or + // `RefundContents`. + let (_, _, _, invoice_tlv_stream) = invoice.as_tlv_stream(); + let invoice_request_bytes = WithoutSignatures(invreq_bytes); + let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream); + + let mut bytes = Vec::new(); + unsigned_tlv_stream.write(&mut bytes).unwrap(); + + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Self { bytes, invoice, tagged_hash } + } + /// The public key corresponding to the key needed to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { self.invoice.fields().signing_pubkey @@ -380,37 +404,33 @@ impl<'a> UnsignedBolt12Invoice<'a> { /// Signs the invoice using the given function. /// /// This is not exported to bindings users as functions aren't currently mapped. - pub fn sign(self, sign: F) -> Result> + pub fn sign(mut self, sign: F) -> Result> where - F: FnOnce(&Message) -> Result + F: FnOnce(&Self) -> Result { - // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may - // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or - // `RefundContents`. - let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream(); - let invoice_request_bytes = WithoutSignatures(self.invreq_bytes); - let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream); - - let mut bytes = Vec::new(); - unsigned_tlv_stream.write(&mut bytes).unwrap(); - let pubkey = self.invoice.fields().signing_pubkey; - let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?; + let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature), }; - signature_tlv_stream.write(&mut bytes).unwrap(); + signature_tlv_stream.write(&mut self.bytes).unwrap(); Ok(Bolt12Invoice { - bytes, + bytes: self.bytes, contents: self.invoice, signature, }) } } +impl AsRef for UnsignedBolt12Invoice { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + /// A `Bolt12Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`]. /// /// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent @@ -1686,15 +1706,14 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let mut unsigned_invoice = invoice_request + let mut invoice_builder = invoice_request .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) - .fallback_v1_p2tr_tweaked(&tweaked_pubkey) - .build().unwrap(); + .fallback_v1_p2tr_tweaked(&tweaked_pubkey); // Only standard addresses will be included. - let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + let fallbacks = invoice_builder.invoice.fields_mut().fallbacks.as_mut().unwrap(); // Non-standard addresses fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] }); fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] }); @@ -1703,7 +1722,7 @@ mod tests { fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 33] }); fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 40] }); - let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); + let invoice = invoice_builder.build().unwrap().sign(recipient_sign).unwrap(); let mut buffer = Vec::new(); invoice.write(&mut buffer).unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index f014bf12..1dea6503 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -44,7 +44,9 @@ //! .quantity(5)? //! .payer_note("foo".to_string()) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -54,9 +56,9 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; -use core::convert::{Infallible, TryFrom}; +use core::convert::{AsRef, Infallible, TryFrom}; use core::ops::Deref; use crate::sign::EntropySource; use crate::io; @@ -66,7 +68,7 @@ use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -76,7 +78,8 @@ use crate::util::string::PrintableString; use crate::prelude::*; -const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); +/// Tag for the hash function used when signing an [`InvoiceRequest`]'s merkle root. +pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; @@ -214,7 +217,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a } fn build_with_checks(mut self) -> Result< - (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>), + (UnsignedInvoiceRequest, Option, Option<&'b Secp256k1>), Bolt12SemanticError > { #[cfg(feature = "std")] { @@ -245,7 +248,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a } fn build_without_checks(mut self) -> - (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>) + (UnsignedInvoiceRequest, Option, Option<&'b Secp256k1>) { // Create the metadata for stateless verification of a Bolt12Invoice. let mut keys = None; @@ -275,22 +278,20 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a debug_assert!(self.payer_id.is_some()); let payer_id = self.payer_id.unwrap(); - let unsigned_invoice = UnsignedInvoiceRequest { - offer: self.offer, - invoice_request: InvoiceRequestContents { - inner: self.invoice_request, - payer_id, - }, + let invoice_request = InvoiceRequestContents { + inner: self.invoice_request, + payer_id, }; + let unsigned_invoice_request = UnsignedInvoiceRequest::new(self.offer, invoice_request); - (unsigned_invoice, keys, secp_ctx) + (unsigned_invoice_request, keys, secp_ctx) } } impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> { /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed /// by [`UnsignedInvoiceRequest::sign`]. - pub fn build(self) -> Result, Bolt12SemanticError> { + pub fn build(self) -> Result { let (unsigned_invoice_request, keys, _) = self.build_with_checks()?; debug_assert!(keys.is_none()); Ok(unsigned_invoice_request) @@ -306,7 +307,9 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId let secp_ctx = secp_ctx.unwrap(); let keys = keys.unwrap(); let invoice_request = unsigned_invoice_request - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); Ok(invoice_request) } @@ -335,52 +338,65 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a self } - pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { + pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest { self.build_without_checks().0 } } /// A semantically valid [`InvoiceRequest`] that hasn't been signed. -pub struct UnsignedInvoiceRequest<'a> { - offer: &'a Offer, +pub struct UnsignedInvoiceRequest { + bytes: Vec, invoice_request: InvoiceRequestContents, + tagged_hash: TaggedHash, } -impl<'a> UnsignedInvoiceRequest<'a> { - /// Signs the invoice request using the given function. - /// - /// This is not exported to bindings users as functions are not yet mapped. - pub fn sign(self, sign: F) -> Result> - where - F: FnOnce(&Message) -> Result - { +impl UnsignedInvoiceRequest { + fn new(offer: &Offer, invoice_request: InvoiceRequestContents) -> Self { // Use the offer bytes instead of the offer TLV stream as the offer may have contained // unknown TLV records, which are not stored in `OfferContents`. let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = - self.invoice_request.as_tlv_stream(); - let offer_bytes = WithoutLength(&self.offer.bytes); + invoice_request.as_tlv_stream(); + let offer_bytes = WithoutLength(&offer.bytes); let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); let mut bytes = Vec::new(); unsigned_tlv_stream.write(&mut bytes).unwrap(); + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Self { bytes, invoice_request, tagged_hash } + } + + /// Signs the invoice request using the given function. + /// + /// This is not exported to bindings users as functions are not yet mapped. + pub fn sign(mut self, sign: F) -> Result> + where + F: FnOnce(&Self) -> Result + { let pubkey = self.invoice_request.payer_id; - let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?; + let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature), }; - signature_tlv_stream.write(&mut bytes).unwrap(); + signature_tlv_stream.write(&mut self.bytes).unwrap(); Ok(InvoiceRequest { - bytes, + bytes: self.bytes, contents: self.invoice_request, signature, }) } } +impl AsRef for UnsignedInvoiceRequest { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + /// An `InvoiceRequest` is a request for a [`Bolt12Invoice`] formulated from an [`Offer`]. /// /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request @@ -591,7 +607,7 @@ impl InvoiceRequest { } impl InvoiceRequestContents { - pub fn metadata(&self) -> &[u8] { + pub(super) fn metadata(&self) -> &[u8] { self.inner.metadata() } @@ -790,7 +806,7 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; - use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; @@ -922,9 +938,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -946,9 +961,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -992,9 +1006,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -1016,9 +1029,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -1771,7 +1783,9 @@ mod tests { .build().unwrap() .request_invoice(vec![1; 32], keys.public_key()).unwrap() .build().unwrap() - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); let mut encoded_invoice_request = Vec::new(); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index f7c33902..d15039cd 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -12,6 +12,7 @@ use bitcoin::hashes::{Hash, HashEngine, sha256}; use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; +use core::convert::AsRef; use crate::io; use crate::util::ser::{BigSize, Readable, Writeable, Writer}; @@ -24,6 +25,33 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { (240, signature: Signature), }); +/// A hash for use in a specific context by tweaking with a context-dependent tag as per [BIP 340] +/// and computed over the merkle root of a TLV stream to sign as defined in [BOLT 12]. +/// +/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +/// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation +pub struct TaggedHash(Message); + +impl TaggedHash { + /// Creates a tagged hash with the given parameters. + /// + /// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record. + pub(super) fn new(tag: &str, tlv_stream: &[u8]) -> Self { + Self(message_digest(tag, tlv_stream)) + } + + /// Returns the digest to sign. + pub fn as_digest(&self) -> &Message { + &self.0 + } +} + +impl AsRef for TaggedHash { + fn as_ref(&self) -> &TaggedHash { + self + } +} + /// Error when signing messages. #[derive(Debug, PartialEq)] pub enum SignError { @@ -33,22 +61,28 @@ pub enum SignError { Verification(secp256k1::Error), } -/// Signs a message digest consisting of a tagged hash of the given bytes, checking if it can be -/// verified with the supplied pubkey. +/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream, checking if it +/// can be verified with the supplied `pubkey`. /// -/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. -pub(super) fn sign_message( - sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +/// Since `message` is any type that implements [`AsRef`], `sign` may be a closure that +/// takes a message such as [`Bolt12Invoice`] or [`InvoiceRequest`]. This allows further message +/// verification before signing its [`TaggedHash`]. +/// +/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub(super) fn sign_message( + sign: F, message: &T, pubkey: PublicKey, ) -> Result> where - F: FnOnce(&Message) -> Result + F: FnOnce(&T) -> Result, + T: AsRef, { - let digest = message_digest(tag, bytes); - let signature = sign(&digest).map_err(|e| SignError::Signing(e))?; + let signature = sign(message).map_err(|e| SignError::Signing(e))?; + let digest = message.as_ref().as_digest(); let pubkey = pubkey.into(); let secp_ctx = Secp256k1::verification_only(); - secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?; + secp_ctx.verify_schnorr(&signature, digest, &pubkey).map_err(|e| SignError::Verification(e))?; Ok(signature) } @@ -207,12 +241,12 @@ impl<'a> Iterator for TlvStream<'a> { /// Encoding for a pre-serialized TLV stream that excludes any signature TLV records. /// /// Panics if the wrapped bytes are not a well-formed TLV stream. -pub(super) struct WithoutSignatures<'a>(pub &'a Vec); +pub(super) struct WithoutSignatures<'a>(pub &'a [u8]); impl<'a> Writeable for WithoutSignatures<'a> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let tlv_stream = TlvStream::new(&self.0[..]); + let tlv_stream = TlvStream::new(self.0); for record in tlv_stream.skip_signatures() { writer.write_all(record.record_bytes)?; } @@ -271,7 +305,9 @@ mod tests { .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() .build_unchecked() - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); assert_eq!( invoice_request.to_string(), @@ -304,7 +340,9 @@ mod tests { .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() .build_unchecked() - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); let mut bytes_without_signature = Vec::new(); @@ -334,7 +372,9 @@ mod tests { .build_unchecked() .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() .build_unchecked() - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); let tlv_stream = TlvStream::new(&invoice_request.bytes).range(0..1) diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs index 230c6aa1..f1b3c79e 100644 --- a/lightning/src/offers/test_utils.rs +++ b/lightning/src/offers/test_utils.rs @@ -9,25 +9,26 @@ //! Utilities for testing BOLT 12 Offers interfaces -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; use bitcoin::secp256k1::schnorr::Signature; -use core::convert::Infallible; +use core::convert::{AsRef, Infallible}; use core::time::Duration; use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::sign::EntropySource; use crate::ln::PaymentHash; use crate::ln::features::BlindedHopFeatures; use crate::offers::invoice::BlindedPayInfo; +use crate::offers::merkle::TaggedHash; pub(super) fn payer_keys() -> KeyPair { let secp_ctx = Secp256k1::new(); KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) } -pub(super) fn payer_sign(digest: &Message) -> Result { +pub(super) fn payer_sign>(message: &T) -> Result { let secp_ctx = Secp256k1::new(); let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) } pub(super) fn payer_pubkey() -> PublicKey { @@ -39,10 +40,10 @@ pub(super) fn recipient_keys() -> KeyPair { KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) } -pub(super) fn recipient_sign(digest: &Message) -> Result { +pub(super) fn recipient_sign>(message: &T) -> Result { let secp_ctx = Secp256k1::new(); let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); - Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) } pub(super) fn recipient_pubkey() -> PublicKey {