Fix Misclassification of ECDSA Signatures in verifyMultisig() (#1973)

* Fix multisignature routing to properly detect signature type

* Improve testing suite

* update dep in tests

* lint

* revert changes to tests

* Add reprocible test

---------

Co-authored-by: tarikgul <tariksnow37@gmail.com>
This commit is contained in:
Valentin Fernandez
2025-02-16 12:36:35 -05:00
committed by GitHub
co-authored by tarikgul
parent 748f670351
commit 2cbea7bf8d
2 changed files with 70 additions and 22 deletions
@@ -7,6 +7,7 @@ import { hexToU8a, stringToU8a, u8aConcat, u8aToHex, u8aWrapBytes } from '@polka
import { waitReady } from '@polkadot/wasm-crypto';
import { decodeAddress } from '../address/index.js';
import { secp256k1Sign } from '../secp256k1/sign.js';
import { signatureVerify } from './index.js';
const ADDR_ED = 'DxN4uvzwPzJLtn17yew6jEffPhXQfdKHTp2brufb98vGbPN';
@@ -37,7 +38,7 @@ describe('signatureVerify', (): void => {
});
describe('verifyDetect', (): void => {
it('verifies an ed25519 signature', (): void => {
it('verifies ed25519 signature', (): void => {
expect(signatureVerify(MESSAGE, SIG_ED, ADDR_ED)).toEqual({
crypto: 'ed25519',
isValid: true,
@@ -46,7 +47,7 @@ describe('signatureVerify', (): void => {
});
});
it('verifies an ecdsa signature', (): void => {
it('verifies ecdsa signatures', (): void => {
expect(signatureVerify(MESSAGE, SIG_EC, ADDR_EC)).toEqual({
crypto: 'ecdsa',
isValid: true,
@@ -173,11 +174,57 @@ describe('signatureVerify', (): void => {
it('fails on an invalid signature', (): void => {
expect(signatureVerify(MESSAGE, MUL_SR, ADDR_ED)).toEqual({
crypto: 'sr25519',
crypto: 'none',
isValid: false,
isWrapped: false,
publicKey: new Uint8Array([61, 12, 55, 211, 0, 211, 97, 199, 4, 37, 17, 213, 81, 175, 166, 23, 251, 199, 144, 210, 19, 83, 186, 1, 196, 231, 14, 156, 171, 46, 141, 146])
});
});
/**
* ref: https://github.com/polkadot-js/common/issues/1898
*
* The following test ensures that we cover a reproduction that showed
* an inherent issue with verifying ecdsa signatures which is fixed in
* https://github.com/polkadot-js/common/pull/1973.
*
* It uses a random secretKey, and publicKey pair along with `secp256k1Sign`
* as the signer which is used for `ecdsa`.
*/
it('Ensure ecdsa can sign and verify 1000 messages', (): void => {
const verifyThousandMessages = () => {
const secretKey = new Uint8Array([
103, 97, 114, 98, 97, 103, 101, 32, 114, 105, 100,
103, 101, 32, 107, 105, 99, 107, 32, 114, 111, 115,
101, 32, 101, 110, 100, 32, 115, 113, 117, 101
]);
const publicKey = new Uint8Array([
2, 179, 102, 92, 246, 50, 172, 88,
81, 116, 8, 211, 192, 131, 154, 184,
122, 83, 180, 104, 4, 227, 214, 195,
140, 11, 82, 229, 49, 211, 185, 176,
63
]);
for (let i = 0; i < 1000; i++) {
const message = `message ${i}`;
const encodedMessage = stringToU8a(message);
const signature = secp256k1Sign(encodedMessage, { secretKey });
const { isValid: valid } = signatureVerify(
message,
signature,
publicKey
);
if (!valid) {
return false;
}
}
return true;
};
expect(verifyThousandMessages()).toEqual(true);
});
});
});
+20 -19
View File
@@ -31,13 +31,10 @@ const VERIFIERS_ECDSA: Verifier[] = [
const VERIFIERS: Verifier[] = [
['ed25519', ed25519Verify],
['sr25519', sr25519Verify],
...VERIFIERS_ECDSA
['sr25519', sr25519Verify]
];
const CRYPTO_TYPES: ('ed25519' | 'sr25519' | 'ecdsa')[] = ['ed25519', 'sr25519', 'ecdsa'];
function verifyDetect (result: VerifyResult, { message, publicKey, signature }: VerifyInput, verifiers = VERIFIERS): VerifyResult {
function verifyDetect (result: VerifyResult, { message, publicKey, signature }: VerifyInput, verifiers = [...VERIFIERS, ...VERIFIERS_ECDSA]): VerifyResult {
result.isValid = verifiers.some(([crypto, verify]): boolean => {
try {
if (verify(message, signature, publicKey)) {
@@ -56,25 +53,29 @@ function verifyDetect (result: VerifyResult, { message, publicKey, signature }:
}
function verifyMultisig (result: VerifyResult, { message, publicKey, signature }: VerifyInput): VerifyResult {
if (![0, 1, 2].includes(signature[0])) {
if (![0, 1, 2].includes(signature[0]) || ![65, 66].includes(signature.length)) {
throw new Error(`Unknown crypto type, expected signature prefix [0..2], found ${signature[0]}`);
}
const type = CRYPTO_TYPES[signature[0]] || 'none';
// If the signature is 66 bytes it must be an ecdsa signature
// containing: prefix [1 byte] + signature [65] bytes.
// Remove the and then verify
if (signature.length === 66) {
result = verifyDetect(result, { message, publicKey, signature: signature.subarray(1) }, VERIFIERS_ECDSA);
} else {
// The signature contains 65 bytes which is either
// - A ed25519 or sr25519 signature [1 byte prefix + 64 bytes]
// - An ecdsa signature [65 bytes]
result = verifyDetect(result, { message, publicKey, signature: signature.subarray(1) }, VERIFIERS);
result.crypto = type;
if (!result.isValid) {
result = verifyDetect(result, { message, publicKey, signature }, VERIFIERS_ECDSA);
}
try {
result.isValid = {
ecdsa: () => verifyDetect(result, { message, publicKey, signature: signature.subarray(1) }, VERIFIERS_ECDSA).isValid,
ed25519: () => ed25519Verify(message, signature.subarray(1), publicKey),
none: () => {
throw Error('no verify for `none` crypto type');
},
sr25519: () => sr25519Verify(message, signature.subarray(1), publicKey)
}[type]();
} catch {
// ignore, result.isValid still set to false
// If both failed, explicitly set crypto to 'none'
if (!result.isValid) {
result.crypto = 'none';
}
}
return result;