Skip to content

Commit 285a45c

Browse files
offer: improve merkle tree signature API with validation
This commit addresses review feedback by adding a validating version of the merkle tree signature functions that returns proper error types instead of panicking on invalid input. The new from_tlv_stream_bytes function validates TLV streams before processing and returns a Result, making it safer for command line tools and external applications to use. Additionally, documentation has been improved to clarify that these are low-level functions for specific use cases, with clear guidance to use higher-level methods for production. Comprehensive tests have been added to ensure consistency between the validating and non-validating functions and prevent regressions. Signed-off-by: Vincenzo Palazzo <[email protected]>
1 parent be23002 commit 285a45c

File tree

1 file changed

+188
-5
lines changed

1 file changed

+188
-5
lines changed

lightning/src/offers/merkle.rs

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,64 @@ impl TaggedHash {
4141
/// Creates a tagged hash with the given parameters.
4242
///
4343
/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record.
44-
pub fn from_valid_tlv_stream_bytes(tag: &'static str, bytes: &[u8]) -> Self {
44+
pub(super) fn from_valid_tlv_stream_bytes(tag: &'static str, bytes: &[u8]) -> Self {
4545
let tlv_stream = TlvStream::new(bytes);
4646
Self::from_tlv_stream(tag, tlv_stream)
4747
}
4848

49+
/// Creates a tagged hash with the given parameters, validating the TLV stream.
50+
///
51+
/// This is a low-level function exposed for specific use cases like command-line tools
52+
/// and testing. For production use, prefer higher-level methods like
53+
/// [`Bolt12Invoice::try_from`] which handle validation automatically.
54+
///
55+
/// Returns an error if `bytes` is not a well-formed TLV stream containing at least one TLV record.
56+
///
57+
/// [`Bolt12Invoice::try_from`]: crate::offers::invoice::Bolt12Invoice::try_from
58+
pub fn from_tlv_stream_bytes(tag: &'static str, bytes: &[u8]) -> Result<Self, TlvStreamError> {
59+
// Validate the TLV stream first
60+
if bytes.is_empty() {
61+
return Err(TlvStreamError::EmptyStream);
62+
}
63+
64+
// Try to parse the TLV stream to check validity
65+
let mut cursor = io::Cursor::new(bytes);
66+
let mut has_records = false;
67+
68+
while cursor.position() < bytes.len() as u64 {
69+
// Try to read type
70+
let type_result = <BigSize as Readable>::read(&mut cursor);
71+
if type_result.is_err() {
72+
return Err(TlvStreamError::InvalidRecord);
73+
}
74+
75+
// Try to read length
76+
let length_result = <BigSize as Readable>::read(&mut cursor);
77+
if length_result.is_err() {
78+
return Err(TlvStreamError::InvalidRecord);
79+
}
80+
81+
let length = length_result.unwrap().0;
82+
let end_position = cursor.position() + length;
83+
84+
// Check if the record extends beyond the buffer
85+
if end_position > bytes.len() as u64 {
86+
return Err(TlvStreamError::InvalidRecord);
87+
}
88+
89+
// Skip the value
90+
cursor.set_position(end_position);
91+
has_records = true;
92+
}
93+
94+
if !has_records {
95+
return Err(TlvStreamError::EmptyStream);
96+
}
97+
98+
// If validation passes, create the tagged hash
99+
Ok(Self::from_valid_tlv_stream_bytes(tag, bytes))
100+
}
101+
49102
/// Creates a tagged hash with the given parameters.
50103
///
51104
/// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record.
@@ -93,8 +146,17 @@ pub enum SignError {
93146
Verification(secp256k1::Error),
94147
}
95148

149+
/// Error when parsing TLV streams.
150+
#[derive(Debug, PartialEq)]
151+
pub enum TlvStreamError {
152+
/// The TLV stream is empty (contains no records).
153+
EmptyStream,
154+
/// The TLV stream contains an invalid record.
155+
InvalidRecord,
156+
}
157+
96158
/// A function for signing a [`TaggedHash`].
97-
pub(super) trait SignFn<T: AsRef<TaggedHash>> {
159+
pub trait SignFn<T: AsRef<TaggedHash>> {
98160
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
99161
fn sign(&self, message: &T) -> Result<Signature, ()>;
100162
}
@@ -111,15 +173,17 @@ where
111173
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream, checking if it
112174
/// can be verified with the supplied `pubkey`.
113175
///
176+
/// This is a low-level function exposed for specific use cases like command-line tools
177+
/// and testing. For production use, prefer higher-level methods on invoice types that handle
178+
/// signing automatically.
179+
///
114180
/// Since `message` is any type that implements [`AsRef<TaggedHash>`], `sign` may be a closure that
115181
/// takes a message such as [`Bolt12Invoice`] or [`InvoiceRequest`]. This allows further message
116182
/// verification before signing its [`TaggedHash`].
117183
///
118184
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
119185
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
120-
pub fn sign_message<F, T>(
121-
f: F, message: &T, pubkey: PublicKey,
122-
) -> Result<Signature, SignError>
186+
pub fn sign_message<F, T>(f: F, message: &T, pubkey: PublicKey) -> Result<Signature, SignError>
123187
where
124188
F: SignFn<T>,
125189
T: AsRef<TaggedHash>,
@@ -136,6 +200,12 @@ where
136200

137201
/// Verifies the signature with a pubkey over the given message using a tagged hash as the message
138202
/// digest.
203+
///
204+
/// This is a low-level function exposed for specific use cases like command-line tools
205+
/// and testing. For production use, prefer higher-level methods like
206+
/// [`Bolt12Invoice::try_from`] which handle signature verification automatically.
207+
///
208+
/// [`Bolt12Invoice::try_from`]: crate::offers::invoice::Bolt12Invoice::try_from
139209
pub fn verify_signature(
140210
signature: &Signature, message: &TaggedHash, pubkey: PublicKey,
141211
) -> Result<(), secp256k1::Error> {
@@ -481,6 +551,119 @@ mod tests {
481551
assert_eq!(tlv_stream, invoice_request.bytes);
482552
}
483553

554+
#[test]
555+
fn validates_tlv_stream_bytes() {
556+
// Test with valid TLV stream
557+
const VALID_HEX: &'static str = "010203e8";
558+
let valid_bytes = <Vec<u8>>::from_hex(VALID_HEX).unwrap();
559+
let result = super::TaggedHash::from_tlv_stream_bytes("test", &valid_bytes);
560+
assert!(result.is_ok());
561+
562+
// Test with empty stream
563+
let empty_bytes = Vec::new();
564+
let result = super::TaggedHash::from_tlv_stream_bytes("test", &empty_bytes);
565+
assert_eq!(result, Err(super::TlvStreamError::EmptyStream));
566+
567+
// Test with invalid TLV stream (truncated)
568+
let invalid_bytes = vec![0x01, 0x02]; // Type and length but no value
569+
let result = super::TaggedHash::from_tlv_stream_bytes("test", &invalid_bytes);
570+
assert_eq!(result, Err(super::TlvStreamError::InvalidRecord));
571+
}
572+
573+
#[test]
574+
fn consistent_results_between_validating_and_non_validating_functions() {
575+
// Test vectors from BOLT 12
576+
let test_vectors = vec![
577+
"010203e8",
578+
"010203e802080000010000020003",
579+
"010203e802080000010000020003", // Using same as above for simplicity
580+
];
581+
582+
for hex_data in test_vectors {
583+
let bytes = <Vec<u8>>::from_hex(hex_data).unwrap();
584+
let tag = "test_tag";
585+
586+
// Create tagged hash using the validating function
587+
let validating_result = super::TaggedHash::from_tlv_stream_bytes(tag, &bytes);
588+
assert!(
589+
validating_result.is_ok(),
590+
"Validating function should succeed for valid TLV stream"
591+
);
592+
let validating_hash = validating_result.unwrap();
593+
594+
// Create tagged hash using the non-validating function
595+
let non_validating_hash = super::TaggedHash::from_valid_tlv_stream_bytes(tag, &bytes);
596+
597+
// Both should produce identical results
598+
assert_eq!(
599+
validating_hash.tag(),
600+
non_validating_hash.tag(),
601+
"Tags should be identical"
602+
);
603+
assert_eq!(
604+
validating_hash.merkle_root(),
605+
non_validating_hash.merkle_root(),
606+
"Merkle roots should be identical"
607+
);
608+
assert_eq!(
609+
validating_hash.as_digest(),
610+
non_validating_hash.as_digest(),
611+
"Digests should be identical"
612+
);
613+
assert_eq!(validating_hash, non_validating_hash, "Tagged hashes should be identical");
614+
}
615+
}
616+
617+
#[test]
618+
fn regression_test_with_invoice_request_data() {
619+
// Use real invoice request data to ensure no regression
620+
let expanded_key = ExpandedKey::new([42; 32]);
621+
let nonce = Nonce([0u8; 16]);
622+
let secp_ctx = Secp256k1::new();
623+
let payment_id = PaymentId([1; 32]);
624+
625+
let recipient_pubkey = {
626+
let secret_key = SecretKey::from_slice(&[41; 32]).unwrap();
627+
Keypair::from_secret_key(&secp_ctx, &secret_key).public_key()
628+
};
629+
630+
let invoice_request = OfferBuilder::new(recipient_pubkey)
631+
.amount_msats(100)
632+
.build_unchecked()
633+
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id)
634+
.unwrap()
635+
.build_and_sign()
636+
.unwrap();
637+
638+
// Extract bytes without signature for testing
639+
let mut bytes_without_signature = Vec::new();
640+
let tlv_stream_without_signatures = TlvStream::new(&invoice_request.bytes)
641+
.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type));
642+
for record in tlv_stream_without_signatures {
643+
record.write(&mut bytes_without_signature).unwrap();
644+
}
645+
646+
let tag = "invoice_request";
647+
648+
// Test both functions produce the same result
649+
let validating_result =
650+
super::TaggedHash::from_tlv_stream_bytes(tag, &bytes_without_signature);
651+
assert!(
652+
validating_result.is_ok(),
653+
"Should successfully validate real invoice request data"
654+
);
655+
let validating_hash = validating_result.unwrap();
656+
657+
let non_validating_hash =
658+
super::TaggedHash::from_valid_tlv_stream_bytes(tag, &bytes_without_signature);
659+
660+
// Verify they produce identical results
661+
assert_eq!(
662+
validating_hash, non_validating_hash,
663+
"Both functions should produce identical results for real data"
664+
);
665+
}
666+
484667
impl AsRef<[u8]> for InvoiceRequest {
485668
fn as_ref(&self) -> &[u8] {
486669
&self.bytes

0 commit comments

Comments
 (0)