@@ -157,6 +157,30 @@ describe('verifySignature', () => {
157157 expect ( result ) . toBe ( INVALID ) ;
158158 } ) ;
159159
160+ it ( 'sorts parameters alphabetically regardless of input order' , async ( ) => {
161+ const validSignature = Buffer . from ( new Array ( 64 ) . fill ( 0 ) ) . toString (
162+ 'base64' ,
163+ ) ;
164+ // Parameters in REVERSE alphabetical order
165+ const url = new URL (
166+ `https://link.metamask.io/perps?utm_source=carousel&utm_medium=in-product&utm_campaign=cmp123&sig_params=utm_campaign,utm_medium,utm_source&sig=${ validSignature } ` ,
167+ ) ;
168+
169+ mockSubtle . verify . mockResolvedValue ( true ) ;
170+ await verifyDeeplinkSignature ( url ) ;
171+
172+ const verifyCall = mockSubtle . verify . mock . calls [ 0 ] ;
173+ const canonicalUrl = new TextDecoder ( ) . decode (
174+ verifyCall [ 3 ] as Uint8Array ,
175+ ) ;
176+
177+ // sig_params (s) should come BEFORE utm_* (u) alphabetically
178+ // This is the exact bug that broke the marketing links!
179+ expect ( canonicalUrl ) . toBe (
180+ 'https://link.metamask.io/perps?sig_params=utm_campaign%2Cutm_medium%2Cutm_source&utm_campaign=cmp123&utm_medium=in-product&utm_source=carousel' ,
181+ ) ;
182+ } ) ;
183+
160184 it ( 'canonicalizes URL by removing sig parameter and sorting others' , async ( ) => {
161185 const validSignature = Buffer . from ( new Array ( 64 ) . fill ( 0 ) ) . toString (
162186 'base64' ,
@@ -264,6 +288,25 @@ describe('verifySignature', () => {
264288 ) ;
265289 } ) ;
266290
291+ // it('includes only sig_params when sig_params is empty string', async () => {
292+ // const validSignature = Buffer.from(new Array(64).fill(0)).toString('base64');
293+ // // sig_params is EMPTY - means "sign only the base path"
294+ // // UTMs are added AFTER signing and should be ignored
295+ // const url = new URL(
296+ // `https://link.metamask.io/perps?sig_params=&sig=${validSignature}&utm_source=carousel&utm_medium=in-product`,
297+ // );
298+
299+ // mockSubtle.verify.mockResolvedValue(true);
300+ // await verifyDeeplinkSignature(url);
301+
302+ // const verifyCall = mockSubtle.verify.mock.calls[0];
303+ // const canonicalUrl = new TextDecoder().decode(verifyCall[3] as Uint8Array);
304+
305+ // // ONLY sig_params should be in canonical URL
306+ // // UTMs should be EXCLUDED (they were added after signing)
307+ // expect(canonicalUrl).toBe('https://link.metamask.io/perps?sig_params=');
308+ // });
309+
267310 describe ( 'with sig_params' , ( ) => {
268311 it ( 'includes only parameters listed in sig_params for verification' , async ( ) => {
269312 const validSignature = Buffer . from ( new Array ( 64 ) . fill ( 0 ) ) . toString (
0 commit comments