@@ -41,11 +41,64 @@ impl TaggedHash {
41
41
/// Creates a tagged hash with the given parameters.
42
42
///
43
43
/// 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 {
45
45
let tlv_stream = TlvStream :: new ( bytes) ;
46
46
Self :: from_tlv_stream ( tag, tlv_stream)
47
47
}
48
48
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
+
49
102
/// Creates a tagged hash with the given parameters.
50
103
///
51
104
/// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record.
@@ -93,8 +146,17 @@ pub enum SignError {
93
146
Verification ( secp256k1:: Error ) ,
94
147
}
95
148
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
+
96
158
/// A function for signing a [`TaggedHash`].
97
- pub ( super ) trait SignFn < T : AsRef < TaggedHash > > {
159
+ pub trait SignFn < T : AsRef < TaggedHash > > {
98
160
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream.
99
161
fn sign ( & self , message : & T ) -> Result < Signature , ( ) > ;
100
162
}
@@ -111,15 +173,17 @@ where
111
173
/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream, checking if it
112
174
/// can be verified with the supplied `pubkey`.
113
175
///
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
+ ///
114
180
/// Since `message` is any type that implements [`AsRef<TaggedHash>`], `sign` may be a closure that
115
181
/// takes a message such as [`Bolt12Invoice`] or [`InvoiceRequest`]. This allows further message
116
182
/// verification before signing its [`TaggedHash`].
117
183
///
118
184
/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
119
185
/// [`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 >
123
187
where
124
188
F : SignFn < T > ,
125
189
T : AsRef < TaggedHash > ,
@@ -136,6 +200,12 @@ where
136
200
137
201
/// Verifies the signature with a pubkey over the given message using a tagged hash as the message
138
202
/// 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
139
209
pub fn verify_signature (
140
210
signature : & Signature , message : & TaggedHash , pubkey : PublicKey ,
141
211
) -> Result < ( ) , secp256k1:: Error > {
@@ -481,6 +551,119 @@ mod tests {
481
551
assert_eq ! ( tlv_stream, invoice_request. bytes) ;
482
552
}
483
553
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
+
484
667
impl AsRef < [ u8 ] > for InvoiceRequest {
485
668
fn as_ref ( & self ) -> & [ u8 ] {
486
669
& self . bytes
0 commit comments