Skip to content

Commit c8d0a90

Browse files
committed
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<TaggedHash>, which each unsigned type implements. This allows the signing function to take any unsigned message and obtain its tagged hash.
1 parent 4bb4a97 commit c8d0a90

File tree

7 files changed

+181
-114
lines changed

7 files changed

+181
-114
lines changed

fuzz/src/invoice_request_deser.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
3838
if signing_pubkey == odd_pubkey || signing_pubkey == even_pubkey {
3939
unsigned_invoice
4040
.sign::<_, Infallible>(
41-
|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
41+
|message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
4242
)
4343
.unwrap()
4444
.write(&mut buffer)
4545
.unwrap();
4646
} else {
4747
unsigned_invoice
4848
.sign::<_, Infallible>(
49-
|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
49+
|message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
5050
)
5151
.unwrap_err();
5252
}
@@ -69,9 +69,9 @@ fn privkey(byte: u8) -> SecretKey {
6969
SecretKey::from_slice(&[byte; 32]).unwrap()
7070
}
7171

72-
fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>(
73-
invoice_request: &'a InvoiceRequest, secp_ctx: &Secp256k1<T>
74-
) -> Result<UnsignedBolt12Invoice<'a>, Bolt12SemanticError> {
72+
fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
73+
invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1<T>
74+
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
7575
let entropy_source = Randomness {};
7676
let paths = vec![
7777
BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(),

fuzz/src/offer_deser.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
3030
if let Ok(invoice_request) = build_response(&offer, pubkey) {
3131
invoice_request
3232
.sign::<_, Infallible>(
33-
|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
33+
|message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
3434
)
3535
.unwrap()
3636
.write(&mut buffer)
@@ -39,9 +39,9 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
3939
}
4040
}
4141

42-
fn build_response<'a>(
43-
offer: &'a Offer, pubkey: PublicKey
44-
) -> Result<UnsignedInvoiceRequest<'a>, Bolt12SemanticError> {
42+
fn build_response(
43+
offer: &Offer, pubkey: PublicKey
44+
) -> Result<UnsignedInvoiceRequest, Bolt12SemanticError> {
4545
let mut builder = offer.request_invoice(vec![42; 64], pubkey)?;
4646

4747
builder = match offer.amount() {

fuzz/src/refund_deser.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
3434
if let Ok(invoice) = build_response(&refund, pubkey, &secp_ctx) {
3535
invoice
3636
.sign::<_, Infallible>(
37-
|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))
37+
|message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
3838
)
3939
.unwrap()
4040
.write(&mut buffer)
@@ -58,9 +58,9 @@ fn privkey(byte: u8) -> SecretKey {
5858
SecretKey::from_slice(&[byte; 32]).unwrap()
5959
}
6060

61-
fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>(
62-
refund: &'a Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1<T>
63-
) -> Result<UnsignedBolt12Invoice<'a>, Bolt12SemanticError> {
61+
fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
62+
refund: &Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1<T>
63+
) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
6464
let entropy_source = Randomness {};
6565
let paths = vec![
6666
BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(),

lightning/src/offers/invoice.rs

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
//! .allow_mpp()
5656
//! .fallback_v0_p2wpkh(&wpubkey_hash)
5757
//! .build()?
58-
//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
58+
//! .sign::<_, Infallible>(
59+
//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
60+
//! )
5961
//! .expect("failed verifying signature")
6062
//! .write(&mut buffer)
6163
//! .unwrap();
@@ -84,7 +86,9 @@
8486
//! .allow_mpp()
8587
//! .fallback_v0_p2wpkh(&wpubkey_hash)
8688
//! .build()?
87-
//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
89+
//! .sign::<_, Infallible>(
90+
//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
91+
//! )
8892
//! .expect("failed verifying signature")
8993
//! .write(&mut buffer)
9094
//! .unwrap();
@@ -97,11 +101,11 @@ use bitcoin::blockdata::constants::ChainHash;
97101
use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
98102
use bitcoin::hashes::Hash;
99103
use bitcoin::network::constants::Network;
100-
use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self};
104+
use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self};
101105
use bitcoin::secp256k1::schnorr::Signature;
102106
use bitcoin::util::address::{Address, Payload, WitnessVersion};
103107
use bitcoin::util::schnorr::TweakedPublicKey;
104-
use core::convert::{Infallible, TryFrom};
108+
use core::convert::{AsRef, Infallible, TryFrom};
105109
use core::time::Duration;
106110
use crate::io;
107111
use crate::blinded_path::BlindedPath;
@@ -110,7 +114,7 @@ use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
110114
use crate::ln::inbound_payment::ExpandedKey;
111115
use crate::ln::msgs::DecodeError;
112116
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
113-
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self};
117+
use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self};
114118
use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef};
115119
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
116120
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
@@ -126,7 +130,8 @@ use std::time::SystemTime;
126130

127131
const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
128132

129-
pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
133+
/// Tag for the hash function used when signing a [`Bolt12Invoice`]'s merkle root.
134+
pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
130135

