Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions packages/siwe/lib/client.multisig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Wallet } from 'ethers';
import { SiweMessage } from './client';
import * as ethersCompat from './ethersCompat';
import * as utils from './utils';

function exampleMessage(address: string): SiweMessage {
return new SiweMessage({
address,
domain: 'login.xyz',
statement: 'Sign-In With Ethereum Example Statement',
uri: 'https://login.xyz',
version: '1',
nonce: 'bTyXgcQxn2htgkjJn',
issuedAt: '2022-01-27T17:09:38.578Z',
chainId: 1,
});
}

describe('Multisig / EIP-1271 signature routing', () => {
let errorSpy: jest.SpyInstance;
let verifySpy: jest.SpyInstance;
let eip1271Spy: jest.SpyInstance;

beforeEach(() => {
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
verifySpy = jest.spyOn(ethersCompat, 'verifyMessage');
eip1271Spy = jest
.spyOn(utils, 'checkContractWalletSignature')
.mockResolvedValue(true);
});

afterEach(() => {
jest.restoreAllMocks();
});

test('skips ecrecover for contract-wallet signatures longer than 65 bytes', async () => {
const msg = exampleMessage('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
// 80-byte signature, typical of concatenated Safe / multisig owner sigs
const signature = `0x${'ab'.repeat(80)}`;

const result = await msg.verify(
{ signature },
{ provider: {} as any, suppressExceptions: true }
);

expect(verifySpy).not.toHaveBeenCalled();
expect(eip1271Spy).toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
expect(result.success).toBe(true);
});

test('skips ecrecover for 66-byte contract-wallet signatures', async () => {
const msg = exampleMessage('0x0e565A6dFc43DE21455a67bbF196f7F7b15447A7');
// Loopring-style: 65-byte ECDSA plus a trailing type byte
const signature = `0x${'cd'.repeat(66)}`;

const result = await msg.verify(
{ signature },
{ provider: {} as any, suppressExceptions: true }
);

expect(verifySpy).not.toHaveBeenCalled();
expect(eip1271Spy).toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
expect(result.success).toBe(true);
});

test('still recovers 65-byte EOA signatures with ecrecover', async () => {
const wallet = Wallet.createRandom();
const msg = exampleMessage(wallet.address);
const signature = await wallet.signMessage(msg.toMessage());
const hex = signature.startsWith('0x') ? signature.slice(2) : signature;
expect(hex.length).toBe(130);

const result = await msg.verify({ signature });

expect(verifySpy).toHaveBeenCalled();
expect(result.success).toBeTruthy();
});

test('still attempts ecrecover for 64-byte compact signatures', async () => {
const msg = exampleMessage('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
const signature = `0x${'ab'.repeat(64)}`;

await msg.verify(
{ signature },
{ provider: {} as any, suppressExceptions: true }
);

expect(verifySpy).toHaveBeenCalled();
});
});
33 changes: 28 additions & 5 deletions packages/siwe/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ import {
checkInvalidKeys,
} from './utils';

/**
* ECDSA signatures recovered via ecrecover are 65 bytes (r||s||v) or
* 64 bytes (EIP-2098 compact). Contract-wallet / multisig signatures
* (EIP-1271) are other lengths; ethers `verifyMessage` throws
* `invalid raw signature length` for those.
*/
function isEcdsaSignature(signature?: string): boolean {
if (!signature) {
return false;
}
const hex =
signature.startsWith('0x') || signature.startsWith('0X')
? signature.slice(2)
: signature;
return hex.length === 130 || hex.length === 128;
}

export class SiweMessage {
/**RFC 3986 URI scheme for the authority that is requesting the signing. */
scheme?: string;
Expand Down Expand Up @@ -312,12 +329,18 @@ export class SiweMessage {
});
}

/** Recover address from signature */
/** Recover address from signature.
* Only 65-byte (r||s||v) and 64-byte (EIP-2098) signatures can be
* recovered with ecrecover. Longer contract-wallet / multisig
* signatures make ethers throw `invalid raw signature length`;
* skip straight to EIP-1271 for those. */
let addr;
try {
addr = verifyMessage(EIP4361Message, signature);
} catch (e) {
console.error(e);
if (isEcdsaSignature(signature)) {
try {
addr = verifyMessage(EIP4361Message, signature);
} catch (e) {
console.error(e);
}
}
/** Match signature with message's address */
if (addr === this.address) {
Expand Down