From 7047bf5aa513b029d878fb4f12625dffa4f37b30 Mon Sep 17 00:00:00 2001 From: Jaco Date: Fri, 17 Sep 2021 15:48:33 +0300 Subject: [PATCH] Detect and verify wrapped messages (#1110) * Detect and verify wrapped messages * Expose isWrapped detection * Complexity cleanups * Update wrap.spec.ts * Update wrap.ts * Extract fn getter * Test for wrapping to detect path --- CHANGELOG.md | 2 + .../util-crypto/src/signature/verify.spec.ts | 35 ++++- packages/util-crypto/src/signature/verify.ts | 32 +++- packages/util-crypto/src/types.ts | 1 + packages/util/src/u8a/index.ts | 1 + packages/util/src/u8a/wrap.spec.ts | 137 ++++++++++++++++++ packages/util/src/u8a/wrap.ts | 49 +++++++ 7 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 packages/util/src/u8a/wrap.spec.ts create mode 100644 packages/util/src/u8a/wrap.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc1d06bd..83b524c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes: +- Add `u8aWrapBytes` & `u8aUnwrapBytes` (originally from `@polkadot/extension-dapp`) +- Verify both wrapped and unwrapped signatures transparently - Adjust `isHex` return to `0x${string}` under TypeScript - Adjust shared repo Jest config diff --git a/packages/util-crypto/src/signature/verify.spec.ts b/packages/util-crypto/src/signature/verify.spec.ts index fc52bc535..dbc993348 100644 --- a/packages/util-crypto/src/signature/verify.spec.ts +++ b/packages/util-crypto/src/signature/verify.spec.ts @@ -1,7 +1,7 @@ // Copyright 2017-2021 @polkadot/util-crypto authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { hexToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util'; +import { hexToU8a, stringToU8a, u8aConcat, u8aToHex, u8aWrapBytes } from '@polkadot/util'; import { waitReady } from '@polkadot/wasm-crypto'; import { decodeAddress } from '../address'; @@ -9,11 +9,13 @@ import { signatureVerify } from '.'; const ADDR_ED = 'DxN4uvzwPzJLtn17yew6jEffPhXQfdKHTp2brufb98vGbPN'; const ADDR_SR = 'EK1bFgKm2FsghcttHT7TB7rNyXApFgs9fCbijMGQNyFGBQm'; +const ADDR_SR_WRAP = 'J9nD3s7zssCX7bion1xctAF6xcVexcpy2uwy4jTm9JL8yuK'; const ADDR_EC = 'XyFVXiGaHxoBhXZkSh6NS2rjFyVaVNUo5UiZDqZbuSfUdji'; const ADDR_ET = '0x54Dab85EE2c7b9F7421100d7134eFb5DfA4239bF'; const MESSAGE = 'hello world'; const SIG_ED = '0x299d3bf4c8bb51af732f8067b3a3015c0862a5ff34721749d8ed6577ea2708365d1c5f76bd519009971e41156f12c70abc2533837ceb3bad9a05a99ab923de06'; const SIG_SR = '0xca01419b5a17219f7b78335658cab3b126db523a5df7be4bfc2bef76c2eb3b1dcf4ca86eb877d0a6cf6df12db5995c51d13b00e005d053b892bd09c594434288'; +const SIG_SR_WRAP = '0x84b6afb1c8e54bbcb3f4872baf172580e21310e9387a53742627d6652d121447fa406b82805ed3184fb7bd519175cc9f99f283f97954d95cf966ee164df85489'; const SIG_EC = '0x994638ee586d2c5dbd9bacacbc35d9b7e9018de8f7892f00c900db63bc57b1283e2ee7bc51a9b1c1dae121ac4f4b9e2a41cd1d6bf4bb3e24d7fed6faf6d85e0501'; const SIG_ET = '0x4e35aad35793b71f08566615661c9b741d7c605bc8935ac08608dff685324d71b5704fbd14c9297d2f584ea0735f015dcf0def66b802b3f555e1db916eda4b7700'; const MUL_ED = u8aToHex(u8aConcat(new Uint8Array([0]), hexToU8a(SIG_ED))); @@ -37,6 +39,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_ED, ADDR_ED)).toEqual({ crypto: 'ed25519', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_ED) }); }); @@ -45,6 +48,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_EC, ADDR_EC)).toEqual({ crypto: 'ecdsa', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_EC) }); }); @@ -53,6 +57,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_ET, ADDR_ET)).toEqual({ crypto: 'ethereum', isValid: true, + isWrapped: false, publicKey: hexToU8a(ADDR_ET) }); }); @@ -67,6 +72,7 @@ describe('signatureVerify', (): void => { )).toEqual({ crypto: 'ethereum', isValid: true, + isWrapped: true, publicKey: hexToU8a('0x002309df96687e44280bb72c3818358faeeb699c') }); }); @@ -75,6 +81,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_EC, ADDR_ET)).toEqual({ crypto: 'none', isValid: false, + isWrapped: false, publicKey: hexToU8a(ADDR_ET) }); }); @@ -83,14 +90,34 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_SR, ADDR_SR)).toEqual({ crypto: 'sr25519', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_SR) }); }); + it('verifies an sr25519 signature (with msg wrapper, without wrapped sig)', (): void => { + expect(signatureVerify(u8aWrapBytes(MESSAGE), SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({ + crypto: 'sr25519', + isValid: true, + isWrapped: true, + publicKey: decodeAddress(ADDR_SR_WRAP) + }); + }); + + it('verifies an sr25519 signature (without msg wrapper, with wrapped sig)', (): void => { + expect(signatureVerify(MESSAGE, SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({ + crypto: 'sr25519', + isValid: true, + isWrapped: false, + publicKey: decodeAddress(ADDR_SR_WRAP) + }); + }); + it('allows various inputs', (): void => { expect(signatureVerify(stringToU8a(MESSAGE), hexToU8a(SIG_ED), decodeAddress(ADDR_ED))).toEqual({ crypto: 'ed25519', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_ED) }); }); @@ -99,6 +126,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, SIG_SR, ADDR_ED)).toEqual({ crypto: 'none', isValid: false, + isWrapped: false, publicKey: decodeAddress(ADDR_ED) }); }); @@ -109,6 +137,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, MUL_ED, ADDR_ED)).toEqual({ crypto: 'ed25519', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_ED) }); }); @@ -117,6 +146,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, MUL_EC, ADDR_EC)).toEqual({ crypto: 'ecdsa', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_EC) }); }); @@ -125,6 +155,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, MUL_ET, ADDR_ET)).toEqual({ crypto: 'ethereum', isValid: true, + isWrapped: false, publicKey: hexToU8a(ADDR_ET) }); }); @@ -133,6 +164,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, MUL_SR, ADDR_SR)).toEqual({ crypto: 'sr25519', isValid: true, + isWrapped: false, publicKey: decodeAddress(ADDR_SR) }); }); @@ -141,6 +173,7 @@ describe('signatureVerify', (): void => { expect(signatureVerify(MESSAGE, MUL_SR, ADDR_ED)).toEqual({ crypto: 'sr25519', 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]) }); }); diff --git a/packages/util-crypto/src/signature/verify.ts b/packages/util-crypto/src/signature/verify.ts index 5c541fa4e..e6127a272 100644 --- a/packages/util-crypto/src/signature/verify.ts +++ b/packages/util-crypto/src/signature/verify.ts @@ -4,7 +4,7 @@ import type { HexString } from '@polkadot/util/types'; import type { KeypairType, VerifyResult } from '../types'; -import { assert, u8aToU8a } from '@polkadot/util'; +import { assert, u8aIsWrapped, u8aToU8a, u8aUnwrapBytes, u8aWrapBytes } from '@polkadot/util'; import { decodeAddress } from '../address/decode'; import { naclVerify } from '../nacl/verify'; @@ -12,13 +12,15 @@ import { schnorrkelVerify } from '../schnorrkel/verify'; import { secp256k1Verify } from '../secp256k1/verify'; interface VerifyInput { - message: Uint8Array | string; + message: Uint8Array; publicKey: Uint8Array; signature: Uint8Array; } type Verifier = [KeypairType, (message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) => boolean]; +type VerifyFn = (result: VerifyResult, input: VerifyInput) => VerifyResult; + const secp256k1VerifyHasher = (hashType: 'blake2' | 'keccak') => (message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) => secp256k1Verify(message, signature, publicKey, hashType); @@ -75,16 +77,32 @@ function verifyMultisig (result: VerifyResult, { message, publicKey, signature } return result; } +function getVerifyFn (signature: Uint8Array): VerifyFn { + return [0, 1, 2].includes(signature[0]) && [65, 66].includes(signature.length) + ? verifyMultisig + : verifyDetect; +} + export function signatureVerify (message: HexString | Uint8Array | string, signature: HexString | Uint8Array | string, addressOrPublicKey: HexString | Uint8Array | string): VerifyResult { const signatureU8a = u8aToU8a(signature); assert([64, 65, 66].includes(signatureU8a.length), () => `Invalid signature length, expected [64..66] bytes, found ${signatureU8a.length}`); const publicKey = decodeAddress(addressOrPublicKey); - const input = { message, publicKey, signature: signatureU8a }; - const result: VerifyResult = { crypto: 'none', isValid: false, publicKey }; + const input = { message: u8aToU8a(message), publicKey, signature: signatureU8a }; + const result: VerifyResult = { crypto: 'none', isValid: false, isWrapped: u8aIsWrapped(input.message, true), publicKey }; + const isWrappedBytes = u8aIsWrapped(input.message, false); + const verifyFn = getVerifyFn(signatureU8a); - return [0, 1, 2].includes(signatureU8a[0]) && [65, 66].includes(signatureU8a.length) - ? verifyMultisig(result, input) - : verifyDetect(result, input); + verifyFn(result, input); + + if (result.crypto !== 'none' || (result.isWrapped && !isWrappedBytes)) { + return result; + } + + input.message = isWrappedBytes + ? u8aUnwrapBytes(input.message) + : u8aWrapBytes(input.message); + + return verifyFn(result, input); } diff --git a/packages/util-crypto/src/types.ts b/packages/util-crypto/src/types.ts index ba7c3bb16..a6b9d84e6 100644 --- a/packages/util-crypto/src/types.ts +++ b/packages/util-crypto/src/types.ts @@ -19,5 +19,6 @@ export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'; export interface VerifyResult { crypto: 'none' | KeypairType; isValid: boolean; + isWrapped: boolean; publicKey: Uint8Array; } diff --git a/packages/util/src/u8a/index.ts b/packages/util/src/u8a/index.ts index 35a672fa7..cd1109e65 100644 --- a/packages/util/src/u8a/index.ts +++ b/packages/util/src/u8a/index.ts @@ -16,3 +16,4 @@ export { u8aToBuffer } from './toBuffer'; export { u8aToHex } from './toHex'; export { u8aToString } from './toString'; export { u8aToU8a } from './toU8a'; +export { U8A_WRAP_ETHEREUM, U8A_WRAP_POSTFIX, U8A_WRAP_PREFIX, u8aIsWrapped, u8aWrapBytes, u8aUnwrapBytes } from './wrap'; diff --git a/packages/util/src/u8a/wrap.spec.ts b/packages/util/src/u8a/wrap.spec.ts new file mode 100644 index 000000000..31de98810 --- /dev/null +++ b/packages/util/src/u8a/wrap.spec.ts @@ -0,0 +1,137 @@ +// Copyright 2017-2021 @polkadot/util authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Originally from https://github.com/polkadot-js/extension/pull/743 + +import { u8aConcat, u8aEq, u8aToString } from '@polkadot/util'; + +import { U8A_WRAP_ETHEREUM, U8A_WRAP_POSTFIX, U8A_WRAP_PREFIX, u8aUnwrapBytes, u8aWrapBytes } from './wrap'; + +const TEST_DATA = 'this is just some random message that we expect to be wrapped along the way'; +const TEST_ETH = u8aConcat(U8A_WRAP_ETHEREUM, TEST_DATA); +const TEST_WRAP_EMPTY = `${u8aToString(U8A_WRAP_PREFIX)}${u8aToString(U8A_WRAP_POSTFIX)}`; +const TEST_WRAP_FULL = `${u8aToString(U8A_WRAP_PREFIX)}${TEST_DATA}${u8aToString(U8A_WRAP_POSTFIX)}`; +const TEST_WARP_HALF_PRE = `${u8aToString(U8A_WRAP_PREFIX)}${TEST_DATA}`; +const TEST_WRAP_HALF_POST = `${TEST_DATA}${u8aToString(U8A_WRAP_POSTFIX)}`; + +describe('u8aWrapBytes', (): void => { + it('wraps empty bytes', (): void => { + expect( + u8aEq( + u8aWrapBytes(new Uint8Array()), + u8aConcat(U8A_WRAP_PREFIX, U8A_WRAP_POSTFIX) + ) + ).toBe(true); + }); + + it('wraps when no wrapping is detected', (): void => { + expect( + u8aToString( + u8aWrapBytes(TEST_DATA) + ) + ).toEqual(TEST_WRAP_FULL); + }); + + it('wraps when only start wrap is detected', (): void => { + expect( + u8aToString( + u8aWrapBytes(TEST_WARP_HALF_PRE) + ) + ).toEqual(`${u8aToString(U8A_WRAP_PREFIX)}${TEST_WARP_HALF_PRE}${u8aToString(U8A_WRAP_POSTFIX)}`); + }); + + it('wraps when only end wrap is detected', (): void => { + expect( + u8aToString( + u8aWrapBytes(TEST_WRAP_HALF_POST) + ) + ).toEqual(`${u8aToString(U8A_WRAP_PREFIX)}${TEST_WRAP_HALF_POST}${u8aToString(U8A_WRAP_POSTFIX)}`); + }); + + it('does not re-wrap when a wrap is already present', (): void => { + expect( + u8aToString( + u8aWrapBytes(TEST_WRAP_FULL) + ) + ).toEqual(TEST_WRAP_FULL); + }); + + it('does not re-wrap when a wrap (empty data) is already present', (): void => { + expect( + u8aToString( + u8aWrapBytes(TEST_WRAP_EMPTY) + ) + ).toEqual(TEST_WRAP_EMPTY); + }); +}); + +describe('unwrapBytes', (): void => { + it('unwraps empty bytes', (): void => { + expect( + u8aEq( + u8aUnwrapBytes(new Uint8Array()), + new Uint8Array() + ) + ).toBe(true); + }); + + it('unwraps when no wrapping is detected', (): void => { + expect( + u8aToString( + u8aUnwrapBytes(TEST_DATA) + ) + ).toEqual(TEST_DATA); + }); + + it('unwraps when no wrapping is detected (only start)', (): void => { + expect( + u8aToString( + u8aUnwrapBytes(TEST_WARP_HALF_PRE) + ) + ).toEqual(TEST_WARP_HALF_PRE); + }); + + it('unwraps when no wrapping is detected (only end)', (): void => { + expect( + u8aToString( + u8aUnwrapBytes(TEST_WRAP_HALF_POST) + ) + ).toEqual(TEST_WRAP_HALF_POST); + }); + + it('unwraps when a wrap is present', (): void => { + expect( + u8aToString( + u8aUnwrapBytes(TEST_WRAP_FULL) + ) + ).toEqual(TEST_DATA); + }); + + it('unwraps when a an empty wrap is present', (): void => { + expect( + u8aToString( + u8aUnwrapBytes(TEST_WRAP_EMPTY) + ) + ).toEqual(''); + }); + + describe('Ethereum-style', (): void => { + it('does not wrap an Ethereum wrap', (): void => { + expect( + u8aEq( + u8aWrapBytes(TEST_ETH), + TEST_ETH + ) + ).toBe(true); + }); + + it('does not unwrap an Ethereum wrap', (): void => { + expect( + u8aEq( + u8aUnwrapBytes(TEST_ETH), + TEST_ETH + ) + ).toBe(true); + }); + }); +}); diff --git a/packages/util/src/u8a/wrap.ts b/packages/util/src/u8a/wrap.ts new file mode 100644 index 000000000..c37b1b8c1 --- /dev/null +++ b/packages/util/src/u8a/wrap.ts @@ -0,0 +1,49 @@ +// Copyright 2017-2021 @polkadot/util authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Originally from https://github.com/polkadot-js/extension/pull/743 + +import type { U8aLike } from '../types'; + +import { u8aConcat } from './concat'; +import { u8aEq } from './eq'; +import { u8aToU8a } from './toU8a'; + +export const U8A_WRAP_ETHEREUM = u8aToU8a('\x19Ethereum Signed Message:\n'); +export const U8A_WRAP_PREFIX = u8aToU8a(''); +export const U8A_WRAP_POSTFIX = u8aToU8a(''); + +const WRAP_LEN = U8A_WRAP_PREFIX.length + U8A_WRAP_POSTFIX.length; + +export function u8aIsWrapped (u8a: Uint8Array, withEthereum: boolean): boolean { + return ( + ( + u8a.length >= WRAP_LEN && + u8aEq(u8a.subarray(0, U8A_WRAP_PREFIX.length), U8A_WRAP_PREFIX) && + u8aEq(u8a.slice(-U8A_WRAP_POSTFIX.length), U8A_WRAP_POSTFIX) + ) || + ( + withEthereum && + u8a.length >= U8A_WRAP_ETHEREUM.length && + u8aEq(u8a.subarray(0, U8A_WRAP_ETHEREUM.length), U8A_WRAP_ETHEREUM) + ) + ); +} + +export function u8aUnwrapBytes (bytes: U8aLike): Uint8Array { + const u8a = u8aToU8a(bytes); + + // we don't want to unwrap Ethereum-style wraps + return u8aIsWrapped(u8a, false) + ? u8a.subarray(U8A_WRAP_PREFIX.length, u8a.length - U8A_WRAP_POSTFIX.length) + : u8a; +} + +export function u8aWrapBytes (bytes: U8aLike): Uint8Array { + const u8a = u8aToU8a(bytes); + + // if Ethereum-wrapping, we don't add our wrapping bytes + return u8aIsWrapped(u8a, true) + ? u8a + : u8aConcat(U8A_WRAP_PREFIX, u8a, U8A_WRAP_POSTFIX); +}