131136
/// Builds a [`Bolt12Invoice`] from either:
132137
/// - an [`InvoiceRequest`] for the "offer to be paid" flow or
@@ -331,15 +336,15 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> {
331336
impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
332337
/// Builds an unsigned [`Bolt12Invoice`] after checking for valid semantics. It can be signed by
333338
/// [`UnsignedBolt12Invoice::sign`].
334-
pub fn build(self) -> Result<UnsignedBolt12Invoice<'a>, Bolt12SemanticError> {
339+
pub fn build(self) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> {
335340
#[cfg(feature = "std")] {
336341
if self.invoice.is_offer_or_refund_expired() {
337342
return Err(Bolt12SemanticError::AlreadyExpired);
338343
}
339344
}
340345

341346
let InvoiceBuilder { invreq_bytes, invoice, .. } = self;
342-
Ok(UnsignedBolt12Invoice { invreq_bytes, invoice })
347+
Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice))
343348
}
344349
}
345350

@@ -355,23 +360,42 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
355360
}
356361

357362
let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self;
358-
let unsigned_invoice = UnsignedBolt12Invoice { invreq_bytes, invoice };
363+
let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice);
359364

360365
let keys = keys.unwrap();
361366
let invoice = unsigned_invoice
362-
.sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
367+
.sign::<_, Infallible>(
368+
|message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
369+
)
363370
.unwrap();
364371
Ok(invoice)
365372
}
366373
}
367374

368375
/// A semantically valid [`Bolt12Invoice`] that hasn't been signed.
369-
pub struct UnsignedBolt12Invoice<'a> {
370-
invreq_bytes: &'a Vec<u8>,
376+
pub struct UnsignedBolt12Invoice {
377+
bytes: Vec<u8>,
371378
invoice: InvoiceContents,
379+
tagged_hash: TaggedHash,
372380
}
373381

374-
impl<'a> UnsignedBolt12Invoice<'a> {
382+
impl UnsignedBolt12Invoice {
383+
fn new(invreq_bytes: &[u8], invoice: InvoiceContents) -> Self {
384+
// Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
385+
// have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
386+
// `RefundContents`.
387+
let (_, _, _, invoice_tlv_stream) = invoice.as_tlv_stream();
388+
let invoice_request_bytes = WithoutSignatures(invreq_bytes);
389+
let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
390+
391+
let mut bytes = Vec::new();
392+
unsigned_tlv_stream.write(&mut bytes).unwrap();
393+
394+
let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes);
395+
396+
Self { bytes, invoice, tagged_hash }
397+
}
398+
375399
/// The public key corresponding to the key needed to sign the invoice.
376400
pub fn signing_pubkey(&self) -> PublicKey {
377401
self.invoice.fields().signing_pubkey
@@ -380,37 +404,33 @@ impl<'a> UnsignedBolt12Invoice<'a> {
380404
/// Signs the invoice using the given function.
381405
///
382406
/// This is not exported to bindings users as functions aren't currently mapped.
383-
pub fn sign<F, E>(self, sign: F) -> Result<Bolt12Invoice, SignError<E>>
407+
pub fn sign<F, E>(mut self, sign: F) -> Result<Bolt12Invoice, SignError<E>>
384408
where
385-
F: FnOnce(&Message) -> Result<Signature, E>
409+
F: FnOnce(&Self) -> Result<Signature, E>
386410
{
387-
// Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
388-
// have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
389-
// `RefundContents`.
390-
let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream();
391-
let invoice_request_bytes = WithoutSignatures(self.invreq_bytes);
392-
let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
393-
394-
let mut bytes = Vec::new();
395-
unsigned_tlv_stream.write(&mut bytes).unwrap();
396-
397411
let pubkey = self.invoice.fields().signing_pubkey;
398-
let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?;
412+
let signature = merkle::sign_message(sign, &self, pubkey)?;
399413

400414
// Append the signature TLV record to the bytes.
401415
let signature_tlv_stream = SignatureTlvStreamRef {
402416
signature: Some(&signature),
403417
};
404-
signature_tlv_stream.write(&mut bytes).unwrap();
418+
signature_tlv_stream.write(&mut self.bytes).unwrap();
405419

406420
Ok(Bolt12Invoice {
407-
bytes,
421+
bytes: self.bytes,
408422
contents: self.invoice,
409423
signature,
410424
})
411425
}
412426
}
413427

428+
impl AsRef<TaggedHash> for UnsignedBolt12Invoice {
429+
fn as_ref(&self) -> &TaggedHash {
430+
&self.tagged_hash
431+
}
432+
}
433+
414434
/// A `Bolt12Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
415435
///
416436
/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
@@ -1686,15 +1706,14 @@ mod tests {
16861706
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
16871707
.build().unwrap()
16881708
.sign(payer_sign).unwrap();
1689-
let mut unsigned_invoice = invoice_request
1709+
let mut invoice_builder = invoice_request
16901710
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
16911711
.fallback_v0_p2wsh(&script.wscript_hash())
16921712
.fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap())
1693-
.fallback_v1_p2tr_tweaked(&tweaked_pubkey)
1694-
.build().unwrap();
1713+
.fallback_v1_p2tr_tweaked(&tweaked_pubkey);
16951714

16961715
// Only standard addresses will be included.
1697-
let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap();
1716+
let fallbacks = invoice_builder.invoice.fields_mut().fallbacks.as_mut().unwrap();
16981717
// Non-standard addresses
16991718
fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] });
17001719
fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] });
@@ -1703,7 +1722,7 @@ mod tests {
17031722
fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 33] });
17041723
fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 40] });
17051724

1706-
let invoice = unsigned_invoice.sign(recipient_sign).unwrap();
1725+
let invoice = invoice_builder.build().unwrap().sign(recipient_sign).unwrap();
17071726
let mut buffer = Vec::new();
17081727
invoice.write(&mut buffer).unwrap();
17091728

0 commit comments

Comments
 (0)