11// Copyright 2023-, Semiotic AI, Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4- use std:: collections:: { hash_set , HashSet } ;
4+ use std:: collections:: HashSet ;
55
66use anyhow:: { bail, Ok , Result } ;
77use rayon:: prelude:: * ;
8- use tap_core:: signed_message :: { Eip712SignedMessage , SignatureBytes , SignatureBytesExt } ;
8+ use tap_core:: { receipt :: WithUniqueId , signed_message :: Eip712SignedMessage } ;
99use tap_graph:: { Receipt , ReceiptAggregateVoucher } ;
1010use thegraph_core:: alloy:: {
1111 dyn_abi:: Eip712Domain , primitives:: Address , signers:: local:: PrivateKeySigner ,
@@ -19,7 +19,7 @@ pub fn check_and_aggregate_receipts(
1919 wallet : & PrivateKeySigner ,
2020 accepted_addresses : & HashSet < Address > ,
2121) -> Result < Eip712SignedMessage < ReceiptAggregateVoucher > > {
22- check_signatures_unique ( receipts) ?;
22+ check_signatures_unique ( domain_separator , receipts) ?;
2323
2424 // Check that the receipts are signed by an accepted signer address
2525 receipts. par_iter ( ) . try_for_each ( |receipt| {
@@ -93,14 +93,17 @@ fn check_allocation_id(
9393 Ok ( ( ) )
9494}
9595
96- fn check_signatures_unique ( receipts : & [ Eip712SignedMessage < Receipt > ] ) -> Result < ( ) > {
97- let mut receipt_signatures: hash_set:: HashSet < SignatureBytes > = hash_set:: HashSet :: new ( ) ;
96+ fn check_signatures_unique (
97+ domain_separator : & Eip712Domain ,
98+ receipts : & [ Eip712SignedMessage < Receipt > ] ,
99+ ) -> Result < ( ) > {
100+ let mut receipt_signatures = HashSet :: new ( ) ;
98101 for receipt in receipts. iter ( ) {
99- let signature = receipt. signature . get_signature_bytes ( ) ;
102+ let signature = receipt. unique_id ( domain_separator ) ? ;
100103 if !receipt_signatures. insert ( signature) {
101104 return Err ( tap_core:: Error :: DuplicateReceiptSignature ( format ! (
102105 "{:?}" ,
103- receipt. signature
106+ receipt. unique_id ( domain_separator ) ?
104107 ) )
105108 . into ( ) ) ;
106109 }
@@ -136,7 +139,9 @@ mod tests {
136139 use tap_core:: { signed_message:: Eip712SignedMessage , tap_eip712_domain} ;
137140 use tap_graph:: { Receipt , ReceiptAggregateVoucher } ;
138141 use thegraph_core:: alloy:: {
139- dyn_abi:: Eip712Domain , primitives:: Address , signers:: local:: PrivateKeySigner ,
142+ dyn_abi:: Eip712Domain ,
143+ primitives:: { Address , U256 } ,
144+ signers:: { local:: PrivateKeySigner , Signature } ,
140145 } ;
141146
142147 use super :: * ;
@@ -163,6 +168,66 @@ mod tests {
163168 tap_eip712_domain ( 1 , Address :: from ( [ 0x11u8 ; 20 ] ) )
164169 }
165170
171+ #[ rstest]
172+ #[ test]
173+ fn test_signature_malleability_vulnerability (
174+ keys : ( PrivateKeySigner , Address ) ,
175+ allocation_ids : Vec < Address > ,
176+ domain_separator : Eip712Domain ,
177+ ) {
178+ // Create a test receipt
179+ let receipt = Eip712SignedMessage :: new (
180+ & domain_separator,
181+ Receipt :: new ( allocation_ids[ 0 ] , 42 ) . unwrap ( ) ,
182+ & keys. 0 ,
183+ )
184+ . unwrap ( ) ;
185+
186+ // Get the original signature components
187+ let r = receipt. signature . r ( ) ;
188+ let s = receipt. signature . s ( ) ;
189+ let v = receipt. signature . v ( ) ;
190+
191+ // Create a malleated signature by changing the s value and flipping v
192+ // Get the Secp256k1 curve order
193+ let n = U256 :: from_str_radix (
194+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" ,
195+ 16 ,
196+ )
197+ . unwrap ( ) ;
198+ let s_malleated = n - s;
199+ let v_malleated = !v; // Flip the parity bit
200+
201+ // Create a new signature with the malleated components
202+ let signature_malleated = Signature :: new ( r, s_malleated, v_malleated) ;
203+
204+ // Create a new signed message with the malleated signature but same message
205+ let receipt_malleated = Eip712SignedMessage {
206+ message : receipt. message . clone ( ) ,
207+ signature : signature_malleated,
208+ } ;
209+
210+ // Verify that both signatures recover to the same signer
211+ let original_signer = receipt. recover_signer ( & domain_separator) . unwrap ( ) ;
212+ let malleated_signer = receipt_malleated. recover_signer ( & domain_separator) . unwrap ( ) ;
213+
214+ assert_eq ! (
215+ original_signer, malleated_signer,
216+ "Both signatures should recover to the same signer"
217+ ) ;
218+
219+ // Try to check if signatures are unique using the current implementation
220+ let receipts = vec ! [ receipt, receipt_malleated] ;
221+
222+ // This should return an error because the signatures are different
223+ // but the messages are the same, which if allowed would present a security vulnerability
224+ let result = check_signatures_unique ( & domain_separator, & receipts) ;
225+
226+ // The result should be an error because the malleated signature is not treated as unique
227+ // and is detected as a duplicate
228+ assert ! ( result. is_err( ) ) ;
229+ }
230+
166231 #[ rstest]
167232 #[ test]
168233 fn check_signatures_unique_fail (
@@ -181,7 +246,7 @@ mod tests {
181246 receipts. push ( receipt. clone ( ) ) ;
182247 receipts. push ( receipt) ;
183248
184- let res = check_signatures_unique ( & receipts) ;
249+ let res = check_signatures_unique ( & domain_separator , & receipts) ;
185250 assert ! ( res. is_err( ) ) ;
186251 }
187252
@@ -208,7 +273,7 @@ mod tests {
208273 . unwrap( ) ,
209274 ] ;
210275
211- let res = check_signatures_unique ( & receipts) ;
276+ let res = check_signatures_unique ( & domain_separator , & receipts) ;
212277 assert ! ( res. is_ok( ) ) ;
213278 }
214279
0 commit comments